2

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.

Greedo
  • 4,967
  • 2
  • 30
  • 78
  • I don't understand what you are trying to achieve in the first place. Why do you care about QueryInterface? Why using pointers? There was a reason why most of these functions are undocumented. Just pass ByVal Object type. PS: there may be more than one vtable. – Simon Mourier Aug 24 '22 at 17:45
  • @SimonMourier I tried to show with the example; as you say there is one VTable for each interface of the class (sometimes it is the same one just extended) and ObjPtr gives the address of a single one of those interfaces. I am offsetting from the start of any particular VTable to invoke a method on it - why? Often because it's a non exposed VBA interface (IUnknown, IDispatch) or non dual interface (IEnumVariant) so there is no other way to invoke the methods manually (I cannot declare a variable of that interface type)... – Greedo Aug 24 '22 at 22:09
  • ...I am making a nice wrapper around `DispCallFunc` to do the argument prep, passing args as ParamArray, default params for VBA COM stuff like enforcing STDCALL. Crucially, I need the correct interface pointer to be supplied. Based on the experiment in the question `ByVal Object` calls `IUnk::QI` on the supplied COM interface to get `IDispatch`, meaning if I pass something that isn't a Dual/automation interface extending `IDispatch`, the new pointer in the variable will no longer point to the caller supplied VTable but instead whatever VTable `IUnknown::QueryInterface(IID_IDispatch)` points to – Greedo Aug 24 '22 at 22:14
  • Sorry I still don't understand what problem you're trying to solve initially and I don't understand what the example is supposed to do, why do you need to access vtables pointers, etc. :-) ByVal Object will not necessary call QI for IDispatch, VB determines the (internally variant) type of object and act differently depending on this type. It will query for IDispatch only if needed. – Simon Mourier Aug 25 '22 at 06:26
  • @SimonMourier That's fine:) Ok concrete example, I do a lot of stuff with vtables but here's what I'm currently working on: First; I am calling `IDispatch::Invoke` manually on a class to have control over passing `dispid = -4` (this is how to get an enumerator for classes like Dictionary which don't have a `[_NewEnum]` method). So I use the offset in the IDispatch VTable to call Invoke - this can't be done another way since VBA doesn't let you declare variables of that type (As Object hides the methods). Second, I call `IEnumVARIANT::Next` to manually advance the enumerator from step one... – Greedo Aug 25 '22 at 08:29
  • ... This again requires VTable magic, as IEnumVARIANT is not a dual interface, it is derived from IUnknown only, which VBA doesn't support calls to. So for both these steps I want the same friendly helper function above to wrap the args, make the call, do some defaults. In step 1, my interface is Dual so `As Object` does not call QI and the VTable is not changed. The issue is, in step 2, my interface is not Dual, so I need to pass the raw IUnknown without VBA attempting to swap to the IDispatch interface that the enumerator may or may not support, but will definitely be the wrong vtable... – Greedo Aug 25 '22 at 08:35
  • ... And vice-versa if I pass the param As IUnknown, now step 1 fails as VBA retrieves a different VTable to the one with IDispatch defined (as I show in the question - there seems to be no way to allow both IUnknown and IDispatch extending interfaces to be passed without automatically QI from one to the other and losing the correct vtable.) However the objptr function itself doesn't mess the interface you supply to it, so I want that behaviour for my helper function. – Greedo Aug 25 '22 at 08:39
  • If that doesn't help we can discuss in chat? – Greedo Aug 25 '22 at 08:40
  • My VB6 is a bit rusty but sure, let's chat – Simon Mourier Aug 25 '22 at 08:59
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/247544/discussion-between-simon-mourier-and-greedo). – Simon Mourier Aug 25 '22 at 09:00
  • @Greedo This can be done as you already saw a while ago [here](https://gist.github.com/cristianbuse/b651a3cd740e27a78ea90bca9f7af4d1#file-libclassvb-bas). I even explained in the description of the QueryInterface function as to why. So, copy the correct object pointer to an IUnknown variable but using a memory copy, then pass around the IUnknown instance around and it will release correctly. Just don't cast to IDispatch/Object. – Cristian Buse Aug 25 '22 at 21:41
  • @CristianBuse Ah yes I remember seeing that but in a different context. Still, that doesn't really address the interface of the function - I can't pass the object to memcopy to lock it into an IUnknown, only its objptr, so the user still needs to supply an ObjPtr? Or any Object + the IID so I can do a query interface behind the scenes? Definitely a lot better than nothing as I can accept a pointer but use it safely as an object with your techniques, yet from the p.o.v of user interface it's not quite what I'm after. If I'm understanding correctly? – Greedo Aug 26 '22 at 13:21
  • @Greedo Apologies, it was late and I was lazy to read the whole question and comments. I was thinking that you need a safe way to pass the object around. Will add an answer to address your real question. – Cristian Buse Aug 26 '22 at 15:02
  • @CristianBuse No worries, that was half of my question:). The other half is nicer user interface – Greedo Aug 26 '22 at 15:03

1 Answers1

0

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.

Cristian Buse
  • 4,020
  • 1
  • 13
  • 34