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