使用 VTable hacking 使用标准模块中的方法重载 COM class 方法
Overload COM class methods with methods from a standard module using VTable hacking
小问题 - 我刚刚通过使用低级复制内存 api.
更改其 VTable 中的条目来测试 class 的覆盖方法
背景
我取得了一些成功,并且可以交换 class 的 VTable 中的 2 个条目,如果它们具有相同的签名。所以 class 定义如下:
Option Explicit
Public Sub Meow()
Debug.Print "Meow"
End Sub
Public Sub Woof()
Debug.Print "Woof"
End Sub
... 生成这样的 VTable:
... 我可以交换位置 7 和 8 的条目以使 cls.Meow
打印 Woof
,反之亦然。我还可以将一个 class 的 VTable 中的条目与另一个完全不同的 VTable 的条目交换(前提是我不尝试通过调用 Me.anything
来取消引用隐式 this
指针)
所以我可以再做一个class
Option Explicit
Public Sub Tweet()
Debug.Print "Tweet"
End Sub
并将一个 Woof
的行为与另一个 Tweet
的行为交换。不太复杂,如果有人需要,我可以分享代码。
我不能做什么...
...但是,是否想出如何 将 class 方法与标准模块中的方法交换?
根据 this 文章,VBA 所基于的 COM 机制似乎需要 VBA 隐藏的 class 方法中的 2 个东西:
- 他们有一个隐含的
this
指针
- 他们 return 一个 HRESULT (
typedef long
)
所以我想
Public Sub Meow()
在 class 模块中 Class1
等同于
Public Function Meow(ByVal this As LongPtr) As Long
我也试过了
Public Function Meow(ByRef meObj As Class1) As Long
Public Function Meow(ByRef meObj As Class1) As LongPtr 'but HResult is 32 bit int
Public Sub Meow(ByVal this As LongPtr)
等但是当我尝试从 VTable 调用方法时 VBA 总是崩溃。所以我有点不知所措。我想知道在 64 位计算机上情况是否有所不同,或者标准模块函数是否对调用堆栈做了一些奇怪的事情。问题是我已经看到 examples of code 整个 VTable 是从标准模块函数组装而成的,所以我知道这是可能的,但只是不确定如何正确转换签名
如何使用标准模块中定义的方法覆盖 VTable 条目?
我对你的问题的评论只是部分正确。我仍然相信 Me
关键字在防止 class 方法的 'redirection' 成为标准 .bas 模块中的方法方面发挥了作用。但这仅适用于早期绑定。
IDispatch::Invoke 实际上可以毫无问题地调用 .bas 模块内的方法。您的初始方法签名是正确的:
Public Function Meow(ByRef meObj As Class1) As Long
Class1
代码:
Option Explicit
Public Sub Meow()
Debug.Print "Meow"
End Sub
Public Sub Woof()
Debug.Print "Woof"
End Sub
标准 .bas 模块中的代码:
Option Explicit
Sub Test()
Dim c As Object 'Must be late-binded!
Dim vTblPtr As LongPtr
Dim vTblMeowPtr As LongPtr
Dim originalMeow As LongPtr
'
Set c = New Class1
c.Meow 'Prints "Meow" to the Immediate Window
'
'The address of the virtual table
vTblPtr = MemLongPtr(ObjPtr(c))
'
'The address of the Class1.Meow method within the virtual table
vTblMeowPtr = vTblPtr + 7 * PTR_SIZE
'
'The current address of the Class1.Meow method
originalMeow = MemLongPtr(vTblMeowPtr)
'
'Replace the address of Meow with the one in a .bas module
MemLongPtr(vTblMeowPtr) = VBA.Int(AddressOf Moew)
'
c.Meow 'Prints "Meow in .bas" to the Immediate Window
'
'Revert the original address
MemLongPtr(vTblMeowPtr) = originalMeow
'
c.Meow 'Prints "Meow" to the Immediate Window
End Sub
Public Function Moew(ByVal this As Class1) As Long
Debug.Print "Meow in .bas"
End Function
我已经使用 LibMemory 进行内存操作。
如果您将 Meow
class 方法更改为 Function
而不是 Sub
那么您需要在.bas 模块中 Meow
方法中参数列表的末尾。
编辑#1
我想到了下面评论中讨论的问题,我能想到的唯一原因是 IDispatch 只能使用指向 IUnknown 接口的指针。
这意味着:
Public Function Meow(ByRef this As Class1) As Long
会使应用程序崩溃
但是,这有效:
Public Function Moew(ByVal this As Class1) As Long
Debug.Print "Meow in .bas"
End Function
因为传递 ByVal
在 IUnknown 上强制执行 QueryInterface 和 AddRef(退出范围时使用 Release)
这也有效:
Public Function Moew(ByRef this As IUnknown) As Long
Debug.Print "Meow in .bas"
End Function
编辑#2
抱歉再次编辑。
Invoke 方法不适用于指向 IUnknown 的指针。它使用指向 IDispatch 的指针。这可以通过以下方式检查:
Public Function Moew(ByVal this As LongPtr) As Long
Debug.Print this
Debug.Print "Meow in .bas"
End Function
这会将 ptr 打印到 IDispatch 接口。那么,为什么 ByRef this As Class1
会失败呢?为什么 ByVal this As Class1
和 ByRef this As IUnknown
有效?
ByRef this As Class1
我相信 VB 无法访问 VarPtr(this) 地址,因此我们正在读取我们不应该读取的内存。 IUnknown 接口上没有额外的 AddRef 或 Release,因为永远不会使用此声明调用该方法。当 Invoke 尝试调用该方法时,应用程序会崩溃。
ByVal this As Class1
该方法只是创建一个 VB 变量(在 VB 内存 space 上)并调用 AddRef
ByRef this As IUnknown
由于这不是双重接口,因此完成了对 QueryInterface 和 AddRef 的调用。 'this' 的内存地址在本地内存 space 上,与第二个示例相同。
小问题 - 我刚刚通过使用低级复制内存 api.
更改其 VTable 中的条目来测试 class 的覆盖方法背景
我取得了一些成功,并且可以交换 class 的 VTable 中的 2 个条目,如果它们具有相同的签名。所以 class 定义如下:
Option Explicit
Public Sub Meow()
Debug.Print "Meow"
End Sub
Public Sub Woof()
Debug.Print "Woof"
End Sub
... 生成这样的 VTable:
... 我可以交换位置 7 和 8 的条目以使 cls.Meow
打印 Woof
,反之亦然。我还可以将一个 class 的 VTable 中的条目与另一个完全不同的 VTable 的条目交换(前提是我不尝试通过调用 Me.anything
来取消引用隐式 this
指针)
所以我可以再做一个class
Option Explicit
Public Sub Tweet()
Debug.Print "Tweet"
End Sub
并将一个 Woof
的行为与另一个 Tweet
的行为交换。不太复杂,如果有人需要,我可以分享代码。
我不能做什么...
...但是,是否想出如何 将 class 方法与标准模块中的方法交换?
根据 this 文章,VBA 所基于的 COM 机制似乎需要 VBA 隐藏的 class 方法中的 2 个东西:
- 他们有一个隐含的
this
指针 - 他们 return 一个 HRESULT (
typedef long
)
所以我想
Public Sub Meow()
在 class 模块中 Class1
等同于
Public Function Meow(ByVal this As LongPtr) As Long
我也试过了
Public Function Meow(ByRef meObj As Class1) As Long
Public Function Meow(ByRef meObj As Class1) As LongPtr 'but HResult is 32 bit int
Public Sub Meow(ByVal this As LongPtr)
等但是当我尝试从 VTable 调用方法时 VBA 总是崩溃。所以我有点不知所措。我想知道在 64 位计算机上情况是否有所不同,或者标准模块函数是否对调用堆栈做了一些奇怪的事情。问题是我已经看到 examples of code 整个 VTable 是从标准模块函数组装而成的,所以我知道这是可能的,但只是不确定如何正确转换签名
如何使用标准模块中定义的方法覆盖 VTable 条目?
我对你的问题的评论只是部分正确。我仍然相信 Me
关键字在防止 class 方法的 'redirection' 成为标准 .bas 模块中的方法方面发挥了作用。但这仅适用于早期绑定。
IDispatch::Invoke 实际上可以毫无问题地调用 .bas 模块内的方法。您的初始方法签名是正确的:
Public Function Meow(ByRef meObj As Class1) As Long
Class1
代码:
Option Explicit
Public Sub Meow()
Debug.Print "Meow"
End Sub
Public Sub Woof()
Debug.Print "Woof"
End Sub
标准 .bas 模块中的代码:
Option Explicit
Sub Test()
Dim c As Object 'Must be late-binded!
Dim vTblPtr As LongPtr
Dim vTblMeowPtr As LongPtr
Dim originalMeow As LongPtr
'
Set c = New Class1
c.Meow 'Prints "Meow" to the Immediate Window
'
'The address of the virtual table
vTblPtr = MemLongPtr(ObjPtr(c))
'
'The address of the Class1.Meow method within the virtual table
vTblMeowPtr = vTblPtr + 7 * PTR_SIZE
'
'The current address of the Class1.Meow method
originalMeow = MemLongPtr(vTblMeowPtr)
'
'Replace the address of Meow with the one in a .bas module
MemLongPtr(vTblMeowPtr) = VBA.Int(AddressOf Moew)
'
c.Meow 'Prints "Meow in .bas" to the Immediate Window
'
'Revert the original address
MemLongPtr(vTblMeowPtr) = originalMeow
'
c.Meow 'Prints "Meow" to the Immediate Window
End Sub
Public Function Moew(ByVal this As Class1) As Long
Debug.Print "Meow in .bas"
End Function
我已经使用 LibMemory 进行内存操作。
如果您将 Meow
class 方法更改为 Function
而不是 Sub
那么您需要在.bas 模块中 Meow
方法中参数列表的末尾。
编辑#1
我想到了下面评论中讨论的问题,我能想到的唯一原因是 IDispatch 只能使用指向 IUnknown 接口的指针。
这意味着:
Public Function Meow(ByRef this As Class1) As Long
会使应用程序崩溃
但是,这有效:
Public Function Moew(ByVal this As Class1) As Long
Debug.Print "Meow in .bas"
End Function
因为传递 ByVal
在 IUnknown 上强制执行 QueryInterface 和 AddRef(退出范围时使用 Release)
这也有效:
Public Function Moew(ByRef this As IUnknown) As Long
Debug.Print "Meow in .bas"
End Function
编辑#2
抱歉再次编辑。
Invoke 方法不适用于指向 IUnknown 的指针。它使用指向 IDispatch 的指针。这可以通过以下方式检查:
Public Function Moew(ByVal this As LongPtr) As Long
Debug.Print this
Debug.Print "Meow in .bas"
End Function
这会将 ptr 打印到 IDispatch 接口。那么,为什么 ByRef this As Class1
会失败呢?为什么 ByVal this As Class1
和 ByRef this As IUnknown
有效?
ByRef this As Class1
我相信 VB 无法访问 VarPtr(this) 地址,因此我们正在读取我们不应该读取的内存。 IUnknown 接口上没有额外的 AddRef 或 Release,因为永远不会使用此声明调用该方法。当 Invoke 尝试调用该方法时,应用程序会崩溃。
ByVal this As Class1
该方法只是创建一个 VB 变量(在 VB 内存 space 上)并调用 AddRef
ByRef this As IUnknown
由于这不是双重接口,因此完成了对 QueryInterface 和 AddRef 的调用。 'this' 的内存地址在本地内存 space 上,与第二个示例相同。