65.9K
CodeProject 正在变化。 阅读更多。
Home

扩展 CComPtr 以进行远程激活

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2024 年 8 月 5 日

MIT

4分钟阅读

viewsIcon

4002

扩展 CComPtr 以方便地进行远程服务器上 DCOM 对象的远程激活

引言

在 C++ 中使用 DCOM 时,您通常会使用活动模板库 (ATL) 类,例如 CComPtr<T>,以减轻引用计数的麻烦,并避免计数错误和内存泄漏。您经常需要做的一件事是声明一个指针,创建一个新对象实例,并通过 CoCreateInstance 检索接口。

这种情况非常普遍,以至于有一个专用的 CComPtr<T>::CoCreateInstance 便利方法,可以将此过程简化为一步完成。

在我特定的环境中,我经常需要使用 DCOM 在远程服务器上激活实例。远程激活是通过 CoCreateInstanceEx 完成的,而 CComPtr<T> 没有提供此方法。但我们可以通过简单地继承它来扩展 CComPtr<T>,并自己添加此方法。

扩展基类

CComPtr<T> 确实是处理 COM 时的一个救星类,因为它减轻了复制、引用计数等方面的所有麻烦。我选择了 CComPtrAny<T> 作为名称,以便直观地表明它支持任何类型的 COM 接口指针:本地和远程。当然,普通的 CComPtr<T> 也可以做到这一点。它不关心实际对象驻留在何处。但它没有用于远程处理的辅助方法。

我曾短暂考虑使用 CComPtrAnywhere<T> 这个名称,但最终没有采纳,因为如果将来我添加了针对某种晦涩激活类型的支持方法,或者我们想添加其他功能,它可能就不再适用了,而“any”(任何)这个词总是可以接受的。

我想就远程激活问题做一个最后的说明:即使客户端尝试本地激活,该计算机上的组件也可能在 DCOM 配置中设置为在另一台服务器上启动。所以,仅仅因为您认为自己启动的是本地组件,并不意味着它就是本地的,并且在没有使用一些技巧的情况下,没有根本的、简单的方法可以发现这一点。

背景

对于远程激活,我们必须使用 CoCreateInstanceEx ,其声明如下:

HRESULT CoCreateInstanceEx(
  [in]      REFCLSID     Clsid,
  [in]      IUnknown     *punkOuter,
  [in]      DWORD        dwClsCtx,
  [in]      COSERVERINFO *pServerInfo,
  [in]      DWORD        dwCount,
  [in, out] MULTI_QI     *pResults
);

我们指定远程服务器的参数在 COSERVERINFO 结构中。

typedef struct _COSERVERINFO {
  DWORD      dwReserved1;
  LPWSTR     pwszName;
  COAUTHINFO *pAuthInfo;
  DWORD      dwReserved2;
} COSERVERINFO;

在这里,我们只提供 pwszName 参数。对于 pAuthInfo ,我们提供 NULL ,这将导致安全性和客户端与服务器之间的身份验证根据组策略和本地安全配置进行协商。

正如 DCOM 加固所示,将此留给计算机自身处理始终是最佳选择,因为它允许管理员对安全性做出最终决定,并且您在这个级别上所能做到的,管理员如果保留 pAuthInfo 默认值也能做到。即使出于某种原因,您需要以不同的用户帐户激活连接,我也认为最好使用模拟 API,让 LSA 根据其配置的安全策略来处理身份验证,而不是显式地编写特定的安全相关选择,因为这也不是面向未来的。

我们还需要关注的另一件事是 pResults 参数。这个名称有点误导,因为它也是输入参数。它是一个 MULTI_QI 结构数组。

typedef struct tagMULTI_QI {
  const IID *pIID;
  IUnknown  *pItf;
  HRESULT   hr;
} MULTI_QI;

我们提供所请求接口的 IID ,作为回报,我们得到一个 HRESULT 和一个(可能)接口指针。为了我们的目的,我们使用一个只有一个元素的数组,因为毕竟我们正在向 CComPtrAny<T> 添加一个实例方法,它只管理一个接口指针。

实现激活

将前面的部分组合在一起,我们可以定义方法签名。

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);
}

我之前提到过,我们不使用特定的安全配置,所以没有必要将其放入签名中。我将远程服务器作为 const wstring& 传递。这样做的好处是我不必每次都进行指针验证。而且从 wstring 转换为 LPWSTR 非常简单。

我应该指出,我不再支持任何平台上的 ASCII 构建。有些东西,如 DCOM API、.NET 等,只支持 Unicode。我认为仅仅为了支持 ASCII 构建就没有意义了,这只会让你在没有增值的情况下进行大量手动转换。

有了签名后,我们就可以将不同的部分组合在一起了。对于服务器信息,我们需要去除 wstring::c_str() 返回值的“const”属性。这通常不是一个好习惯,但在这种情况下,我们知道 win32 API 不会修改它。我们使用 __uuidof 编译器关键字来获取我们想要检索的接口指针的 IID 。总而言之,这相当简单。

如果成功返回了指针,我们就将其附加到我们的 CComPtrAny 中,而不调用 AddRef,因为我们假定拥有所有权,并将它的生命周期附加到我们的 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;

一个额外的重载

原始的 CComPtr<T> 实现重载了 CoCreateInstance 方法,使其接受人类可读的程序 ID 而不是类 ID。由于这是一件微不足道且方便的事情,我们也为远程激活添加了相同的选项。

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);
}

整合所有内容

如果我们把所有这些都放在一起,就会得到以下代码。您可以根据 MIT 许可证自由使用它。

//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);
    }

};

关注点

COM 已经过时,但由于现有代码库的存在,它在未来很多年里仍将继续存在。

令我惊讶的是,远程执行没有默认添加到 CComPtr<T> 中,但值得庆幸的是,添加它非常简单,任何可以抽象掉的样板代码都是有益的。

历史

初版,2024 年 8 月 5 日

© . All rights reserved.