Designing a COM Library for Rust
msrc-blog.microsoft.com
msrc-blog.microsoft.com
I'm really glad MS is doing this. What needs to be a bit clearer to me is how they maintain ABI compatibility under the hood of MSVC for COM interfaces (which uses the vtable layout of an inherited class) and how that's compatible with MinGW/GCC stacks on Windows, mostly what can break it. I got stuck porting VST3 with multiple inheritance, and it was a headache trying to reverse engineer the appropriate struct layouts for COM implementations.
[1] https://github.com/retep998/winapi-rs/blob/0.3/src/macros.rs
[2] https://github.com/Connicpu/com-impl
[3] https://github.com/steinbergmedia/vst3sdk
[4] https://github.com/m-hilgendorf/cli-host (sorry for the messy code, it was a weekend of hacking away trying to host a VST3 in pure Rust)
[1] https://itanium-cxx-abi.github.io/cxx-abi/abi.html#vtable
Tear-off interfaces and delegated implementations are things in this world.
Specifically, this class implementing vtable callable from C++: https://github.com/Const-me/ComLightInterop/blob/master/ComL...
About multiple interfaces, all of them need 3 first vtable entries pointing to the 3 IUnknown methods. Also, don't forget that when client calls QueryInterface on any interface of the same object with IID_IUnknown argument, you must return same IUnknown pointer. Some parts of COM use that pointer as object's identity.
vtable layout in the most basic situations is going to be accidentally portable because those situations boil down to "it's a struct of function pointers," and there's only so many ways you can order the fields of a struct. But even here, MSVC uses a quite different ABI: the order of the vtable entries can change if you overload a virtual method with a non-virtual method.
Also, a public COM interface mustn't have a virtual destructor, because some compilers (e.g. recent GCC) put more than one method in the vtable. Implementation classes might define a virtual destructor, though.
The way MSVC does it may need reverse engineering (it may be patented btw). I could explain how Delphi implements COM interfaces, but any specific implementation is actually more complicated than the ABI, because they're trying to add implementation ergonomics on top of the basic calling convention.
Essential COM by Don Box
https://www.amazon.com/Essential-COM-Don-Box/dp/0201634465
Inside COM by Dale Rogerson
https://www.amazon.com/Inside-Microsoft-Programming-Dale-Rog...
Then if you have access to public libraries, maybe one of them has one of the several Microsoft Systems Journals issues, later MSDN Magazine, with plenty of low level COM articles.
COM is from the days where good documentation was to be found in books, not on the Interwebs.
https://archive.org/details/MSDN_Library_October_2001_Disc_1
https://archive.org/details/MSDN_Library_October_2001_Disc_2
https://archive.org/details/MSDN_Library_October_2001_Disc_3
I highly recommend using them if you're ever forced to work with VBA and need to implement some non-trivial functionality.
The rest is just that interfaces are doubly indirected to get to a vtable (an array or struct of function pointers), the method is chosen by ordinal in the vtable (first three are always the IUnknown methods), and the interface is passed as the first argument.
How you construct those vtables and how you select one to return in QueryInterface, and how you implement interfaces (i.e. traits) so you can convert them into a vtable is where all the work is. You can do anything you like that works as long as it's called according to the COM conventions.
Delphi works by implementing each method in the vtable with a stub which subtracts the offset of the vtable pointer in the instance data from the passed in interface, and then jumps to the implementation method on the instance after the instance has now been adjusted. Instances look like this:
[class pointer, the native vtable] <- normal instance pointers
[vtable interface 1] <- COM interface pointers
...
[vtable interface n] <- COM interface pointers
instance field 1
...
instance field n
So you can see that in order to convert a COM interface pointer into an instance pointer that the methods expect, the COM interface pointer needs to be adjusted depending on the offset of the vtable in the instance.In a language with multiple inheritance like C++, the compiler vendor targeting Windows may choose a layout which is suitable for COM's calling convention (there's more than one way to do MI, e.g. fat pointers is another way to go that wouldn't be COM-compatible). If the vendor does that, then they make implementation of COM interfaces much easier. And if they don't, well, life isn't going to be easy. Technically you could do a bunch of stuff with reflection and code generation, but that's harder and harder these days with security restrictions around code writing code. You could write macros which create statically initialized structures of the right shape, and fields of the right type, to emulate the same effect as Delphi's scheme as I sketched above, or some other method which would work with COM's calling convention.
This isn't quite right. The calling convention is a Windows C++ variant of stdcall. See this issue: https://github.com/rust-lang/rfcs/issues/1342
However, 99% of the time the difference won't be an issue.
> COM has been superseded by WinRT which builds on COM to provide even more guarantees about the binary interface.
Yet the WinRT page says:
> The Windows Runtime is based on Component Object Model (COM) APIs, and it's designed to be accessed through language projections. A projection hides the COM details, and provides a more natural programming experience for a given language.
Which implies that WinRT doesn't supersed (ie. replace) COM but it uses COM itself (though the "language projection" part makes me think that the way it is used is so convoluted that you need language extensions to make it tolerable :-P).
Basically there is a new base interface, IInspectable instead of IUnknown, and many more datatypes are allowed to cross the wire between processes, making it easier to also use generics, lists and arrays across COM libraries.
Additonally, .NET metadata libraries are using instead of the old COM type libraries.
If you prefer, it is the original design of .NET before the CLR came into play, and related to the Longhorn architecture, just using COM instead of .NET. A path started in Vista.
Now for this Rust approach to win the hearts of Windows devs, it needs to be as productive as C++/CX or C++/WinRT VS tooling, in all COM/UWP usage scenarios.
https://docs.microsoft.com/en-us/uwp/winrt-cref/winrt-type-s...
The documentation is a bit lacking, as it appeared with WinRT, then we had UAP, and finally UWP. While it all boils down to the same, those marketing changes, did cause the documentation to keep changing.
Alongside C++/CX, there was the low level "Windows Runtime C++ Template Library (WRL)", which was basically an evolution of ATL, with MDIL 2.0.
https://docs.microsoft.com/en-us/cpp/cppcx/wrl/windows-runti...
Eventually MIDL improved to 3.0, as the work on C++/WinRT progressed.
https://docs.microsoft.com/en-us/uwp/midl-3/
https://channel9.msdn.com/Shows/ifdefWINDOWS/MIDL3-with-Larr...
Here are some technical documents and sessions related to WinRT.
"The Windows Runtime"
https://channel9.msdn.com/Events/Lang-NEXT/Lang-NEXT-2012/Th...
"Windows Runtime internals: understanding "Hello World""
https://channel9.msdn.com/events/Build/BUILD2011/PLAT-875T
The C++/WinRT talks from Kenny Kerr, related how they migrated from C++/CX language extensions to the C++17 compliant WinRT in chronological order.
"CppCon 2017: Scott Jones & Kenny Kerr “C++/WinRT and the Future of C++ on Windows”"
https://www.youtube.com/watch?v=7TdpWB_vRZM
"Effective C++/WinRT for UWP and Win32"
https://www.youtube.com/watch?v=nOFNc2uTmGs
"Meet C++/WinRT 2.0: Faster and smarter in the open"
https://www.youtube.com/watch?v=X41j_gzSwOY
Uff, hope this sheds some light on your questions and anyone else that wants to get a clearer picture of COM and its evolution into UWP.
Thank you very much!
Yes, actually, you can do that. I had such code working. Basically declared the IInspectable VTable as a C struct and filled it with my own implementation. Then wrote an "activator" for the relevant classes the same way. [What COM used to call factories.]
Did the same thing with a few VTables for MS-authored WinRT classes calling into them in the other direction.
All this was tedious, I had complicated reasons for wanting to do this and I eventually switched to WRL when those reasons disappeared. Writing COM/WinRT from C++ with something like WRL is much nicer.
Possibly related, I just spotted this [1] on the personal blog of Kenny Kerr, creator of C++/WinRT:
> On a personal note, I’m spending a lot of my time working with the Rust language and can’t wait to share more about that eventually.
Disclosure: I work on Windows at Microsoft, but not in this area. Posting on my own behalf though.
Thanks for bringing it up.
Most of my experience in using it has been horribly abusing it to drive Office applications and Notes to do some awful things because at the time there weren't any libraries to manipulate certain file types. I have done some truly heinous things with the Lync/Skype COM-based client SDK, back in the times when the Office 365 version of that product was viable, and there was no other API options available.
You probably haven't heard much about it, because now it has the fancy name of UWP, or it comes with .NET wrappers on top.
That said, "UWP" is confusingly also used to refer to Windows apps that tap into WinRT APIs, participate in the modern app lifecycle, are modeled and have identity, or generally tap into newer technologies such as AppContainer. You'll see this, for example, perpetuated in Visual Studio in its project templating. It's a BIG OL' MESS right now; I'm working with/pushing Microsoft to get this communicated clearly and mopped up but old habits die hard.
https://docs.microsoft.com/en-us/windows/apps/desktop/choose...
And then there are some OS editions where some apps can't run or can only run in a container environment.
Until then, sorry to say but I follow up on what is available to us.
I feel Windows' C API (i.e., Win32 API) was adopted at a larger scale than COM. It's much easier to use the Win32 C API than COM objects. You'd have to use COM if you wanted to program the shell or DirectX but most of everything else you could use the C API.
That's probably more a reflection of DirectX being reasonably well designed.
1. The good COM, which is just a class-like ABI.
2. The horrible OLE COM, which allowed you to embed Word docs in Excel and the like which is what most people associate with COM.
If you're using an API like DirectWrite (at least older versions) that requires you to implement interfaces to do basic things like load fonts from memory, then the COM underpinnings of the APIs become very apparent.
I think COM is incomprehensible to most developers today--it certainly is to me, and I maintained a .NET application with COM components for several years! COM is so arcane and verbose.
Why invest hundreds of hours learning something with no clear benefit or future? The documentation introduces COM as:
https://docs.microsoft.com/en-us/windows/win32/com/component...
> COM is a platform-independent, distributed, object-oriented system for creating binary software components that can interact. COM is the foundation technology for Microsoft's OLE (compound documents) and ActiveX (Internet-enabled components) technologies.
What does that even mean? It sounds obsolete.
Really, can't we just leave COM in the 90's?
COM gets regularly new APIs.
Since Vista all major APIs are based on COM.
How do you think those easier technologies are implemented?
Just to give an example, React Native for Windows uses UWP.
As for not being relevant, the software for a platform with 80% market share across desktop and laptops, does not write itself.
COM is an interesting concept. If it were never invented I have a feeling native apps would be spinning up a simple REST API server instead, since that's what we all do now apparently.
I have played with COM in Go to a lesser extent. It’s not too bad actually. My intent was to write Go wrappers around all of the TSF COM interfaces, but I burnt out after getting basic IME working - TSF is enormous!
I believe you are talking about this repo https://github.com/jchv/go-tsf
Do you know any other project that's working on creating basic IME on top of TSF in Go?