Быстрый вопрос. Я только что тестировал методы перезаписи класса, изменяя записи в его VTable, используя низкоуровневый API памяти копирования.
У меня был некоторый успех, и я могу поменять местами 2 записи в VTable класса, если они имеют одинаковую подпись. Таким образом, определение класса выглядит следующим образом:
Option Explicit
Public Sub Meow()
Debug.Print "Meow"
End Sub
Public Sub Woof()
Debug.Print "Woof"
End Sub
... генерирует VTable следующим образом:
... и я могу поменять местами записи в позициях 7 и 8, чтобы cls.Meow
напечатать Woof
и наоборот. Я также могу поменять местами запись из VTable одного класса с VTable совершенно другого класса (при условии, что я не пытаюсь разыменовать неявный указатель this
, вызывая Me.anything
)
Так что я могу сделать еще один класс
Option Explicit
Public Sub Tweet()
Debug.Print "Tweet"
End Sub
и поменяйте поведение Woof
с одного на Tweet
с другого. Не слишком сложно, я могу поделиться кодом, если он нужен людям.
... однако выяснить, как поменять местами метод класса с методом из стандартного модуля?
Основываясь на этой статье, кажется, что механизм COM, на котором построен VBA, требует двух вещей методов класса, которые скрывает VBA:
this
typedef long
)Так что я подумал
Public Sub Meow()
в модуле класса 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)
и т. д. Но VBA всегда падает, когда я пытаюсь вызвать метод из VTable. Так что я немного в растерянности. Интересно, на 64-битном компьютере все по-другому, или стандартные функции модуля делают что-то странное со стеком вызовов? Дело в том, что я видел примеры кода, где вся VTable собирается из стандартных функций модуля, поэтому я знаю, что это возможно, но просто не знаю, как правильно преобразовать подписи.
Как перезаписать запись VTable методом, определенным в стандартном модуле?
Я был только частично прав в своем комментарии к вашему вопросу. Я по-прежнему считаю, что ключевое слово Me
играет роль в предотвращении «перенаправления» метода класса на метод внутри стандартного модуля .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
на Function
вместо Sub
, вам понадобится дополнительный параметр ByRef
в конце списка параметров в методе Meow
в модуле .bas.
РЕДАКТИРОВАТЬ №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
вызывает QueryInterface и AddRef в IUnknown (с 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
Я считаю, что адрес VarPtr(this) недоступен для VB, поэтому мы читаем память, которую не должны. Не похоже, что в интерфейсе IUnknown есть дополнительный AddRef или Release, потому что метод никогда не вызывается с использованием этого объявления. Приложение просто падает, когда Invoke пытается вызвать метод.
ByVal this As Class1
Метод просто создает переменную VB (в пространстве памяти VB) и вызывает AddRef.
ByRef this As IUnknown
Поскольку это не двойной интерфейс, выполняется вызов QueryInterface и AddRef. Адрес памяти this находится в локальной памяти, как и во втором примере.
Спасибо за продолжение! Один вопрос; вы используете ByVal вместо ByRef; это опечатка? Удалось ли вам вызвать какие-либо методы или свойства this
?
Извинения. Я даже не понял. Я сделал это по привычке. Давным-давно, перехватывая IUnknown::Release, я обнаружил, что когда не-VB-функция вызывает VB-функцию, лучше передать параметр экземпляра ByVal
, чтобы избежать сбоев. Я придумаю объяснение, и если я придумаю достойное, я соответствующим образом отредактирую ответ. Да, использование кода в ответе позволяет мне получить доступ ко всем методам класса в методе Meow
модуля .bas. Поднятие ошибок также работает корректно.
@Greedo Отредактировал ответ, надеюсь, я прав в своем предположении. Кстати, ты всегда задаешь правильные вопросы. Отличная работа!
@Greedo Я сделал еще одно редактирование. Надеюсь, на этот раз я правильно понял.
@Greedo Ваш вопрос помог мне обновить логику в логике перенаправления экземпляра (см. Редактирование № 1). Спасибо вам за это! Кажется, что действительно точка входа в класс при раннем связывании имеет некоторые дополнительные механизмы безопасности, в отличие от позднего связывания.
На x64 поведение точно такое же. Однако вы можете «перенаправить» методы IUnknown и IDispatch в функцию внутри модуля .bas (а также методы IEnumVariant в вашем связанном примере). Я предполагаю, что они работают, потому что они не VB. Я думаю, что вы не можете перенаправить метод класса VB из-за того, как работают классы VB. Рассмотрим ключевое слово
Me
(которое ведет себя как функция/свойство). Вы не могли бы воспроизвести поведениеMe
в модуле .bas. Должно быть больше вещей, происходящих за кулисами. Возможно, соглашение о вызовах также отличается.