How to pass a COM Object/Interface to a VBA method without changing its ObjPtr (i.e. no IUnknown::QueryInterface)?

342 Views Asked by At

I'm wondering if there's any way to declare the parameter to a method in such a way that it always preserves the exact ObjPtr/Interface supplied.

I've tried every combo of As Object/IUnknown, ByRef/ByVal I can think of:

Option Explicit

Sub t()
    Dim obj As New Class1
    Debug.Print "--- Default Interface ---"; ObjPtr(obj) 'check objptr before...
    A obj: B obj: C obj: D obj
    Debug.Print "--- Default Interface ---"; ObjPtr(obj) '... and after to make sure it wasn't modified by calling the methods
    
    Dim asObject As Object
    Set asObject = obj
    Debug.Print "------- As Object -------"; ObjPtr(asObject)
    A obj: B obj: C obj: D obj
    Debug.Print "------- As Object -------"; ObjPtr(asObject)
    
    Dim asUnk As IUnknown
    Set asUnk = obj
    Debug.Print "-------- As Unk ---------"; ObjPtr(asUnk)
    A obj: B obj: C obj: D obj
    Debug.Print "-------- As Unk ---------"; ObjPtr(asUnk)
End Sub

Sub A(ByVal obj As IUnknown)
    Debug.Print "ByVal IUnk", ObjPtr(obj)
End Sub

Sub B(ByVal obj As Object)
    Debug.Print "ByVal Object", ObjPtr(obj)
End Sub

Sub C(ByRef obj As IUnknown)
    Debug.Print "ByRef IUnk", ObjPtr(obj)
End Sub

Sub D(ByRef obj As Object)
    Debug.Print "ByRef Object", ObjPtr(obj)
End Sub

But I get this (coloured to show matching ObjPtrs):

--- Default Interface --- 1446521360 
ByVal IUnk     1446521388 
ByVal Object   1446521360 
ByRef IUnk     1446521388 
ByRef Object   1446521360 
--- Default Interface --- 1446521360 

------- As Object ------- 1446521360 
ByVal IUnk     1446521388 
ByVal Object   1446521360 
ByRef IUnk     1446521388 
ByRef Object   1446521360 
------- As Object ------- 1446521360 

-------- As Unk --------- 1446521388 
ByVal IUnk     1446521388 
ByVal Object   1446521360 
ByRef IUnk     1446521388 
ByRef Object   1446521360 
-------- As Unk --------- 1446521388 

Which shows that regardless of the ObjPtr of the variable in calling method t(), the ObjPtr always matches the declared type in the sub routine. I want some way to preserve the ObjPtr.

Of course this will work:

E ObjPtr(pObj)

'...

Sub E(ByVal pObj As LongPtr)

But I want to pass around objects, not pointers for 1: ref-count safety and 2: nicer api (I am designing functions for use by people who don't know what pointers are, I want to hide that complexity)


Example

Public Function CallCOMObjectVTableEntry( _ 
        ??? COMInterface As ???, _ 
        ByVal VTableByteOffset As LongPtr, _
        ByVal FunctionReturnType As CALLRETURNTUYPE_ENUM, _
        ParamArray FunctionParameters() As Variant _ 
    ) As Variant

    Dim vParams() As Variant
    '@Ignore DefaultMemberRequired: apparently not since this code works fine
    vParams() = FunctionParameters()             ' copy passed parameters, if any
    LetSet(CallCOMObjectVTableEntry) = DispCallFunctionWrapper(ObjPtr(COMInterface), VTableByteOffset, FunctionReturnType, CC_STDCALL, vParams)
End Function

I want to accept a very specific interface since I need to get the right offset in memory to the VTable entry. I'm hoping this is possible because the ObjPtr function itself does exactly what I want - receives a COM Interface without coercing it into another form.

1

There are 1 best solutions below

0
Cristian Buse On

Whenever we pass pass an object derived from IDispatch to a function that expects a parameter of type IUnknown or the other way around, inevitably there will be a call made to IUnknown::QueryInterface behind the scenes and the interface reaching the function won't be the same as the one we passed because of the implicit cast.

Without using raw pointers, which you already mentioned, the only way I can think of to go around the implicit QueryInterface is to declare the function parameter as Variant. Something like:

Option Explicit

Sub Main()
    Dim c As New Class1
    Dim u As IUnknown: Set u = c

    Debug.Print ObjPtr(c)
    TestCOMOBject c

    Debug.Print ObjPtr(u)
    TestCOMOBject u
End Sub

Public Function TestCOMOBject(ByVal COMOBject As Variant) As Variant
    If Not IsObject(COMOBject) Then
        If VarType(COMOBject) <> vbDataObject Then Exit Function
    End If
    If COMOBject Is Nothing Then Exit Function
    '
    Debug.Print ObjPtr(COMOBject)
    Debug.Print
End Function

which I know it is not ideal, because now we need the extra checks which are made at run-time as opposed to compile-time.

You could expose 2 separate methods, one expecting Object and one expecting IUnknown but there is no way to enforce the user to call the correct one anyway.

So, it's either Variant or raw pointer.