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
.
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. 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 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.