Examples of language-neutral Component Object Model (COM) interfaces for IPC and programming languages interoperability.
Project listing:
Module | Description |
---|---|
ComSupport | .Net support functions for COM EXE registration (not needed for DLLs) |
MyInterfaces | COM interface definitions (MyInterfaces.idl) |
MyClientCpp | Sample C++ client |
MyClientCs | Sample C# client |
MyClientPy | Sample Python client |
MyDllServerCpp | C++ server DLL implementation |
MyDllServerCs | C# server DLL implementation |
MyExeServerCpp | C++ server EXE implementation |
MyExeServerCs | C# server EXE implementation |
ServiceWrapper | Support project for running "regular" COM servers as a Windows service |
Both servers are implemented as on-demand loaded COM EXE servers. The processes can also be started manually in advance if desired. The .Net samples are based on OutOfProcCOM.
The server projects implement the IMyServer
interface whereas the client projects implement the IMyClient
callback interface:
.py
files.MyExeServerCs.exe /regserver
with admin privileges,MyExeServerCpp.exe /regserver
with admin privileges,regsvr32.exe MyDlleServerCpp.dll
with admin privileges,regsvr32.exe MyDlleServerCs.dll
with admin privileges,MyClientCpp.exe
MyClientCs.exe
MyClientPy.py
MyExeServerCs.exe /unregserver
with admin privileges,MyExeServerCpp.exe /unregserver
with admin privileges,regsvr32.exe /u MyDlleServerCpp.dll
with admin privileges,regsvr32.exe /u MyDlleServerCs.dll
with admin privileges,UNREGISTER.bat
with admin privileges,Server registration is primarily needed for on-demand loaded COM servers.
The client programs should output something resembling this:
pi = 3.141592653589793
Received message:
sev=Info
time=7/4/2023 10:27:57 AM
value=1.23
desc=Hello there!
color=(255, 0, 0)
data=[0,1,2,3]
...
COM interfaces are first defined in IDL files. The IDL files are afterwards compiled into TLB type libraries, TLH/TLI headers and/or .Net DLL assemblies, depending on the target language(s):
Some common concepts:
[1] Globally Unique Identifier (GUID) are 128bit values typically expressed as a {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
hexadecimal string. Must be globally unique, so they cannot be copied between projects. Typically generated with the uuidgen.exe
tool from a Developer Command Prompt.
[2] Neither COM interfaces nor their associated data structures or enumerations can be changed after an external release, since that would break compatibility with existing code. Developers instead need to create a new interface with a new GUID when changes are required. It's then common to add a version number suffix to the interface name as versioning scheme.
COM servers can be compiled as either:
COM provides transparent marshaling. This means that the client code doesn't need to know if the COM server runs in the same or a different process. The object that exposes a COM interface looks the same, regardless of if it's running in the same or a separate process or is implemented in a different programming language.
Microsoft provides COMServerDemo sample code for DLL-based servers implemented in C#/.Net and Create a Classic COM Component Using WRL for servers implemented in C++.
[1] DLL-based COM servers can also be started in a separate process if configuring a DllSurrogate. This can sometimes be useful for testing, since it allows the same COM server to either be loaded in-proc or out-of-proc without having to rebuild it.
Both C++, .Net and Python can automatically map COM HRESULT
error codes to exceptions, so that developers doesn't need to explicitly check each call for failure.
HRESULT
error codes are automatically mapped to _com_error
exceptions. It's still possible to call the "old" HRESULT versions by adding a raw_
prefix to the method names.HRESULT
error codes are automatically mapped to comparable .Net exceptions.HRESULT
error codes are automatically mapped to COMError
exceptions.COM clients and servers can decide their threading model for incoming calls[1] by configuring the thread associated with the class(es) receiving callbacks to run in either:
[1] The threading model only affects incoming calls marshalled by the COM runtime. This typically means COM servers implemented in a different programming language or running in a different process. Direct C++ communication between COM objects in the same process are not affected by the threading model.
[2] STA threads need to pump messages to process incoming calls - just like all GUI applications does to process mouse & keyboard events. The implementation then needs to consider that reentrancy can occur as part of the message pumping if pumping messages while processing an incoming call.
In the STA case COM pointers can only be called by the associated thread. The COM pointer need to be explicitly marshaled before they can be accessed by a different thread. This is handled automatically in .Net but needs to be performed explicitly with RoGetAgileReference
and IAgileReference::Resolve
in C++ as done by the ComMarshal sample. Marshaling between threads is not required in the MTA case.
Registry folders associated with a COM server:
HKCR\Interface\{GUID}
: Registry entries for a COM interface.HKCR\CLSID\{GUID}
: Primary registry entries for a creatable COM class. Not needed if the class is not directly creatable.HKCR\AppID\{GUID}
: Optional additional registry entries for a COM class. Can use same GUID as the COM ClassID (CLSID)HKCR\TypeLib\{GUID}
: Registry entries for a type library. Used for marshaling calls between languages and processes.HKCR\{ProgID}
: Optional string-based programmatic identifier for a COM class.Additional registry folders:
HKCR\WOW6432Node
.HKCU\SOFTWARE\Classes
.COM servers are responsible for registering and unregistering themselves. This is typically done through the following commands from an admin command prompt:
regsvr32.exe <server-name>.dll
and regsvr32.exe /u <server-name>.dll
<server-name>.exe /regserver
and <server-name>.exe /unregserver
Rules:
CComPtr<T>
or _com_ptr_t<T>
to ease access.SysAllocString
/SysFreeString
. Use CComBSTR
wrapper to ease access.SafeArrayCreate
/SafeArrayDestroy
. Use CComSafeArray<T>
to ease access.CoTaskMemAlloc
/CoTaskMemFree
.[in]
arguments are allocated and freed by the caller (automatic for function being called).[out]
arguments are allocated by the function called and later freed by the caller.[in,out]
identical to [in]
, but the function being called might also free & reallocate.ref
by default[ref]
: Never NULL or aliased.[unique]
: Can be NULL, change to/from NULL but not aliased.[ptr]
: Can be NULL, change to/from NULL and be aliased.[pointer_default()]
]: Set semantics for nested pointers. Defaults to unique
unless specified.QueryInterface
reflexive, symmetric and transitive rules.These topics are primary relevant when authoring COM servers. Consumers of a COM server can usually disregard these topics.
The ServiceWrapper project can be used to start a COM server as a Windows background service.
If doing so, then one can also consider using the Running Object Table (ROT) to connect to the already running COM server instead of CoCreateInstance
. This can be achieved through the following functions:
GetActiveObject
only succeed if the COM server is already running, wheras CoCreateInstance
always succeed and will start the COM server if not already running.
Some noteworthy details:
RunAs
registry value can be used to configure the COM server to start in a different user account. The RunInSandbox ComRunAs
tool can be used to configure this.CoInitializeSecurity
in the COM server to enable this.COM security defaults are quite strict. Most security settings for a COM server can be configured through AppID registry entries. See the RunInSandbox project for examples of security sandboxing and elevation with COM.
Windows includes an in-built "automation" marshaler (oleaut32.dll) for propagating IPC calls between processes. This is sufficient for most projects, but it's also possible to customize IPC marshaling by implementing the IMarshal
interface. The SharedMemMarshal project demonstrates how a custom marshaler can utilize shared memory to avoid copying overhead when passing large amounts of data between processes.