什么是COM组件?
1.COM组件是以WIN32动态链接库(DLL)或可执行文件(EXE)形式发布的可执行代码组成。
2.COM组件是一些小的二进制可执行文件,必须以二进制的形式发布。
3.COM组件可以给应用程序、操作系统以及其它组件提供服务。
4.自定义的COM组件可以在运行时刻同其它组件连接起来构成某个应用程序。
5.COM组件必须是动态链接的。
使用COM组件需要注意:
1.必须要保证升级应用时不破坏与以前版本的向后兼容性;
2.必须要做到扩展系统服务时不依赖于特定的操作系统。
COM组件不是一种计算机语言;
COM组件不是DLL,只是利用DLL来给组件提供动态链接的能力;
COM组件不是API函数集;
COM组件不是类;
COM组件中的接口是一组由组件实现的,提供给客户使用的函数(在COM中的接口是一个包含函数指针数组的内存结构,数组元素是一个由组件实现的函数地址)。DLL的接口就是它所输出的函数。
一:背景
1.讲故事
最近遇到了好几起和 COM
相关的Dump,由于对 COM 整体运作不是很了解,所以分析此类dump还是比较头疼的,比如下面这个经典的 COM 调用栈。
0:044> ~~[138c]s win32u!NtUserMessageCall+0x14: 00007ffc`5c891184 c3 ret 0:061> k # Child-SP RetAddr Call Site 00 0000008c`00ffec68 00007ffc`5f21bfbe win32u!NtUserMessageCall+0x14 01 0000008c`00ffec70 00007ffc`5f21be38 user32!SendMessageWorker+0x11e 02 0000008c`00ffed10 00007ffc`124fd4af user32!SendMessageW+0xf8 03 0000008c`00ffed70 00007ffc`125e943b xxx!DllUnregisterServer+0x3029f 04 0000008c`00ffeda0 00007ffc`125e9685 xxx!DllUnregisterServer+0x11c22b 05 0000008c`00ffede0 00007ffc`600b50e7 xxx!DllUnregisterServer+0x11c475 06 0000008c`00ffee20 00007ffc`60093ccd ntdll!LdrpCallInitRoutine+0x6f 07 0000008c`00ffee90 00007ffc`60092eef ntdll!LdrpProcessDetachNode+0xf5 08 0000008c`00ffef60 00007ffc`600ae319 ntdll!LdrpUnloadNode+0x3f 09 0000008c`00ffefb0 00007ffc`600ae293 ntdll!LdrpDecrementModuleLoadCountEx+0x71 0a 0000008c`00ffefe0 00007ffc`5cd7c00e ntdll!LdrUnloadDll+0x93 0b 0000008c`00fff010 00007ffc`5d47cf78 KERNELBASE!FreeLibrary+0x1e 0c 0000008c`00fff040 00007ffc`5d447aa3 combase!CClassCache::CDllPathEntry::CFinishObject::Finish+0x28 [onecore\com\combase\objact\dllcache.cxx @ 3420] 0d 0000008c`00fff070 00007ffc`5d4471a9 combase!CClassCache::CFinishComposite::Finish+0x4b [onecore\com\combase\objact\dllcache.cxx @ 3530] 0e 0000008c`00fff0a0 00007ffc`5d3f1499 combase!CClassCache::FreeUnused+0xdd [onecore\com\combase\objact\dllcache.cxx @ 6547] 0f 0000008c`00fff650 00007ffc`5d3f13c7 combase!CoFreeUnusedLibrariesEx+0x89 [onecore\com\combase\objact\dllapi.cxx @ 117] 10 (Inline Function) --------`-------- combase!CoFreeUnusedLibraries+0xa [onecore\com\combase\objact\dllapi.cxx @ 74] 11 0000008c`00fff690 00007ffc`6008a019 combase!CDllHost::MTADllUnloadCallback+0x17 [onecore\com\combase\objact\dllhost.cxx @ 929] 12 0000008c`00fff6c0 00007ffc`6008bec4 ntdll!TppTimerpExecuteCallback+0xa9 13 0000008c`00fff710 00007ffc`5f167e94 ntdll!TppWorkerThread+0x644 14 0000008c`00fffa00 00007ffc`600d7ad1 kernel32!BaseThreadInitThunk+0x14 15 0000008c`00fffa30 00000000`00000000 ntdll!RtlUserThreadStart+0x21
为了做一个简单的梳理,我们搭建一个简单的多语言 COM 互操作。
二:COM 多语言互操作
1. 背景
可能很多新生代的程序员都不知道 COM ,最多也只听过这个名词,其实在 Windows 上有海量的 COM 组件,这些组件信息都是注册在 HKEY_CLASSES_ROOT\CLSID
节点目录,截图如下:
这个和微服务中的 注册中心
是一个道理,这一篇我们用 C# 写一个COM组件,用 C++ 去调用。
2. C# 写一个 COM 组件
写一个 .NET Framework 4.8 下的 32bit FlyCom
组件,一个接口,一个实现类,具体原理后续再分析,先搭建尝尝鲜, C# 代码如下:
namespace FlyCom { [Guid("31A3CED7-B4F1-4D59-881A-EA1D7ABCC4CF")] public interface BaseFly { [DispId(1)] string Show(string str); } [Guid("270C3ED3-053D-4324-9176-9C3FA2BE58A7")] [ProgId("FlyCom.Show")] public class Fly : BaseFly { public string Show(string str) { return $"str={str}, length={str.Length}"; } } }
这里简单说一下:
1.Guid
一个是接口(BaseFly) 的唯一码,即 IID 信息, 一个是 COM组件的 唯一码,叫做 CLSID。
2.ProgId
因为 GUID 不方便记忆,所以给这个 COM组件 取一个别名叫 FlyCom.Show
。
3.DispId
这个是为了遵循 COM多语言互通下的 vtable调用标准,表示第一个接口方法是 Show
,后续再聊。
有了代码,接下来还要做三个配置。
- 对 COM 的可见性
修改 AssemblyInfo.cs 中的 ComVisible = true,参考如下:
// Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(true)]
- 生成签名
一般来说,将 com 放到 注册表,最好都生成一个强签名,否则会有警告提示。
- 注册 com 互操作
在属性面板中,选择 Build 选项卡,选中 Register for COM interop
选项即可。
3. 注册 COM 到注册表
要将 com组件 放到注册表,需要使用注册表编辑工具 regasm
。
Microsoft Windows [版本 10.0.19042.746] (c) 2020 Microsoft Corporation. 保留所有权利。 C:\Users\Administrator>cd /d C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\x64 C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\x64>C:\Windows\Microsoft.NET\Framework\v4.0.30319\regasm.exe D:\net6\ConsoleApp1\FlyCom\bin\Debug\FlyCom.dll /tlb:FlyCom.tlb /CodeBase Microsoft .NET Framework 程序集注册实用工具版本 4.8.4084.0 (适用于 Microsoft .NET Framework 版本 4.8.4084.0) 版权所有 (C) Microsoft Corporation。保留所有权利。 成功注册了类型 成功注册了导出到“D:\net6\ConsoleApp1\FlyCom\bin\Debug\FlyCom.tlb”的程序集和类型库 C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\x64>
从输出中可以看到已成功注册,并且生成了一个 FlyCom.tlb
代理文件,接下来可以到注册表中验证一下 GUID=270C3ED3-053D-4324-9176-9C3FA2BE58A7
注册项以及别名为 FlyCom.Show
的注册项。
4. 使用 C++ 调用
要想 C++ 调用 C# 写的 COM 组件,就像 RPC 调用一样,直接自动生成的代理文件即可,将 FlyCom.tlb
复制到 根目录,并且将程序改成 Win32
位,截图如下:
接下来就是完整的 C++ 代码。
#include <Windows.h> #include <string.h> #include <iostream> #import "FlyCom.tlb" named_guids raw_interface_only using namespace std; int main() { CoInitialize(NULL); FlyCom::BaseFlyPtr ptr; ptr.CreateInstance("FlyCom.Show"); wchar_t* c = ptr->Show(L"hello world"); wprintf(L"%s", c); getchar(); }
将程序跑起来后,真的很完美。
从 C++ 调用 COM 的流程图可以很清楚的看到,这是面向接口编程的方式,非常完美。
三:COM 多语言互通原理
1. 架构图
千言万语不及一张图。
这就是 COM 能够实现多语言互通的规范,熟悉 C++ 的朋友肯定知道 vtable
,C++ 能够实现多态,全靠这玩意,COM 也是用了 vtable 这套模式,所以诸如 JAVA,C#,VBS 必须在二进制层面将代码组织成上图这种形式,才能实现 COM 的互通。
所以在 C# 中你看到的 DispId 特性就是为了按照 vtable 方式进行组织,对于 ole32 和 combase 这些 COM 运行环境的基石,我们后续用 windbg 来解读一下,这一篇就先到这里,希望对你有帮助。
到此这篇关于 老生常谈.NET中的 COM 组件的文章就介绍到这了,更多相关.NET COM 组件内容请搜索好代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持好代码网!