Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

Extending CComPtr for remote activation

5.00/5 (4 votes)
5 Aug 2024MIT4 min read 3.2K  
Extending CComPtr for remote activation of DCOM objects on a remote server, in a convenient matter
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

C++
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.

C++
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.

C++
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.

C++
template <class T> class CComPtrAny : public CComPtr<T>
{
public:
    /// <summary>
    /// create a remote instance of the requested COM object for the specified classID
    /// </summary>
    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.

C++
    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.

C++
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.

C++
//Copyright (c) 2022 Bruno van Dooren
//Permission is hereby granted, free of charge, to any person obtaining a copy
//of this software and associated documentation files (the "Software"), to deal
//in the Software without restriction, including without limitation the rights
//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//copies of the Software, and to permit persons to whom the Software is
//furnished to do so, subject to the following conditions:
//The above copyright notice and this permission notice shall be included in all
//copies or substantial portions of the Software.
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//SOFTWARE.
 

#pragma once
#include <atlbase.h>
#include <string>

/// <summary>
/// This class extends CComPtr<T> with 2 new methods for the purpose of activation
/// on a remote server
/// </summary>
template <class T> class CComPtrAny : public CComPtr<T>
{
public:
    /// <summary>
    /// create a remote instance of the requested COM object for the specified classID
    /// </summary>
    HRESULT CoCreateInstanceEx(
        _In_ const std::wstring& remoteServer,
        _In_ REFCLSID rclsid,
        _Inout_opt_ LPUNKNOWN pUnkOuter = NULL,
        _In_ DWORD dwClsContext = CLSCTX_ALL)
    {
        // the name (or IP address) of the remote server is supplied via the COSEVERINFO
        // parameter. This also allows for a lot of other arcane options related to security
        // and identity, but those are not relevant 99% of the time. Should we ever need them,
        // we can still simply add a more specialized the method with additional parameters.
        COSERVERINFO info;
        memset(&info, 0, sizeof(info));
        info.pwszName = const_cast<LPWSTR>(remoteServer.c_str());

        // The interface to CoCreateInstanceEx allows for creating multiple interfaces at the same time.
        // But here we need only need 1. We remove the need for the caller to provide the IID.
        // by extracting it from the associated class at compile time with the __uuid keyword.
        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)) {
            // The call succeeded. Evaluate the specific result
            hRes = mqi[0].hr;
            if (SUCCEEDED(hRes)) {
                // success indicates an AddRef'ed pointer was returned, of which we now have
                // to take ownership.
                this->Attach(static_cast<T*>(mqi[0].pItf));
            }
        }

        return hRes;
    }

    /// <summary>
    /// Create a remote instance of the requested COM object for a supplied
    /// human readable ProgramID.
    /// </summary>
    HRESULT CoCreateInstanceEx(
        _In_ const std::wstring& remoteServer,
        _In_ const std::wstring& progID,
        _Inout_opt_ LPUNKNOWN pUnkOuter = NULL,
        _In_ DWORD dwClsContext = CLSCTX_ALL)
    {
        // translate the program ID to a class ID and create it remotely.
        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

License

This article, along with any associated source code and files, is licensed under The MIT License