2

The AddressOf operator works only with methods inside standard .bas modules. I am using the following code to retrieve the addresses of class methods:

Option Explicit

#If VBA7 Then
    Private Declare PtrSafe Function DispCallFunc Lib "oleaut32.dll" (ByVal pvInstance As LongPtr, ByVal oVft As LongPtr, ByVal cc As tagCALLCONV, ByVal vtReturn As Integer, ByVal cActuals As Long, ByRef prgvt As Integer, ByRef prgpvarg As LongPtr, ByRef pvargResult As Variant) As Long
    Private Declare PtrSafe Function DispGetIDsOfNames Lib "oleaut32.dll" (ByVal ptinfo As LongPtr, ByVal rgszNames As LongPtr, ByVal cNames As Long, ByVal rgDispId As LongPtr) As Long
#Else
    Private Declare Function DispCallFunc Lib "oleaut32.dll" (ByVal pvInstance As Long, ByVal oVft As Long, ByVal cc As tagCALLCONV, ByVal vtReturn As Integer, ByVal cActuals As Long, ByRef prgvt As Integer, ByRef prgpvarg As Long, ByRef pvargResult As Variant) As Long
    Private Declare Function DispGetIDsOfNames Lib "oleaut32.dll" (ByVal ptinfo As Long, ByVal rgszNames As Long, ByVal cNames As Long, ByVal rgDispId As Long) As Long
#End If

Private Type INVOKE_ARGS
    args() As Variant
    argsVT() As Integer
    #If VBA7 Then
        argsPtrs() As LongPtr
    #Else
        argsPtrs() As Long
    #End If
    argsCount As Long
End Type

#If Win64 Then
    Private Const PTR_SIZE As Long = 8
#Else
    Private Const PTR_SIZE As Long = 4
#End If

'IDispatch derives from the IUnknown interface
Private Enum IDispatchVtblOffset
    oQueryInterface = PTR_SIZE * 0   'IUnknown
    oAddRef = PTR_SIZE * 1           'IUnknown
    oRelease = PTR_SIZE * 2          'IUnknown
    oGetTypeInfoCount = PTR_SIZE * 3 'IDispatch
    oGetTypeInfo = PTR_SIZE * 4      'IDispatch
    oGetIDsOfNames = PTR_SIZE * 5    'IDispatch
    oInvoke = PTR_SIZE * 6           'IDispatch
End Enum

'ITypeInfo derives from the IUnknown interface
Private Enum ITypeInfoVtblOffset
    oQueryInterface = PTR_SIZE * 0   'IUnknown
    oAddRef = PTR_SIZE * 1           'IUnknown
    oRelease = PTR_SIZE * 2          'IUnknown
    oGetTypeAttr = PTR_SIZE * 3
    oGetTypeComp = PTR_SIZE * 4
    oGetFuncDesc = PTR_SIZE * 5
    oGetVarDesc = PTR_SIZE * 6
    oGetNames = PTR_SIZE * 7
    oGetRefTypeOfImplType = PTR_SIZE * 8
    oGetImplTypeFlags = PTR_SIZE * 9
    oGetIDsOfNames = PTR_SIZE * 10
    oInvoke = PTR_SIZE * 11
    oGetDocumentation = PTR_SIZE * 12
    oGetDllEntry = PTR_SIZE * 13
    oGetRefTypeInfo = PTR_SIZE * 14
    oAddressOfMember = PTR_SIZE * 15
    oCreateInstance = PTR_SIZE * 16
    oGetMops = PTR_SIZE * 17
    oGetContainingTypeLib = PTR_SIZE * 18
    oReleaseTypeAttr = PTR_SIZE * 19
    oReleaseFuncDesc = PTR_SIZE * 20
    oReleaseVarDesc = PTR_SIZE * 21
End Enum

Private Enum tagINVOKEKIND
    INVOKE_FUNC = &H1
    INVOKE_PROPERTYGET = &H2
    INVOKE_PROPERTYPUT = &H4
    INVOKE_PROPERTYPUTREF = &H8
End Enum

'Calling Conventions
Private Enum tagCALLCONV
    CC_FASTCALL = 0
    CC_CDECL = 1
    CC_MSCPASCAL = 2
    CC_PASCAL = CC_MSCPASCAL
    CC_MACPASCAL = 3
    CC_STDCALL = 4
    CC_FPFASTCALL = 5
    CC_SYSCALL = 6
    CC_MPWCDECL = 7
    CC_MPWPASCAL = 8
    CC_MAX = 9
End Enum

Const S_OK As Long = 0

#If VBA7 Then
Public Function GetAddressOfClassMethod(ByVal classInstance As Object, ByVal methodName As String) As LongPtr
#Else
Public Function GetAddressOfClassMethod(ByVal classInstance As Object, ByVal methodName As String) As Long
#End If
    #If VBA7 Then
        Dim iDispatchPtr As LongPtr
        Dim iTypeInfoPtr As LongPtr
    #Else
        Dim iDispatchPtr As Long
        Dim iTypeInfoPtr As Long
    #End If
    Dim localeID As Long 'Not really needed. Could pass 0 instead
    '
    'Get a pointer to the IDispatch interface
    iDispatchPtr = ObjPtr(GetDefaultInterface(classInstance))
    '
    'Get a pointer to the ITypeInfo interface
    localeID = Application.LanguageSettings.LanguageID(msoLanguageIDUI)
    IDispatch_GetTypeInfo iDispatchPtr, 0, localeID, iTypeInfoPtr
    '
    Dim arrNames(0 To 0) As String: arrNames(0) = methodName
    Dim arrIDs(0 To 0) As Long
    '
    'Get ID of required member
    DispGetIDsOfNames iTypeInfoPtr, VarPtr(arrNames(0)), 1, VarPtr(arrIDs(0))
    '
    'Get address of member
    ITypeInfo_AddressOfMember iTypeInfoPtr, arrIDs(0), INVOKE_FUNC, GetAddressOfClassMethod
End Function

'*******************************************************************************
'Returns the default interface for an object
'All VB intefaces are dual interfaces meaning all interfaces are derived from
'   IDispatch which in turn is derived from IUnknown. In VB the Object datatype
'   stands for the IDispatch interface.
'Casting from a custom interface (derived only from IUnknown) to IDispatch
'   forces a call to QueryInterface for the IDispatch interface (which knows
'   about the default interface)
'*******************************************************************************
Private Function GetDefaultInterface(obj As IUnknown) As Object
    Set GetDefaultInterface = obj
End Function

'*******************************************************************************
'IDispatch::GetTypeInfo
'*******************************************************************************
#If VBA7 Then
Private Function IDispatch_GetTypeInfo(ByVal iDispatchPtr As LongPtr, ByVal iTInfo As Long, ByVal lcid As Long, ByRef ppTInfo As LongPtr) As Long
#Else
Private Function IDispatch_GetTypeInfo(ByVal iDispatchPtr As Long, ByVal iTInfo As Long, ByVal lcid As Long, ByRef ppTInfo As Long) As Long
#End If
    Dim hResult As Long
    '
    With CreateInvokeArgs(iTInfo, lcid, VarPtr(ppTInfo))
        hResult = DispCallFunc(iDispatchPtr, IDispatchVtblOffset.oGetTypeInfo, CC_STDCALL, vbLong, .argsCount, .argsVT(0), .argsPtrs(0), IDispatch_GetTypeInfo)
    End With
    If hResult <> S_OK Then Err.Raise hResult, "IDispatch_GetTypeInfo"
End Function

'*******************************************************************************
'ITypeInfo::AddressOfMember
'*******************************************************************************
#If VBA7 Then
Private Function ITypeInfo_AddressOfMember(ByVal iTypeInfoPtr As LongPtr, ByVal memid As Long, ByVal invKind As tagINVOKEKIND, ByRef ppv As LongPtr) As Long
#Else
Private Function ITypeInfo_AddressOfMember(ByVal iTypeInfoPtr As Long, ByVal memid As Long, ByVal invKind As tagINVOKEKIND, ByRef ppv As Long) As Long
#End If
    Dim hResult As Long
    '
    With CreateInvokeArgs(memid, invKind, VarPtr(ppv))
        hResult = DispCallFunc(iTypeInfoPtr, ITypeInfoVtblOffset.oAddressOfMember, CC_STDCALL, vbLong, .argsCount, .argsVT(0), .argsPtrs(0), ITypeInfo_AddressOfMember)
    End With
    If hResult <> S_OK Then Err.Raise hResult, "ITypeInfo_AddressOfMember"
End Function

'*******************************************************************************
'Helper function that creates the necessary arrays to use with DispCallFunc
'Passing arguments:
'   - ByVal: pass the arg
'   - ByRef: pass VarPtr(arg)
'*******************************************************************************
Private Function CreateInvokeArgs(ParamArray args() As Variant) As INVOKE_ARGS
    With CreateInvokeArgs
        .argsCount = UBound(args) + 1 'ParamArray is always 0-based (LBound)
        If .argsCount = 0 Then
            ReDim .argsVT(0 To 0)
            ReDim .argsPtrs(0 To 0)
            Exit Function
        End If
        '
        .args = args 'Avoid ByRef issues by making a copy
        ReDim .argsVT(0 To .argsCount - 1)
        ReDim .argsPtrs(0 To .argsCount - 1)
        Dim i As Long
        '
        'For Each is not used because it does copies of the values inside the
        '   array and we need the actual addresses of the values (ByRef)
        For i = 0 To .argsCount - 1
            .argsVT(i) = VarType(.args(i))
            .argsPtrs(i) = VarPtr(.args(i))
        Next i
    End With
End Function

Assuming a Class1 class that has a Name method, I could use the above like this:

Debug.Print GetAddressOfClassMethod(New Class1, "Name")

The approach always works fine on x32 and most of the time on x64. The problem is that sometimes it causes a crash on x64. The crash happens only after the ITypeInfo_AddressOfMember call. The IDispatch_GetTypeInfo never causes a crash.

I haven't posted the code here, but I also call other methods of the ITypeInfo interface and even of the ITypeComp interface but I don't get crashes.

Am I doing something wrong? Any ideas of why the crashes occur?

Cristian Buse
  • 4,020
  • 1
  • 13
  • 34
  • Maybe it belongs to the 8 byte padding used by 64-bit VBA? See the update of my question here: https://stackoverflow.com/q/61391802/7658533 – AHeyne Dec 30 '20 at 13:38
  • @UnhandledException Thanks for the link. I am not using any structs (custom types) here so padding is not an issue. – Cristian Buse Dec 30 '20 at 14:17
  • If you were able to obtain the instance method pointer correctly each time, to what use would you put it? The number itself is completely useless for the outside world (e.g. WinAPI callbacks), and in the realm of VBA, there are much easier ways to call objects dynamically than messing with pointers. – GSerg Dec 30 '20 at 17:05
  • 1
    @GSerg The only purpose is to understand the mechanism behind VB which is very useful for finding the cause of other bugs or coming up with new approaches (e.g. [Private Initializer](https://codereview.stackexchange.com/questions/253233/private-vba-class-initializer-called-from-factory-2)). A lot of the stuff I know now about the workings of VB is because I've tested and played with the limits of the language. – Cristian Buse Dec 30 '20 at 17:24
  • Using only a much more trivial CopyMemory method I had [crashes when porting from 32 to 64 bits](https://stackoverflow.com/questions/60943510/trying-to-reverse-objptr-to-an-object-im-getting-kernel-not-found-error-53-in-6/60963247#60963247). It all boils down to assure the size of object based on your table of sizes **ITypeInfoVtblOffset** and **IDispatchVtblOffset** versus get actual size through LenB(Obj). Maybe it is worth to do [some tests to compare](https://codereview.stackexchange.com/a/239725/186595). – Marcelo Scofano Diniz Dec 30 '20 at 20:48
  • @MarceloScofanoDiniz It's not the case here. BTW have a look at the [WeakReference](https://github.com/cristianbuse/VBA-WeakReference) repo which is way superior to the one you linked because it deals with instances of the same class that can occupy the same memory address after the initial instance has terminated. Link to CR [here](https://codereview.stackexchange.com/a/249125/227582) – Cristian Buse Dec 30 '20 at 21:10
  • Ok, I'll sit and digest. Very good stuff you have there; thanks for sharing. – Marcelo Scofano Diniz Dec 30 '20 at 21:33
  • Very impressive all thread at CR. Congratulations; I too got 64 bits crashes. – Marcelo Scofano Diniz Dec 31 '20 at 02:28
  • Ever make any progress with this? – Greedo Feb 25 '22 at 13:35
  • 1
    @Greedo Some related progress, yes. I shared it with you on GitHub (a while ago) but I think you missed the notification and I deleted the repo a few weeks later. I created a [gist](https://gist.github.com/cristianbuse/b651a3cd740e27a78ea90bca9f7af4d1) now. It allows getting the ITypeInfo pointer with native calls. There are 3 references you need but I created a file in the gist with those. See the ```Testing``` module. – Cristian Buse Feb 25 '22 at 14:27
  • @Greedo The nice part is that there is no need to find the address of a class method as long as you redirect via an interface in which case you know the position in the vtbl. The bad part is that it only works for VB classes – Cristian Buse Feb 25 '22 at 14:37
  • @Greedo Forgot to mention that I chose to use ```IUnknown``` instead of raw pointers so there is no risk that the object gets destroyed. Plus ```Release``` gets called automatically. The only thing is to copy the correct ```ptr``` via ```MemLongPtr``` or ```CopyMemory```. Maybe this could be useful in your project. Can't wait to see what you come up with :) – Cristian Buse Feb 25 '22 at 21:04
  • 1
    @CristianBuse Thanks for all this. I've got something mostly working now, was useful to have your examples to draw inspiration from! – Greedo Feb 27 '22 at 09:23

0 Answers0