聊一聊被 .NET程序员 遗忘的 COM 组件

net,程序员,遗忘,com,组件 · 浏览次数 : 5242

小编点评

**COM 多语言互操作** 1. **背景** - COM 组件信息注册在 HKEY_CLASSES_ROOT\\CLSID 节点目录 - 注册表中包含别名为 FlyCom.Show 的注册项 2. **C# 写一个 COM 组件** - 创建接口基类 Fly 和实现类 Fly - 使用 vtable 和属性委托实现接口方法 - 创建 FlyCom.dll 文件并复制到根目录 3. **注册 COM 到注册表** - 使用 CoInitialize 函数初始化 COM 组件 - 使用 CreateInstance 函数创建实例 - 设置别名为 FlyCom.Show 的注册项 4. **使用 C++ 调用 COM 组件** - 使用 CoInitialize 函数初始化 COM 组件 - 使用 vtable 和属性委托实现接口方法 - 创建 FlyCom.dll 文件并复制到根目录 5. **Vtable 的使用** - Vtable 是一个表格,用于存放 COM 组件接口方法的参数类型和方法签名 - 在 C++ 中,可以使用 vtable 来定义接口方法的参数类型 - 在 C# 中,可以使用 vtable 来定义接口方法的返回值类型 6. **COM 的互通原理** - COM 采用 vtable 模式,将接口方法的参数类型和方法签名与 COM 组件的注册表中对应类型和方法签名进行映射 - C++ 中可以使用 vtable 来定义接口方法的参数类型和方法签名,并通过 vtable 来调用 COM 组件的方法

正文

一:背景

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。

  1. ProgId

因为 GUID 不方便记忆,所以给这个 COM组件 取一个别名叫 FlyCom.Show

  1. 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 组件

一:背景 1.讲故事 最近遇到了好几起和 COM 相关的Dump,由于对 COM 整体运作不是很了解,所以分析此类dump还是比较头疼的,比如下面这个经典的 COM 调用栈。 0:044> ~~[138c]s win32u!NtUserMessageCall+0x14: 00007ffc`5c891

.NET周报【10月第3期 2022-10-25】

国内文章 聊一聊被 .NET程序员 遗忘的 COM 组件 https://www.cnblogs.com/huangxincheng/p/16799234.html 将Windows编程中经典的COM组件拿出来再复习一下,解释了COM组件互相调用的原理。 使用 C# 开发的轻量级开源数据库 Lite

聊一聊 WPF 程序的键盘是如何被窃听的?

一:背景 1.讲故事 前几天群里很热闹,看了下在争论两个问题: 电脑里要不要装杀毒软件 ? 应该装什么杀毒软件 ? 不管杀毒软件流氓不流氓,在如今病毒肆虐的当下互联网,装一个还是能帮我们拦截很多意想不到的东西,为了眼见为实,这一篇我们就聊一个窃听 键盘事件 的恶意代码。 2. 思路 实现思路非常简单

前端:异地登录!!!

哈喽,大家好 !今天来聊一聊前端怎么实现异地登录提示!!! 在数字化时代,账户安全是每个用户和开发者都不容忽视的问题。 异地登录提示是一种安全措施,用于提醒用户他们的账户可能在不同的位置被访问。 这通常涉及到检测登录行为的异常,比如IP地址的变化,并在检测到异常时通知用户。 用户登录记录的重要性 首

[转帖]浅谈系统稳定性与高可用保障的几种思路

https://segmentfault.com/u/dewujishu 一、前言 高并发、高可用、高性能被称为互联网三高架构,这三者都是工程师和架构师在系统架构设计中必须考虑的因素之一。今天我们就来聊一聊三H中的高可用,也是我们常说的系统稳定性。 本篇文章只聊思路,没有太多的深入细节。阅读全文大概

SQLSERVER 临时表和表变量到底有什么区别?

一:背景 1. 讲故事 今天和大家聊一套面试中经常被问到的高频题,对,就是 临时表 和 表变量 这俩玩意,如果有朋友在面试中回答的不好,可以尝试看下这篇能不能帮你成功迈过。 二:到底有什么区别 1. 前置思考 不管是 临时表 还是 表变量 都带了 表 这个词,既然提到了 表 ,按推理自然会落到某一个

聊一聊领域驱动与贫血模型

写在前面 前段时间跟领导讨论技术债概念时不可避免地提到了代码的质量,而影响代码质量的因素向来都不是单一的,诸如项目因素、管理因素、技术选型、人员素质等等,因为是技术债务,自然就从技术角度来分析,单纯从技术角度来看代码质量,其实又细分很多原因,如代码设计、代码规范、编程技巧等等,但我个人觉得这些都是技

聊一聊 C# 弱引用 底层是怎么玩的

一:背景 1. 讲故事 最近在分析dump时,发现有程序的卡死和WeakReference有关,在以前只知道怎么用,但不清楚底层逻辑走向是什么样的,借着这个dump的契机来简单研究下。 二:弱引用的玩法 1. 一些基础概念 用过WeakReference的朋友都知道这里面又可以分为弱短和弱长两个概念

聊一聊 Monitor.Wait 和 Pluse 的底层玩法

一:背景 1. 讲故事 在dump分析的过程中经常会看到很多线程卡在Monitor.Wait方法上,曾经也有不少人问我为什么用 !syncblk 看不到 Monitor.Wait 上的锁信息,刚好昨天有时间我就来研究一下。 二:Monitor.Wait 底层怎么玩的 1. 案例演示 为了方便讲述,先

聊一聊Java中的Steam流

在我们的日常编程任务中,对于集合的制造和处理是必不可少的。当我们需要对于集合进行分组或查找的操作时,需要用迭代器对于集合进行操作,而当我们需要处理的数据量很大的时候,为了提高性能,就需要使用到并行处理,这样的处理方式是很复杂的。流可以帮助开发者节约宝贵的时间,让以上的事情变得轻松。