Implementing OOP COM Server with cppwinrt - microsoft/wil GitHub Wiki
WIL include functionalities for implementing OOP COM server with just C++/WinRT. Below is an example of a complete server implementation:
#define WINRT_CUSTOM_MODULE_LOCK
#include <wil/cppwinrt_notifiable_server_lock.h>
#include <wil/cppwinrt_register_com_server.h>
struct __declspec(uuid("eaaadc39-ab4b-47bf-9678-f14c1e947de8")) MyServer
: winrt::implements<MyServer, IUnknown>
{
};
struct __declspec(uuid("265bdfe9-7a18-4767-84b9-ea5f956bb846")) MyServer2
: winrt::implements<MyServer2, IUnknown>
{
};
int CALLBACK wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) {
wil::unique_event moduleEvent(wil::EventOptions::None);
wil::notifiable_server_lock::instance().set_notifier([&]() {
moduleEvent.SetEvent();
});
winrt::init_apartment();
auto revoker = wil::register_com_server(wil::make_array_of_uuid<MyServer, MyServer2>());
moduleEvent.wait();
return 0;
}
#define WINRT_CUSTOM_MODULE_LOCK
#include <wil/cppwinrt_notifiable_server_lock.h>
#include <wil/cppwinrt_register_com_server.h>
#include <winrt/MyServerNamespace.h>
int CALLBACK wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) {
wil::unique_event moduleEvent(wil::EventOptions::None);
wil::notifiable_server_lock::instance().set_notifier([&]() {
moduleEvent.SetEvent();
});
winrt::init_apartment();
auto revoker = wil::register_com_server(wil::make_array_of_guid<MyServerNamespace::MyServer, MyServerNamespace::MyServer2>());
moduleEvent.wait();
return 0;
}
Even with C++/WinRT 2.0, online tutorials for implementing COM server still uses WRL, so much so that internal Microsoft engineers opened this issue asking for a canonical support/implementation of COM server in C++/WinRT.
Although that issue had been closed, several examples in the discussion shows that C++/WinRT had always included necessary tools/customization points for implementing a COM server, it just lacks a higher level component connecting the dots.
WIL provides such components for connecting the dots.
A complete COM server has two jobs:
- Implementing class factory and registering of the factory
- Synchronize its lifetime with outstanding objects
Include the following headers and #define
for each of the job:
// Implement class factory and register the factory
#include <wil/cppwinrt_register_com_server.h>
// Synchronize server lifetime with outstanding objects
#define WINRT_CUSTOM_MODULE_LOCK
#include <wil/cppwinrt_notifiable_server_lock.h>
template<typename... Types>
[[no_discard]] auto wil::register_com_server(wil::clsid_array<Ts...> const& clsids, DWORD context, DWORD flags);
Register multiple classes passed via Types
. You must pass an array of CLSID(s) of Types
via clsids
. For your convenience, you can use wil::make_array_of_uuid<Types>
for COM classes or wil::make_array_of_guid<Types>
for WinRT classes. See doc for more details
If any of the registration fails, the whole operation fails by throwing the underlying winrt::hresult
.
You can customize the registration context and flags. Default is CLSCTX_LOCAL_SERVER
and REGCLS_MULTIPLEUSE
respectively.
If you explicitly pass in REGCLS_SUSPENDED
, you must resume classes on your own.
And overload with just a single class and GUID is also provided, but you need to spell out the class type twice, one for the function call, another for __uuidof
(COM class) or winrt::guid_of
(WinRT class).
template <typename... Types>
constexpr auto make_array_of_uuid();
template <typename... Types>
constexpr auto make_array_of_guid();
WIL's register_com_server
asks users to pass CLSIDs explicitly, but spelling out the GUID for each registered type can be cumbersome.
make_array_of_(uuid|guid)
are two convenience functions to help reduce the boilerplate.
If your implementation type specified __declspec(uuid("$uuid")
, use make_array_of_uuid
. Otherwise, if you type is a WinRT classes, use make_array_of_guid
. Using make_array_of_guid
with any type that specified __declspec(uuid("$uuid"))
would fail a static assert.
Note that you should be aware of the consequence of specifying a default interface that is not unique (e.g. IStringable
).
- First, define
WINRT_CUSTOM_MODULE_LOCK
and include"wil/cppwinrt_notifiable_server_lock.h"
- Wait for an event to exit the server program. Then, signal the event when there is no more outstanding objects with
wil::notifiable_server_lock::instance().set_notifier([]()
{
// TODO: Signal the event
});
You can use wil's unique_event
and SetEvent
for (2). See examples in the introduction.
Some care needs to be taken when you create a COM server with C++/WinRT, regardless of whether you are using wil::register_com_server
. Consider the following table.
COM class* | WinRT runtime class with winmd | Class implementing WinRT interface/having a runtime class base* | |
---|---|---|---|
how to get the CLSID | __uuidof |
winrt::guid_of |
__uuidof |
source of CLSID | __declspec(uuid("$uuid")) |
guid of default interface | __declspec(uuid("$uuid")) |
make_array_of helper to use |
make_array_of_uuid |
make_array_of_guid |
make_array_of_uuid |
note | N/A | remove any public constructor | always specify __declspec(uuid("$uuid"))
|
* - From C++/WinRT's POV, a class implementing a COM interface and a class implementing a WinRT interface/has a runtime class base is the same as both are implemented using winrt::implements<SomeInterface>
. This table tries to separate the two for the purpose of explanation.
The contract for COM server is that the CLSID in the global registry should be unique. That is, given a CLSID, there should be only 1 assembly that can handle its create instance request.
Using WinRT to implement COM server allows developers to enjoy all benefits of WinRT authoring, but some care needs to be taken to make sure that the CLSID, which when obtained via winrt::guid_of
is in fact the GUID of an interface, is unique.
Runtime class are loosely defined as "class that comes with a winmd". There are two parts to make sure the registration CLSID of a runtime class is unique:
- Make sure the interface can only be consumed by one runtime class
- Make sure the runtime class can only be activated by one server
Typical use case of implementing a COM server with WinRT runtime classes is that you already have an existing runtime classes, and you want to expose some of the classes via a COM server. In that case, the default interface is guaranteed to be exclusive to the runtime class, so you don't have to worry about whether the interface is unique to your runtime class. It always is.
The last piece of the puzzle is to make sure your runtime class isn't activatable. That is, do not add any public constructor in your runtime class definition. That makes sure that your server shipping the winmd is the only assembly that can create the runtime class. Otherwise, other processes might be able to load the dll and winmd and activate the runtime class and register them.
(You can also simply not ship the winmd, or put the interface in an internal winmd, depending on how you want to share the interface for consumption of the COM server)
Class implementing WinRT interface normally doesn't need to specify __declspec(uuid("$uuid")
. However, since these classes usually implement existing interfaces. Using these interfaces as CLSID would break the CLSID uniqueness contract as anyone can implementing these interfaces.
Therefore, it's strongly recommended that these classes specify __declspec(uuid("$uuid"))
explicitly. It also follows that you should use __uuidof
or make_array_of_uuid
to obtain the CLSID.