core.sys.windows.com.ComObject apparently has wrongly laid out Vtable

Carl Sturtivant sturtivant at gmail.com
Sat Mar 23 20:29:28 UTC 2024


Here is a proof of concept that a genuine COM object can be 
created in D that works in the outside word, yet NOT using 
`core.sys.windows.unknwn.IUnknown`, following a recipe I 
suggested above.

The vtbl[] of the COM object is the vtbl[] of the D class object 
and is NOT the vtbl[] of a D interface inside it, the way it 
would have been had the class been implemented conventionally by 
inheriting from `core.sys.windows.unknwn.IUnknown`.
```D
import core.sys.windows.windows;
import core.sys.windows.com;

import std.stdio;

extern(C++)
class COMclassObject {
     extern(Windows):
   //Does NOT inherit from D interface IUnknown
   //Specifies extern(C++) for the class:
   //    in hope of a COM compatible vtbl[]
   //    with QueryInterface in slot 0
   //Specifies extern(Windows) for the methods:
   //    in hope of COM compatible calling (__stdcall)
   //Hope that methods are added to vtbl[] for COM:
   //    in the order they are written
   //Cast directly to void* :
   //    to get a pointer to its vtbl[] as per ABI docs here:
   // https://dlang.org/spec/abi.html#classes

     HRESULT QueryInterface(REFIID riid, void** ppvObject) {
writeln("Called QueryInterface.");
         if( *riid == IID_IUnknown ) {
             *ppvObject = cast(void*)this; //cast directly to void*
             AddRef();
             return S_OK;
         }
         *ppvObject = null;
         return E_NOINTERFACE;
     }
     import core.atomic;

     ULONG AddRef() {
writeln("Called AddRef.");
         return atomicOp!"+="(*cast(shared)&count, 1);
     }
     ULONG Release() { //e.g. GC version, does not destroy
writeln("Called Release.");
         LONG lRef = atomicOp!"-="(*cast(shared)&count, 1);
         return cast(ULONG)lRef;
     }
     LONG count = 0;
}

void main() {
     //act as COM server
         auto comClassObject = new COMclassObject();
     //cast to void* to get COM interface
         auto pInterface = cast(void*)comClassObject;

     //instead of sending pInterface to an outside client
     //simulate being that COM client here

     //test as COM client conventionally here
     //for convenience, using D's interface IUnknown
     // https://dlang.org/spec/interface.html#com-interfaces
         auto unknown = cast(IUnknown)pInterface;
     //get a COM interface using QueryInterface
         void* pUnk;
     write("Calling QueryInterface: ");
         HRESULT hr = unknown.QueryInterface(&IID_IUnknown, &pUnk);
         assert(SUCCEEDED(hr));
    //use the resulting interface conventionally as test
         auto unk = cast(IUnknown)pUnk;
     write("Calling AddRef: ");
         unk.AddRef();
   //check that the actual pointers involved are identical
     writefln("comClassObject: %x", pointer(comClassObject));
     writefln("    pInterface: %x", pointer(pInterface));
     writefln("       unknown: %x", pointer(unknown));
     writefln("          pUnk: %x", pointer(comClassObject));
     writefln("           unk: %x", pointer(comClassObject));
}

     //examine a class or interface
     //as its actual pointer (no cast)
union vunion(REF) {
     REF refvar;
     void* ptr;
     this(REF r) { refvar = r; }
}
void* pointer(REF)(REF refvar) {
     return vunion!REF(refvar).ptr;
}
```
Output:
```
Calling QueryInterface: Called QueryInterface.
Called AddRef.
Calling AddRef: Called AddRef.
comClassObject: 21de9ef0010
     pInterface: 21de9ef0010
        unknown: 21de9ef0010
           pUnk: 21de9ef0010
            unk: 21de9ef0010
```
Without `extern(C++)` this produces a crash because the vtbl[] 
has QueryInterface in slot 1 according to the D ABI docs linked 
in the above code, with type information in slot 0 where COM 
expects QueryInterface to be.

However, I wondered whether the `extern(Windows):` qualification 
was operating inside an `extern(C++):` class, or were those calls 
accidentally successful with mismatched calling conventions, and 
the stack/registers were in some way silently corrupted. So I 
commented `extern(Windows):` out and recompiled and it still 
worked!

So then I found out about this. [Windows x64 Calling 
Conventions](https://stackoverflow.com/questions/66398953/writing-a-microsoft-fastcall-64-bit-assembly-function/66403563#66403563). With a 64-bit compilation there's only one calling convention  and that apparently includes win32 API calls. So `extern(Windows):` is unnecessary and makes no difference!

It seems that if you only care about 64-bits, the annotation 
`extern(C++):` fixes up a COM compatible vtbl[] and you're in 
business provided you take care with method order-of-declaration 
so you're compatible with the COM interface you're implementing.

I then compiled and ran at 32-bits where there *is* a well-known 
difference. With `extern(Windows):` for the methods the program 
again worked, with output
```
Calling QueryInterface: Called QueryInterface.
Called AddRef.
Calling AddRef: Called AddRef.
comClassObject: 2640010
     pInterface: 2640010
        unknown: 2640010
           pUnk: 2640010
            unk: 2640010
```
whereas with `extern(Windows):` commented out, the output was
```
Calling QueryInterface: Called QueryInterface.

object.Error@(0): Access Violation
----------------
0x00A91045
0x00A910ED
0x00A9FAF7
0x00A9FA57
0x00A9F8C6
0x00A9B2EC
0x00A912A7
0x76A4FCC9 in BaseThreadInitThunk
0x770E7C5E in RtlGetAppContainerNamedObjectPath
0x770E7C2E in RtlGetAppContainerNamedObjectPath
```
suggesting a calling convention mismatch.

Either way, COM can apparently be implemented with classes and 
not interfaces. It seems this behavior of `extern(C++):` classes 
in D comes from the way the [C++ single inheritance interface in 
D](https://dlang.org/spec/abi.html#interfaces) works. We might 
expect that the vtbl[] of a C++ interface in D starts without D's 
type information which is irrelevant to C++, and in fact by the 
above proof-of-concept conforms to the COM standard. Apparently 
the vtbl[] of an `extern(C++):` class in D counts as a C++ 
interface in D.

Is there any text in the documentation which when juxtaposed 
would enable me to overtly infer all of this? I can't find such.

Please confirm or deny that these conclusions are correct in 
general.


More information about the Digitalmars-d mailing list