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

Carl Sturtivant sturtivant at gmail.com
Tue Mar 19 18:09:03 UTC 2024


Very interesting, and thank you.

It's the cast I am using! In the notation of your code, by 
casting comObject directly to C_IUnknown* I am getting a 
different pointer than you do by first casting to drtcom.IUnknown 
and then casting to C_IUnknown*. There are various other subtle 
differences going on here too. In my code the Vtables are the 
same despite the cast I use. But you get your Vtable from the 
drtcom.IUnknown variable, not the comObject variable. The results 
are different. Here's a modification of your main function that 
shows what's going on

```D
void main() {
     auto comObject = new drtcom.ComObject;
     auto iunknownObject = cast(drtcom.IUnknown)comObject;

     writeln("initial count ", comObject.count);

      {
          C_IUnknown* ip1 = cast(C_IUnknown*)iunknownObject;
          writeln("               ip1 vtable: ", ip1.lpVtbl);

          auto ipaddref = cast(void*)ip1.lpVtbl.AddRef;
          writeln("               ip1 AddRef: ", ipaddref);
      }

      {
          C_IUnknown* ip2 = cast(C_IUnknown*)comObject;
          writeln("               ip2 vtable: ", ip2.lpVtbl);

          auto ipaddref = cast(void*)ip2.lpVtbl.AddRef;
          writeln("               ip2 AddRef: ", ipaddref);
      }


      {
          auto vtable = iunknownObject.__vptr;
          writeln("   iunknownObject vtable: ", vtable);

          auto addref = 
cast(void*)(&iunknownObject.AddRef).funcptr;
          writeln("   iunknownObject AddRef: ", addref);
      }

      {
          auto vtable = comObject.__vptr;
          writeln("        comObject vtable: ", vtable);

          auto addref = cast(void*)(&comObject.AddRef).funcptr;
          writeln("        comObject AddRef: ", addref);
      }

      {
          comObject.AddRef();
          writeln("After D com object count: ", comObject.count);

          C_IUnknown* ip_old = cast(C_IUnknown*)iunknownObject;
          C_IUnknown* ip = cast(C_IUnknown*)comObject;
          writefln("ip     = %s ", ip);
          writefln("ip_old = %s", ip_old);
          ip.lpVtbl.AddRef(ip);
          writeln("After C com object count: ", comObject.count);
      }
}
```
The output is as follows.
```
initial count 0
                ip1 vtable: 7FF7EA4318A8
                ip1 AddRef: 7FF7EA3C2C90
                ip2 vtable: 7FF7EA4318E0
                ip2 AddRef: 7FF7EA3C7800
    iunknownObject vtable: 7FF7EA4318A8
    iunknownObject AddRef: 7FF7EA3C2C90
         comObject vtable: 7FF7EA4318E0
         comObject AddRef: 7FF7EA3C2D50
After D com object count: 1
ip     = 20227481000
ip_old = 20227481010
After C com object count: 1
```
I am confident that casting a D interface variable to a pointer 
works at the binary level in the expected way, especially for 
drtcom.IUnknown because the only reasonably simple assumption the 
compiler can make is that it is a valid COM object, not something 
that came from D.

The cast of a ComObject to a pointer produces something 16 bytes 
earlier than the actual start of the COM object according to the 
working cast, suggesting that the compiler is using some trickery 
in it's internal representation of COM objects made from D 
classes that inherit from the D interface IUnknown.

Intuitively, D may make a "normal" object with the "COM" object 
inside it offset by 16 bytes: two words at 64 bits. We might 
guess this is the two usual words in a class object, a Vtable 
pointer and a monitor. We might guess that a "COM" Vtable pointer 
follows that inside the "COM" part and it points to an offset 
inside the "normal" vtable so as to start with the necessary 
QueryInterface, AddRef, Release.

However, when casting such an object to a pointer, a pointer to 
the normal object is produced, NOT a pointer to the internal 
"COM" object. This is possible because D knows the type of the 
object because it is manufactured by D. This is responsible for 
the bad behavior shown in my initial post.

If the ComObject is created with extern(C++) we might guess that 
it is then behaves exactly as what it purports to be so as to be 
entirely compatible with C++. COM in Windows has both a C++ API 
and a binary compatible C API, so now everything works as 
expected. extern(C++) object pointers/references need to just 
work, so the compiler won't produce a bogus offset, whether or 
not the extern(C++) object is secretly embedded inside a "normal" 
object, which we might guess it is. There's just a different 
convention for pointers and references.





More information about the Digitalmars-d mailing list