In this article I extend the existing CComPtr smart pointer class that is a part of the ATL framework. CComPtr is a reference counting smart pointer which has a method for conveniently creating a new instance in a 1 step process. However, it has no option for activating a COM object on a remote system. Since this is a common scenario and every implementation contains the same boilerplate code, abstracting it away is useful.
Introduction
When working with DCOM in C++, you typically use the Active Template Library (ATL) classes such as CComPtr<T>
for taking the sting out of reference counting and avoiding counting errors and memory leaks. One of the things you have to do often is to declare a pointer, and create a new object instance and retrieve an interface via CoCreateInstance
.
It happens so often that there is a dedicated CComPtr<T>::CoCreateInstance
convenience method to make this a 1 step process.
In my particular environment, I often have to use DCOM to activate the instance on a remote server. Remote activation is done via CoCreateInstanceEx, for which CComPtr<T>
does not have a method. But we can extend CComPtr<T>
by simply inheriting from it, and adding that method ourselves.
Extending the base class
CComPtr<T>
is really one of those classes that is a lifesaver when dealing with COM because it takes all the sting out of copying, reference counting, etc. I settled on CComPtrAny<T>
for the name to make it intuitive that it supports any kind of COM interface pointer: local and remote. Of course the regular CComPtr<T>
does that too. It doesn't care where the actual object is residing. But it doesn't have helper methods for remoting.
I briefly considered using the name CComPtrAnywhere<T>
but decided not to because if at some point I add a method for some arcane activation type or we want to add something else, it wouldn't really fit anymore, whereas 'any' will always be ok.
I feel I should leave a final note about remote activation: even if a client tries a local activation, it is possible that the component on the computer itself is configured in DCOMConfig to launch on another server. So just because you think you launch a local component doesn't mean it is local, and there is no fundamental and easy way to find out without resorting to trickery.
Background
For remote activation we are required to use CoCreateInstanceEx
which has the following declaration
HRESULT CoCreateInstanceEx(
[in] REFCLSID Clsid,
[in] IUnknown *punkOuter,
[in] DWORD dwClsCtx,
[in] COSERVERINFO *pServerInfo,
[in] DWORD dwCount,
[in, out] MULTI_QI *pResults
);
The argument where we specify the remote server is in the COSERVERINFO
struct.
typedef struct _COSERVERINFO {
DWORD dwReserved1;
LPWSTR pwszName;
COAUTHINFO *pAuthInfo;
DWORD dwReserved2;
} COSERVERINFO;
From this, we only supply the pwszName
parameter. We supply NULL
for pAuthInfo
which will cause security and authenticate between the client and server to be negotiated according to group policy and local security configuration.
As has been shown with DCOM hardening, leaving this up to the computers themselves is always the best idea because it allows for an administrator to make the final call on security and there is nothing that you could do at this level that an admin also couldn't do if you leave pAuthInfo
default. Even IF for some reason you need to activate the connection under a different user account, I would argue it is better to use the impersonation APIs and let the LSA take care of authentication according to its configured security policy instead of explicitly coding specific security related choices because that too is not future proof.
The other thing we need to look at is the pResults parameter. The name is a bit deceptive because it's also the input parameter. It's an array of MULTI_QI
structures.
typedef struct tagMULTI_QI {
const IID *pIID;
IUnknown *pItf;
HRESULT hr;
} MULTI_QI;
We supply the IID
of the requested interface, and in return we get a HRESULT
and (possibly) an interface pointer. For our purpose we use an array with only 1 element because after all we are adding an instance method to CComPtrAny<T>
which manages only 1 interface pointer.
Implementing the activation
Putting the previous pieces together, we can define the method signature.
template <class T> class CComPtrAny : public CComPtr<T>
{
public:
HRESULT CoCreateInstanceEx(
_In_ const std::wstring& remoteServer,
_In_ REFCLSID rclsid,
_Inout_opt_ LPUNKNOWN pUnkOuter = NULL,
_In_ DWORD dwClsContext = CLSCTX_ALL);
}
I already mentioned we don't use specific security configuration so there is no point in putting that in the signature. I pass the remote server as a const wstring&
. The benefit is that I don't need to do pointer validation every time. And converting from wstring
to LPWSTR
is trivial.
I should note that I no longer support ASCII builds on any platform. Some things like the DCOM apis, .NET, etc are unicode only. Supporting ASCII builds just for the sake of it is pointless in my opinion, and just means you'll be doing a lot of manual conversions for no added value.
With the signature in place we can now put the different pieces together. For the server info we need to cast away the 'const'ness of the wstring::c_str()
return value. This is usually not good practice but in this case we known that the win32 api will not modify it. We use the __uuidof
compiler keyword to get the IID
for the interface pointer we want to retrieve. All in all it's pretty simple.
If a pointer was successfully returned, we attach it to our CComPtrAny
without AddRef
'ing it because we assume ownership and 'attach' its lifecycle to that of our CComPtrAny
.
COSERVERINFO info;
memset(&info, 0, sizeof(info));
info.pwszName = const_cast<LPWSTR>(remoteServer.c_str());
MULTI_QI mqi[1];
mqi[0].pIID = & __uuidof(T);
mqi[0].pItf = NULL;
mqi[0].hr = 0;
HRESULT hRes = ::CoCreateInstanceEx(rclsid, pUnkOuter, dwClsContext, &info, 1, mqi);
if (SUCCEEDED(hRes)) {
hRes = mqi[0].hr;
if (SUCCEEDED(hRes)) {
this->Attach(static_cast<T*>(mqi[0].pItf));
}
}
return hRes;
An Extra Overload
The original CComPtr<T>
implementation overloads the CoCreateInstance
method to take a human readable program ID instead of the class ID. Since this is a trivial and convenient thing to do, we add the same option for remote activation.
HRESULT CoCreateInstanceEx(
_In_ const std::wstring& remoteServer,
_In_ const std::wstring& progID,
_Inout_opt_ LPUNKNOWN pUnkOuter = NULL,
_In_ DWORD dwClsContext = CLSCTX_ALL)
{
CLSID classId;
memset(&classId, 0, sizeof(classId));
HRESULT hRes = CLSIDFromProgID(remoteServer.c_str(), &classId);
if (FAILED(hRes)) {
return hRes;
}
return CoCreateInstanceEx(remoteServer, classId, pUnkOuter, dwClsContext);
}
Putting everything together
If we put all this together, we get the following code. Feel free to use it under the MIT license.
#pragma once
#include <atlbase.h>
#include <string>
template <class T> class CComPtrAny : public CComPtr<T>
{
public:
HRESULT CoCreateInstanceEx(
_In_ const std::wstring& remoteServer,
_In_ REFCLSID rclsid,
_Inout_opt_ LPUNKNOWN pUnkOuter = NULL,
_In_ DWORD dwClsContext = CLSCTX_ALL)
{
COSERVERINFO info;
memset(&info, 0, sizeof(info));
info.pwszName = const_cast<LPWSTR>(remoteServer.c_str());
MULTI_QI mqi[1];
mqi[0].pIID = &__uuidof(T);
mqi[0].pItf = NULL;
mqi[0].hr = 0;
HRESULT hRes = ::CoCreateInstanceEx(rclsid, pUnkOuter, dwClsContext, &info, 1, mqi);
if (SUCCEEDED(hRes)) {
hRes = mqi[0].hr;
if (SUCCEEDED(hRes)) {
this->Attach(static_cast<T*>(mqi[0].pItf));
}
}
return hRes;
}
HRESULT CoCreateInstanceEx(
_In_ const std::wstring& remoteServer,
_In_ const std::wstring& progID,
_Inout_opt_ LPUNKNOWN pUnkOuter = NULL,
_In_ DWORD dwClsContext = CLSCTX_ALL)
{
CLSID classId;
memset(&classId, 0, sizeof(classId));
HRESULT hRes = CLSIDFromProgID(remoteServer.c_str(), &classId);
if (FAILED(hRes)) {
return hRes;
}
return CoCreateInstanceEx(remoteServer, classId, pUnkOuter, dwClsContext);
}
};
Points of Interest
COM is old and dated but it's still going to be around for years to come, thanks to the installed codebases that continue to exist.
It surprised me that remote execution was not added by default to CComPtr<T
> but thankfully, adding it is trivial and any amount of boilerplate that can be abstracted away is a good thing.
History
First version, 05AUG2024