非IUnknown继承的COM接口实现与IDL转换及远程调用疑问
我正在实现用于Word文档导出PDF的COM接口,遇到两类特殊接口:
- 部分接口未继承
IUnknown,比如IMsoServerFileManagerSite:
#undef INTERFACE #define INTERFACE IMsoServerFileManagerSite DECLARE_INTERFACE(IMsoServerFileManagerSite) { STDMETHOD_(BOOL, FGetHandle) (const WCHAR *pwzFileName, HANDLE *phFile, BOOL fRead, BOOL fWrite) PURE; STDMETHOD_(BOOL, FCloseHandle) (HANDLE hFile) PURE; };
我发现用[local]属性可以把这类接口转成IDL:
[ local, uuid(f8e47685-e402-4119-aa07-4ea4ff1f1123) ] interface IMsoServerFileManagerSite { BOOL FGetHandle(const WCHAR *pwzFileName, HANDLE *phFile, BOOL fRead, BOOL fWrite); BOOL FCloseHandle(HANDLE hFile); }
- 另一类
IMsoDocExporter接口虽继承IUnknown,但部分方法返回void而非HRESULT。将实现该接口的对象传入Word API后,调试发现我的DLL被combase.dll!CRemoteUnknown::RemQueryInterface调用。
现提出以下疑问:
combase.dll!CRemoteUnknown::RemQueryInterface是否意味着对象被远程查询?即是否需要代理/存根,无法使用[local]属性?- 使用
HANDLE等非自动化类型且不返回HRESULT的接口能否被封送? - 如何将这类接口转换为IDL以让MIDL生成代理/存根实现?(比如MIDL文档提到可使用
call_as映射为远程可调用方法,但不清楚HANDLE的处理方式)
问题解答
1. 关于CRemoteUnknown::RemQueryInterface的含义
是的,CRemoteUnknown::RemQueryInterface的调用确实表明对象正在被跨进程/远程上下文查询。这意味着COM运行时正在尝试为你的对象创建代理/存根,此时[local]属性不再适用——[local]标记的接口只能在同一进程内调用,不支持跨进程封送。
如果你的对象需要被Word进程(或其他外部进程)访问,就必须提供有效的代理/存根实现,不能依赖[local]。
2. 非自动化类型+非HRESULT返回值的接口能否封送
默认情况下不行:
- COM标准封送要求接口方法必须返回
HRESULT,这是跨进程错误处理的基础; HANDLE是进程相关的内核对象引用,本身不具备跨进程可移植性——不同进程中的HANDLE值指向的是完全不同的内核对象实例,直接封送HANDLE会导致无效引用。
不过可以通过自定义封送或类型转换来解决这个问题,不能直接依赖MIDL的默认封送逻辑。
3. 转换为可生成代理/存根的IDL方案
针对这类特殊接口,需要分步骤处理:
步骤1:修正方法返回值为HRESULT
首先,必须把原接口中返回BOOL/void的方法,在IDL中映射为返回HRESULT的方法——这是MIDL生成代理/存根的硬性要求。可以用call_as属性来做本地方法和远程方法的映射:
[ uuid(xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx), object ] interface IMsoDocExporter : IUnknown { // 远程方法:符合COM标准,返回HRESULT HRESULT Remote_Method([in] PARAM_TYPE param); // 本地方法:对应原接口的void/BOOL返回方法 void Method([in] PARAM_TYPE param) call_as(Remote_Method); }
你的实现只需要编写本地方法Method,MIDL会自动生成Remote_Method的代理/存根逻辑,负责把本地调用转换为远程可封送的调用。
步骤2:处理HANDLE类型
HANDLE不能直接封送,需要替换为跨进程可识别的对象标识,比如:
- 如果是文件句柄:可以传递文件路径,在远程进程中重新打开;
- 如果是其他内核对象:可以使用
DuplicateHandle在进程间复制句柄,然后传递复制后的句柄值(注意需要确保目标进程有足够权限); - 自定义对象:可以把对象封装为COM接口,传递接口指针(COM会自动处理代理/存根)。
对于IMsoServerFileManagerSite这类接口,你可以在IDL中重新定义方法,用文件路径替代直接返回HANDLE,或者在远程方法中返回可跨进程的句柄副本:
[ uuid(f8e47685-e402-4119-aa07-4ea4ff1f1123), object ] interface IMsoServerFileManagerSite : IUnknown { HRESULT Remote_FGetHandle([in] const WCHAR *pwzFileName, [out] HANDLE *phFile, [in] BOOL fRead, [in] BOOL fWrite); BOOL FGetHandle([in] const WCHAR *pwzFileName, [out] HANDLE *phFile, [in] BOOL fRead, [in] BOOL fWrite) call_as(Remote_FGetHandle); HRESULT Remote_FCloseHandle([in] HANDLE hFile); BOOL FCloseHandle([in] HANDLE hFile) call_as(Remote_FCloseHandle); }
然后在Remote_FGetHandle的实现中,完成跨进程句柄复制的逻辑,确保远程进程拿到的是有效的句柄。
步骤3:生成代理/存根
确保IDL中所有接口都继承IUnknown(或IDispatch),并且方法都符合COM封送要求(返回HRESULT,使用可封送类型),然后用MIDL编译IDL文件,生成代理/存根DLL,注册后即可支持跨进程调用。
内容的提问来源于stack exchange,提问作者VersBersch

