.NET:C#/C++CLI能否重载/挂钩IUnknown接口?
嘿,这个问题问得挺深入的!刚好我之前研究过.NET COM互操作的底层细节,来给你梳理清楚:
首先明确一点:CLR本身并不允许直接重写它自动生成的CCW(COM Callable Wrapper)里的IUnknown::QueryInterface。毕竟CCW是CLR内部实现的组件,用来处理.NET对象和COM客户端的交互,我们没有办法直接修改它的逻辑。
但你的思路——用DummyGreeter(标记ComVisible但不注册)+DummyWrapper(原生COM包装器)——完全可以实现你要的"挂钩QueryInterface、针对特定IID自定义行为"的效果!本质上是绕开CLR的默认CCW,用自己写的原生COM组件作为中间层,拦截所有COM客户端的调用,包括QueryInterface。
具体实现步骤拆解
1. 先搞定DummyGreeter项目(.NET类库)
创建一个VB.NET类库,把你的Greeter类标记为<ComVisible(True)>,但绝对不要注册这个类库——别勾选项目属性里的"为COM互操作注册",也别用regasm命令注册。这样CLR就不会生成默认的CCW注册表项,所有的COM交互都由我们的DummyWrapper来控制。
示例代码(Greeter.vb):
Imports System.Runtime.InteropServices <ComVisible(True)> Public Class Greeter Public Function SayHello(name As String) As String Return $"Hello, {name}!" End Function End Class
2. 核心:DummyWrapper原生COM DLL
这个项目是实现拦截的关键,我们要写一个原生COM对象,它内部持有.NET对象的CCW引用,然后自己实现IUnknown::QueryInterface,把特定IID的请求拦下来处理,其他请求转发给.NET对象的CCW。
(1)main.cpp的核心逻辑
需要做几件事:加载CLR并创建.NET对象实例、实现自定义的QueryInterface、实现COM类工厂和必要的导出函数。这里给你一个简化的核心示例:
#include <windows.h> #include <mscoree.h> #include <unknwn.h> #pragma comment(lib, "mscoree.lib") // 定义你要拦截的特定IID(自己随便定义一个就行) const IID IID_IGreeterEx = {0x12345678, 0x1234, 0x1234, {0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF}}; // 自定义包装器类,实现IUnknown class DummyGreeterWrapper : public IUnknown { private: LONG m_cRef; IUnknown* m_pNetCCW; // 持有.NET对象的CCW指针 public: DummyGreeterWrapper(IUnknown* pNetCCW) : m_cRef(1), m_pNetCCW(pNetCCW) { m_pNetCCW->AddRef(); } // 重写QueryInterface,实现拦截逻辑 STDMETHODIMP QueryInterface(REFIID riid, void** ppvObject) override { if (riid == IID_IUnknown) { *ppvObject = static_cast<IUnknown*>(this); AddRef(); return S_OK; } // 拦截我们关心的特定IID else if (riid == IID_IGreeterEx) { // 这里可以写你的自定义逻辑:比如返回自己实现的IGreeterEx接口,或者直接返回E_NOINTERFACE // 示例里直接返回空,实际根据需求调整 *ppvObject = nullptr; return E_NOINTERFACE; } // 其他所有IID请求,直接转发给.NET对象的CCW else { return m_pNetCCW->QueryInterface(riid, ppvObject); } } STDMETHODIMP_(ULONG) AddRef() override { return InterlockedIncrement(&m_cRef); } STDMETHODIMP_(ULONG) Release() override { ULONG cRef = InterlockedDecrement(&m_cRef); if (cRef == 0) { m_pNetCCW->Release(); delete this; } return cRef; } }; // COM类工厂,用来创建我们的包装器对象 class DummyGreeterClassFactory : public IClassFactory { private: LONG m_cRef; public: DummyGreeterClassFactory() : m_cRef(1) {} STDMETHODIMP QueryInterface(REFIID riid, void** ppvObject) override { if (riid == IID_IUnknown || riid == IID_IClassFactory) { *ppvObject = static_cast<IClassFactory*>(this); AddRef(); return S_OK; } return E_NOINTERFACE; } STDMETHODIMP_(ULONG) AddRef() override { return InterlockedIncrement(&m_cRef); } STDMETHODIMP_(ULONG) Release() override { ULONG cRef = InterlockedDecrement(&m_cRef); if (cRef == 0) { delete this; } return cRef; } STDMETHODIMP CreateInstance(IUnknown* pUnkOuter, REFIID riid, void** ppvObject) override { if (pUnkOuter != nullptr) { return CLASS_E_NOAGGREGATION; } // 加载CLR,创建.NET对象的实例 ICLRMetaHost* pMetaHost = nullptr; ICLRRuntimeInfo* pRuntimeInfo = nullptr; ICLRRuntimeHost* pRuntimeHost = nullptr; HRESULT hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (LPVOID*)&pMetaHost); if (FAILED(hr)) goto Cleanup; // 这里指定.NET版本,要和DummyGreeter的版本匹配 hr = pMetaHost->GetRuntime(L"v4.0.30319", IID_ICLRRuntimeInfo, (LPVOID*)&pRuntimeInfo); if (FAILED(hr)) goto Cleanup; hr = pRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_ICLRRuntimeHost, (LPVOID*)&pRuntimeHost); if (FAILED(hr)) goto Cleanup; hr = pRuntimeHost->Start(); if (FAILED(hr)) goto Cleanup; // 创建.NET对象,拿到它的CCW指针 IUnknown* pNetCCW = nullptr; hr = pRuntimeHost->CreateDomainAndExecuteAssembly( L"DummyGreeter.dll", L"DummyGreeter.Greeter", L"CreateInstance", nullptr, (DWORD*)&pNetCCW ); if (FAILED(hr)) goto Cleanup; // 创建我们的包装器对象,返回给客户端 DummyGreeterWrapper* pWrapper = new DummyGreeterWrapper(pNetCCW); hr = pWrapper->QueryInterface(riid, ppvObject); pWrapper->Release(); Cleanup: // 别忘了释放所有COM对象,避免内存泄漏 if (pRuntimeHost) pRuntimeHost->Release(); if (pRuntimeInfo) pRuntimeInfo->Release(); if (pMetaHost) pMetaHost->Release(); if (pNetCCW) pNetCCW->Release(); return hr; } STDMETHODIMP LockServer(BOOL fLock) override { return S_OK; } }; // 导出COM必须的函数 extern "C" HRESULT __stdcall DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppvObject) { // 这里要定义你的包装器CLSID,和注册表注册的一致 const CLSID CLSID_DummyGreeter = {0x87654321, 0x4321, 0x4321, {0xAB, 0xCD, 0xEF, 0x90, 0x78, 0x56, 0x34, 0x12}}; if (rclsid != CLSID_DummyGreeter) { return CLASS_E_CLASSNOTAVAILABLE; } DummyGreeterClassFactory* pFactory = new DummyGreeterClassFactory(); HRESULT hr = pFactory->QueryInterface(riid, ppvObject); pFactory->Release(); return hr; } extern "C" HRESULT __stdcall DllCanUnloadNow() { // 简单示例,实际需要跟踪组件的引用计数,确定是否可以卸载 return S_FALSE; } extern "C" HRESULT __stdcall DllRegisterServer() { // 这里需要实现注册表注册逻辑:写入CLSID对应的InprocServer32等项 return S_OK; } extern "C" HRESULT __stdcall DllUnregisterServer() { // 注销注册表项 return S_OK; }
(2)DummyWrapper.def文件
必须导出COM要求的几个函数,所以要加一个.def文件:
LIBRARY "DummyWrapper" EXPORTS DllGetClassObject PRIVATE DllCanUnloadNow PRIVATE DllRegisterServer PRIVATE DllUnregisterServer PRIVATE
几个关键注意点
- CLR版本匹配:代码里指定的.NET版本要和
DummyGreeter项目的目标版本完全一致,不然加载会失败。 - 引用计数要严谨:原生COM对象和.NET CCW的引用计数必须正确管理,不然会出现内存泄漏或者对象提前释放的问题。
- 自定义IID的处理:在QueryInterface里,你可以针对特定IID返回自己的接口实现——比如写一个
IGreeterEx接口,让客户端调用这个接口时执行你想要的逻辑,而不是用.NET对象的默认实现。 - 注册问题:只有
DummyWrapper需要注册为COM组件,DummyGreeter只需要和DummyWrapper放在同一个目录下,或者放在CLR能找到的路径里就行。
总结
虽然CLR不让我们直接修改它的CCW的QueryInterface,但用你想到的"原生包装器+未注册的.NET类库"的方案,完全可以实现拦截特定IID的效果。核心就是用自己的原生COM组件接管所有COM客户端的请求,从而插入自定义逻辑,完美解决你的需求!
内容的提问来源于stack exchange,提问作者Dmytro

