引用本文(DOI: 10.5281/zenodo.21615494)
本文保存于 Zenodo。以下同时提供始终指向最新版本的 DOI,以及固定于您正在阅读版本的 DOI。
小村 豪(2026)。《什么是 Reg-Free COM——无需注册使用 COM 的机制》。小村软件有限公司。https://doi.org/10.5281/zenodo.21615494 https://comcomponent.com/zh-CN/blog/2026/03/16/011-what-is-reg-free-com/
- DOI(最新版本)
- 10.5281/zenodo.21615494
- DOI(此版本)
- 10.5281/zenodo.22282015
在 COM / ActiveX / OCX 的项目中,每次分发和更新都会遇到同样的问题。
- 需要
regsvr32 - 往往还要管理员权限
- 与其他应用装入的别的版本相互冲突
- 卸载之后连带影响到别的产品
- 开发机上能跑,到干净环境却跑不起来
能把这些麻烦大幅减少的,就是 Reg-Free COM。 不过,它并不像名字听上去那样,是一种“COM 的麻烦全部消失”的魔法。消失的主要是 被全局注册拖累的那部分麻烦,位数(bitness)、依赖 DLL、类型库、线程模型上的难点并不会随之消失。
flowchart TB
accTitle: Reg-Free COM 消除的部分与遗留的部分
accDescr: 说明 Reg-Free COM 消除的主要是被全局注册拖累的麻烦,而位数、依赖 DLL、类型库、线程模型上的难点并不会消失。
rf1["Reg-Free COM"] --> rf2["全局注册的麻烦会减少"]
rf1 --> rf3["也有遗留下来的难点"]
rf3 -.-> rf4["位数 / 依赖DLL / 类型库"]
图1:它不是魔法,要把会消失的麻烦和会留下的麻烦分开来期待。
本文围绕 在 Windows 桌面应用中把 COM DLL / OCX 封闭在应用本地使用 这一场景来梳理 Reg-Free COM。
目标读者与前提
本文写给 正在分发使用现有 COM DLL / OCX 的 Windows 桌面应用、并希望摆脱 regsvr32 与管理员权限的开发者。前提是你至少接触过一次 COM 的基础(CLSID、ProgID、CoCreateInstance、in-proc 服务器)。如果这部分还比较模糊,先读“什么是 COM / ActiveX / OCX——差异与关系的整理与解说”会更快。
操作步骤部分会用到 Windows SDK 的 mt.exe 和 sxstrace,因此假定环境中已安装 Visual Studio 或 Windows SDK。
1. 先说结论(一句话)
先给一个略显粗糙但管用的说法。
- Reg-Free COM 是把 COM 的注册信息保存在清单而不是注册表里的做法
- 运行时,
CoCreateInstance或CLSIDFromProgID的解析会先查看 激活上下文(activation context) - 因此,COM DLL / OCX 可以 按应用程序私有持有
- 主要优点是 便于 XCOPY 分发、更容易避免版本冲突、卸载时不易造成破坏
- 但是,32 位 / 64 位的问题不会消失。这一点靠清单怎么写是绕不过去的
- 另外,依赖 DLL、类型库、设计时引用、对非标准注册信息的依赖 都需要另行考虑
- 在实务中,当你 想把应用专用的 COM 组件放在应用旁边 时,它相当合适
概括地说,Reg-Free COM 是一种 把 COM 的激活拉回到应用程序单位的机制。
flowchart TB
accTitle: 注册信息的存放位置发生了变化
accDescr: 说明 Reg-Free COM 把 COM 的注册信息保存在清单而不是注册表里,解析 CoCreateInstance 等调用时会先查看激活上下文,因此可以按应用程序私有持有 COM 组件。
mg1["注册信息保存在清单里"] --> mg2["解析时先查看激活上下文"]
mg2 --> mg3["COM 组件可按应用程序私有持有"]
mg3 -.-> mg4["便于 XCOPY 分发与避免冲突"]
图2:从注册表转到清单,解析的入口变了,这才是本质。
图中实线表示始终成立的关系,虚线表示带条件的关系(成立条件写在详情页各关系的说明中)。关系的完整列表(共 26 条,附依据与确信度)以及主要概念的定义,汇总在知识地图详情页(日文)。数据:JSON-LD / Turtle
2. 本文所说的 Reg-Free COM
Reg-Free COM 是 Registration-Free COM 的简称。中文里有时也写作“免注册 COM”。
这里说的“免注册”,意思是 使用 COM 时不再全面依赖 HKCR / CLSID / InprocServer32 等全局注册表注册。
既不是说 COM 本身消失了,也不是说 不再需要 GUID。
本文主要涉及的是这些对象。
- 原生 COM DLL
- 基于 ATL 的 COM 服务器
- ActiveX / OCX
- 基于 .NET Framework 的 COM 互操作
- 使用 .NET 5+ / .NET 8 的 COM host 进行公开
反过来说,本文想强调的有两点。
- Reg-Free COM 讲的是“激活”这件事
- 类型信息的分发和设计时的引用设置,可能作为另一个议题保留下来
把这两点混在一起,讨论就会相当浑浊。
flowchart TB
accTitle: 不该混为一谈的两个议题
accDescr: 说明 Reg-Free COM 讲的是激活这件事,而类型信息的分发与设计时的引用设置会作为另一个议题保留下来,把两者混在一起会让讨论变得浑浊。
pt1["Reg-Free COM"] --> pt2["讲的是激活这件事"]
pt3["类型信息分发、设计时引用"] --> pt4["作为另一个议题保留"]
pt2 -.-> pt5["混在一起讨论会变浑浊"]
pt4 -.-> pt5
图3:运行时的话题和设计时的话题,从一开始就分开处理。
3. 先用一张图整理
进入图之前,先把本文反复出现的 4 个词定义好。
| 术语 | 含义 |
|---|---|
| 激活上下文(activation context) | 保存“当前这个线程使用哪个 assembly 的哪个版本”的运行时数据结构。CoCreateInstance 会先查看它,然后才轮到注册表 |
| side-by-side assembly | 让同名组件的不同版本在同一台机器上共存的 Windows 机制。它由清单来标识,assemblyIdentity 相当于它的名牌 |
| 应用程序清单 | 附在 EXE 一侧的 XML,用来写明“自己依赖哪些 side-by-side assembly” |
| 组件清单(assembly manifest) | 附在组件一侧的 XML,用来写明“这个 assembly 包含哪些文件、公开哪些 COM 类”。本来放在注册表里的信息转到了这里 |
激活上下文是按线程持有的。COM 会把创建方线程的激活上下文交给宿主线程,然后才调用 LoadLibrary 和 DllGetClassObject,所以调用方不需要做特别的准备工作。
在此基础上,用一张图看整体会更快。
flowchart LR
APP["MyApp.exe"] --> AM["Application Manifest"]
AM --> DEP["dependentAssembly"]
DEP --> CM["Component / Assembly Manifest"]
CM --> META["file / comClass / typelib"]
META --> DLL["VendorControl.dll / .ocx"]
APP --> ACTX["Activation Context"]
ACTX --> COM["CLSIDFromProgID / CoCreateInstance"]
COM --> DLL
图4:从应用的清单顺着找到组件的清单,不经注册表就抵达 DLL。
在普通的 COM 中,调用 CoCreateInstance 时会沿着注册表决定 加载哪个 DLL。
在 Reg-Free COM 中,会在这之前先查看 当前生效的激活上下文,并依据其中记录的清单信息完成解析。
因此,即使在同一台机器上,应用 A 和应用 B 也更容易各自持有 同一系列 COM 组件的不同版本 并同时运行。 这相当于把 COM 的共享文化,稍稍拉回到偏应用本地的一侧。
4. 为什么普通的 COM 分发容易变重
普通 COM 分发之所以重,与其说是 COM 本身不好,不如说是因为存在 全局注册这个前提。
要使用一个 COM 类,大致需要下面这些信息。
| 信息 | 作用 |
|---|---|
| CLSID | 唯一标识类的 GUID |
| ProgID | 便于人使用的名称 |
| InprocServer32 | 加载哪个 DLL |
| ThreadingModel | Apartment / Both 等前提 |
| TypeLib | 类型信息 |
这些信息一旦进入注册表,从整机层面看是方便的,因为容易被多个应用共享。
只是在实务中,这种共享常常适得其反。
- 某个产品的安装程序覆盖了另一个产品的 COM 注册
- 卸载程序“以为只删了自己的东西”,结果破坏了共享的 COM
- 开发机上碰巧存在的注册项,生产机上并没有
- 32 位和 64 位的注册对不上,只有现象在诡异地偏移
也就是说,比起 COM 本体,分发模型更常让人头疼。 Reg-Free COM 就是为减轻这种分发模型之苦而存在的机制。
flowchart TB
accTitle: 全局共享适得其反的过程
accDescr: 说明 COM 的注册信息进入注册表后整机共享虽然方便,但在实务中容易表现为被其他产品覆盖、卸载时连带破坏、开发机与生产机的环境差异,比起 COM 本体更让人头疼的是分发模型。
gs1["注册信息在整机范围共享"] --> gs2["多个应用都能用,很方便"]
gs1 --> gs3["实务中共享会适得其反"]
gs3 -.-> gs4["覆盖 / 连带破坏 / 环境差异"]
gs3 --> gs5["分发模型让人头疼"]
图5:痛点的真身不是 COM 本身,而是全局注册这个前提。
5. Reg-Free COM 的工作原理
5.1 在应用程序清单中写明依赖关系
首先,应用一侧要把 自己依赖哪些 side-by-side assembly 写进应用程序清单。
这份清单可以
- 像
MyApp.exe.manifest那样放在 EXE 旁边 - 作为资源嵌入 EXE
两种方式都能处理。 在实务中,如果希望分发和替换看得清楚就用外部文件,如果更看重不易损坏和分发的简单就用嵌入,这样区分使用的情况比较多。
另外,当外部文件版和嵌入版同时存在时,文件系统上的清单优先。
5.2 在组件清单中写明 COM 信息
接下来,COM 一侧要把 本来放在注册表里的信息 交给组件清单来持有。
这里会写入的,比如是下面这些信息。
comClassclsidprogidthreadingModeltypelib- 需要的话还有 proxy / stub、window class 等
也就是说,这相当于用 XML 来描述 COM 的面貌,取代注册表。
这份清单可以
- 作为独立文件与 DLL 分开放置
- 作为资源嵌入 DLL
两种方式都可以配置。
在实务中,作为 private assembly 嵌入 DLL 往往更不容易出问题。独立文件方式虽然直观,但在文件名与 assemblyIdentity 的对应、放置位置、漏拷贝这些地方容易绊倒。
flowchart TB
accTitle: 组件清单的持有方式
accDescr: 说明组件清单用 XML 持有本来放在注册表里的 comClass、clsid、typelib 等信息,可以选择与 DLL 分开放置或作为资源嵌入 DLL,实务中嵌入方式更不容易出问题。
cm1["把注册表里的信息用XML持有"] --> cm2["作为独立文件放置"]
cm1 --> cm3["嵌入DLL"]
cm2 -.-> cm4["容易因漏拷贝等绊倒"]
cm3 -.-> cm5["实务中往往不容易出问题"]
图6:信息内容相同,但存放方式的选择会左右出问题的概率。
5.3 运行时会先查看激活上下文
Reg-Free COM 的要害就在这里。
当应用调用 CLSIDFromProgID 或 CoCreateInstance 时,COM 运行时会查看 当前处于活动状态的激活上下文。
如果其中已有所需的 ProgID → CLSID、CLSID → DLL 信息,就能不使用注册表完成解析。
反过来,如果清单中缺少必要信息,就会回退到常规的基于注册的解析。 正是这种行为,制造了 在开发机上恰好能跑 的陷阱。以为已经做成 Reg-Free 了,实际上却一直被本地注册救着场。
这是 Reg-Free COM 最难缠的陷阱。
flowchart TB
accTitle: 回退到基于注册的解析这个陷阱
accDescr: 说明解析 CoCreateInstance 时会先查看激活上下文,但如果清单中缺少必要信息就会回退到常规的基于注册的解析,因而产生开发机上靠本地注册恰好能跑的陷阱。
ac1["解析时先查看激活上下文"] --> ac2{"清单里的信息够不够"}
ac2 -->|"够"| ac3["不使用注册表完成解析"]
ac2 -->|"不够"| ac4["回退到基于注册的解析"]
ac4 -.-> ac5["开发机上恰好能跑的陷阱"]
图7:这种悄无声息的回退,是该机制最大的陷阱。
6. 好处是什么
Reg-Free COM 的优点,在实务中相当明确。
6.1 便于 XCOPY 分发
可以把所需文件一并放进应用文件夹,安装程序和注册处理都会变轻。
当然,如果要写入 Program Files 目录,权限是另一回事,但至少 为 COM 注册而做的管理员操作 是容易减少的。
6.2 更容易减少版本冲突
即使同一台机器上存在多个版本的 COM 组件,也更容易让每个应用分别使用自己那一版。
因为其他产品的安装程序,行为突然变了 这类问题就容易避免得多。
6.3 多数情况下不必大改现有代码
Reg-Free COM 改变的是 解析的方式,而不是从根本上改变现有代码的调用方式。
因此只要用对了场合,往往几乎不用动 CoCreateInstance 一侧的代码就能引入。
6.4 删除和回滚更轻松
因为是按应用程序单位封闭的,更新和回滚都会变得相当自然。 说得极端一点,整个文件夹替换掉 这种思路会更容易采用。
flowchart TB
accTitle: 按应用程序封闭带来的好处
accDescr: 说明把 COM 组件按应用程序封闭起来,会带来便于 XCOPY 分发、容易避免版本冲突、几乎不改现有代码即可引入、删除与回滚简单这几个好处。
cl1["按应用程序封闭起来"] --> cl2["便于XCOPY分发"]
cl1 --> cl3["能减少版本冲突"]
cl2 -.-> cl5["可以整个文件夹替换"]
cl3 -.-> cl4["调用代码几乎不用动"]
图8:这些好处都从“封闭起来”这一个性质里长出来。
7. 适合的场景与不适合的场景
7.1 适合的场景
在这些情况下,Reg-Free COM 相当有力。
| 情况 | 契合度 |
|---|---|
| 想把应用专用的 COM DLL / OCX 随应用一起分发 | 非常好 |
| 想在同一台 PC 上共存多个版本 | 非常好 |
| 想避免供应商组件带来的注册问题 | 好 |
| 想在现有桌面应用中私有地使用 ActiveX / OCX | 好 |
| 想让分发变轻,同时不大改现有调用方式 | 好 |
典型来说,它与 业务桌面应用、设备联动工具、VB6 / MFC / WinForms 的既有资产 契合度很好。
7.2 不适合,或需要谨慎评估的场景
另一方面,也有一些需要谨慎评估的情况。
| 情况 | 说明 |
|---|---|
| 想在整台机器范围内共享 COM | Reg-Free 的好处很淡 |
| 位数本来就对不上 | Reg-Free 解决不了 |
| 强烈依赖非标准的注册信息或自有的安装流程 | 难以改写成清单 |
| 依赖 DLL 或 VC++ 运行库的分发还没理清 | 最终会在别处栽跟头 |
| 设计时工具或 IDE 的引用设置以注册表为前提 | 需要另外设计运维方式 |
最后一点尤其重要。 Reg-Free COM 能帮上 运行时的激活,但 设计时的引用设置界面以什么为前提 并不会因此一次性改变。
flowchart TB
accTitle: 运行时得救而设计时仍在
accDescr: 说明 Reg-Free COM 能帮上运行时的激活,但设计时的工具或 IDE 的引用设置如果以注册表为前提,就不会因此改变,需要另外设计运维方式。
rt1["运行时的激活"] --> rt2["Reg-Free 能帮上忙"]
dt1["设计时的引用设置界面"] --> dt2["有时以注册表为前提"]
dt2 -.-> dt3["需要另外设计运维方式"]
图9:跑起来的机制齐备了,开发用的机制还要另外备齐。
8. 常见误解
8.1 用了 Reg-Free COM,位数问题就会消失
不会消失。 32 位进程只能加载 32 位的 in-proc COM DLL,64 位进程也只能加载 64 位 DLL。 这一点在 Reg-Free 下和以往一样。
8.2 用了 Reg-Free COM 就完全不看注册表
这也不对。 如果清单中缺少必要信息,就会回退到常规的基于注册的解析。 因此,在开发机上成功 并不等于 Reg-Free 配置正确。
8.3 用了 Reg-Free COM,类型库的问题也会自动搞定
这一点只对了一半。
清单里确实也能写 typelib 信息,但 VBA 的引用设置、C++ 的 #import、.NET 一侧的设计时引用生成 等类型信息的处理,通常仍需要另行设计。
Reg-Free COM 首先讲的是 让程序能启动 这件事。 如何以类型化的方式开发,是下一个议题。
flowchart TB
accTitle: 启动的话题与类型的话题之间的先后
accDescr: 说明清单里虽然也能写 typelib 信息,但 VBA 的引用设置、C++ 的 import、.NET 的设计时引用生成等类型信息处理需要另行设计,Reg-Free COM 首先讲的是让程序能启动,类型化开发是下一个议题。
st1["配置 Reg-Free COM"] --> st2["先让程序能启动"]
st2 --> st3["再考虑如何以类型化方式开发"]
st3 -.-> st4["VBA引用设置 / import / interop生成"]
图10:能写 typelib,和类型信息的运维已经办妥,是两回事。
8.4 用了 Reg-Free COM,任何 ActiveX / OCX 都能直接搞定
这一点同样危险。 如果组件建立在标准的 COM 注册信息之上,推进起来会比较顺;但如果它对自定义的注册表设置、额外的安装步骤、许可证处理、其他模块群的依赖很重,Reg-Free 化就会突然变得艰难。
8.5 Reg-Free COM 在 .NET Framework 和 .NET 8 上基本一样
相似之处是有的,但工具链相当不同。
.NET Framework + RegAsm 的场景,和 .NET 5+ / .NET 8 + comhost 的场景,虽然同样是 COM,立足点却不一样。
9. 原生 / .NET Framework / .NET 5+ / .NET 8 的差异
这里容易混在一起,先分开看一次。
| 系列 | 概要梳理 |
|---|---|
| 原生 COM DLL / OCX | 基本思路是应用程序清单 + 组件清单 |
| 基于 .NET Framework 的 COM 互操作 | 除了 Win32 风格的应用程序清单,还需要托管组件一侧的清单 |
| 在 .NET 5+ / .NET 8 上公开 COM | 可以用 EnableComHosting 生成 COM host,再用 EnableRegFreeCom 生成 Reg-Free 用的 manifest |
9.1 基于 .NET Framework 的 COM
在基于 .NET Framework 的 COM 中,会形成两层结构:COM 应用一侧的 Win32 风格 application manifest,以及 托管组件一侧的 component manifest。
也就是说,比原生 COM 时多出一份 manifest。10.4 关于名称和资源 ID 的约束,对这里的 component manifest 同样生效。
9.2 在 .NET 5+ / .NET 8 上公开 COM
在 .NET 5+ / .NET 8 中,COM 公开的入口变成 *.comhost.dll。
再加上 EnableRegFreeCom=true,就会输出 用于 Reg-Free COM 的 side-by-side manifest。
类型信息的处理是另一个议题,这一点如 8.3 所述,而在 .NET Core / .NET 5+ 上还会进一步变化。因为这里不再是 .NET Framework 时代那种 TLB 会自然地从程序集里冒出来 的世界,所以如果需要类型化使用,就得另行搭建 TLB 的生成、嵌入与注册。具体步骤整理在“如何从 VBA 以类型化方式使用 .NET 8 的 DLL——通过 COM 公开与 dscom 生成 TLB”。
flowchart TB
accTitle: .NET 5 以后的 Reg-Free COM
accDescr: 说明在 .NET 5 以后用 EnableComHosting 生成作为 COM 公开入口的 comhost.dll,启用 EnableRegFreeCom 后会输出用于 Reg-Free COM 的 side-by-side 清单,但 TLB 的生成与注册需要另行搭建。
dn1["用EnableComHosting构建"] --> dn2["comhost.dll成为入口"]
dn2 --> dn3["启用EnableRegFreeCom"]
dn3 --> dn4["输出Reg-Free用的清单"]
dn4 -.-> dn5["TLB的处理另行搭建"]
图11:在 .NET 8 上两个属性就能搭好台子,类型信息仍是另一份工作。
10. 最小配置示例
这里给出一个最小示例:MyApp.exe 通过 Reg-Free COM 使用 Vendor.CameraControl.dll。
10.1 文件结构示例
MyApp.exe
MyApp.exe.manifest
Vendor.CameraControl.Asm.manifest
Vendor.CameraControl.dll
Vendor.Helper.dll
上面的例子里,假定 组件清单是以独立文件放置的。 也可以做成嵌入 DLL 的形式,但那样一来,assembly 的名称和资源 ID 就会带上固定的约束。连同步骤一起放在 10.4 讲。
10.2 应用程序清单示例
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
type="win32"
name="KomuraSoft.MyApp"
version="1.0.0.0"
processorArchitecture="amd64" />
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Vendor.CameraControl.Asm"
version="1.0.0.0"
processorArchitecture="amd64" />
</dependentAssembly>
</dependency>
</assembly>
10.3 组件清单示例
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
type="win32"
name="Vendor.CameraControl.Asm"
version="1.0.0.0"
processorArchitecture="amd64" />
<file name="Vendor.CameraControl.dll">
<comClass
clsid="{01234567-89AB-CDEF-0123-456789ABCDEF}"
progid="Vendor.CameraControl.1"
threadingModel="Apartment"
tlbid="{89ABCDEF-0123-4567-89AB-CDEF01234567}" />
<typelib
tlbid="{89ABCDEF-0123-4567-89AB-CDEF01234567}"
version="1.0"
helpdir="" />
</file>
</assembly>
在这个例子里,真正重要的不是 XML 的细节,而是 应用一侧的 dependentAssembly 与组件一侧的 assemblyIdentity 要一致。
这里一旦对不上,就会变成只看错误外观根本判断不出原因的启动失败。
另外,上面的 GUID 和名称只是用于说明的示例。实际使用时,需要按照组件所公开的 CLSID / TLBID / ProgID / threading model 正确填写。
flowchart TB
accTitle: 两份清单的一致要求
accDescr: 说明应用一侧的 dependentAssembly 与组件一侧的 assemblyIdentity 在名称和版本上必须一致,一旦对不上就会变成只看错误外观判断不出原因的启动失败。
mm1["应用侧的dependentAssembly"] --> mm3{"名称和版本是否一致"}
mm2["组件侧的assemblyIdentity"] --> mm3
mm3 -->|"一致"| mm4["解析得以成立"]
mm3 -->|"对不上"| mm5["看不出原因的启动失败"]
图12:比起 XML 的细节,两边的名牌是否一致才决定生死。
10.4 清单放在哪里、怎么嵌入
这是操作步骤中最容易卡住的地方。是作为独立文件放置,还是嵌入二进制,会改变可以取什么名字。
side-by-side 会以下面的顺序,相对于应用文件夹去查找 private assembly。
- WinSxS 文件夹
<appdir>\<assemblyname>.DLL<appdir>\<assemblyname>.manifest<appdir>\<assemblyname>\<assemblyname>.DLL<appdir>\<assemblyname>\<assemblyname>.manifest
如果先找到了与 assembly 同名的 DLL,查找就到此为止。 由此可知,只有下面两种组合能成立。
| 放置方式 | assembly 名 | 文件 | 嵌入的资源 ID |
|---|---|---|---|
| 独立文件 | 取一个与 DLL 名 不同 的名字。例:Vendor.CameraControl.Asm |
把 Vendor.CameraControl.Asm.manifest 放在 DLL 旁边 |
不嵌入 |
| 嵌入 DLL | 与 DLL 名 相同 也可以。例:Vendor.CameraControl |
只有 Vendor.CameraControl.dll |
1 |
也就是说,如果像 10.2 和 10.3 的例子那样使用 Vendor.CameraControl.Asm 这个名字,那就是 独立文件 的做法。若要改成嵌入,就把 assemblyIdentity 的 name 改成 Vendor.CameraControl,并把 dependentAssembly 一侧也统一成同样的名字。
flowchart TB
accTitle: 查找中止的机制
accDescr: 说明 side-by-side 会按 WinSxS 到应用文件夹的顺序查找 private assembly,如果先找到与 assembly 同名的 DLL 查找就会中止,因此独立文件方式必须让 assembly 名与 DLL 名不同。
se1["开始查找private assembly"] --> se2["查找与assembly同名的DLL"]
se2 -->|"找到了"| se3["查找到此为止"]
se2 -->|"没找到"| se4["查找同名的manifest文件"]
se3 -.-> se5["独立文件方式要把名字取得不同"]
图13:命名上的约束,来自查找会中途停止这个规格。
还有一个容易搞错的约束。组件清单不能放进 EXE 的资源里。 能放进 EXE 的是应用程序清单。
嵌入使用 Windows SDK 的 mt.exe(清单工具)。请从 Visual Studio 的开发者命令提示符中执行。
rem 1. 嵌入之前,先通过语法检查
mt.exe -manifest MyApp.exe.manifest -validate_manifest
mt.exe -manifest Vendor.CameraControl.manifest -validate_manifest
rem 2. 把应用程序清单嵌入 EXE
mt.exe -manifest MyApp.exe.manifest -outputresource:MyApp.exe;#1
rem 3. 把组件清单嵌入 DLL(资源 ID 为 1)
mt.exe -manifest Vendor.CameraControl.manifest -outputresource:Vendor.CameraControl.dll;#1
rem 4. 取出来确认是否真的嵌入成功
mt.exe -inputresource:Vendor.CameraControl.dll;#1 -out:extracted.manifest
把第 4 条命令取出的 extracted.manifest 与原始 XML 对照,就能排除“以为嵌进去了其实没有”的情况。
flowchart TB
accTitle: 用 mt.exe 嵌入的步骤
accDescr: 说明使用 mt.exe 时先用 validate_manifest 通过语法检查,再把应用程序清单嵌入 EXE、把组件清单以资源 ID 1 嵌入 DLL,最后取出来与原始 XML 对照确认的流程。
mt1["通过语法检查"] --> mt2["把应用侧嵌入EXE"]
mt2 --> mt3["把组件侧嵌入DLL〔ID 1〕"]
mt3 --> mt4["取出来与原始XML对照"]
图14:嵌入要连同确认一起,当作一个步骤来跑完。
mt.exe 有两点需要注意。
- 清单所引用的文件,必须与清单放在同一个目录下。 如果写了
<file name="Vendor.CameraControl.dll">,就要先把那个 DLL 放到清单旁边再执行。构建输出与清单的管理位置分开时,会在这里卡住 -outputresource省略资源 ID 时会使用CREATEPROCESS_MANIFEST_RESOURCE(= 1)。为了不因为无意中变成 1 而出问题,明确写上;#1读起来也更清楚
如果只是替换已经嵌入好的内容,可以用 -updateresource:<文件>;#1。它等同于给 -inputresource 和 -outputresource 传入相同的参数。
10.5 包含干净环境在内的确认步骤
对 Reg-Free COM 来说,“在开发机上跑通了”几乎没有意义。因为始终存在只是被本地注册表注册救了场的可能。按下面的顺序确认。
- 构建,并把分发物整套集中到一个文件夹里。 包括 EXE、清单、COM DLL、依赖 DLL、VC++ 运行库、proxy / stub DLL
- 准备验证环境。 理想情况是目标 COM 从未被注册过的环境。使用 Windows 沙盒 可以每次都从全新状态开始试
- 先在该环境中确认目标 COM 未被注册。 用下面的命令,如果报出找不到键的错误就说明未注册
- 复制文件夹,直接启动。 不执行安装程序,也不执行
regsvr32 - 确认能一直走到 COM 对象的创建。 只是启动起来,无法验证延迟创建的组件。要实际操作到会执行
CoCreateInstance的界面或功能 - 失败的话,按 11.2 的步骤采集日志
步骤 3 中使用的命令是这些。
reg query "HKCR\CLSID\{01234567-89AB-CDEF-0123-456789ABCDEF}" /reg:64
reg query "HKCR\CLSID\{01234567-89AB-CDEF-0123-456789ABCDEF}" /reg:32
reg query "HKCR\Vendor.CameraControl.1"
32 位和 64 位的注册表视图是两回事,所以 务必查看与应用位数相符的一侧。/reg:64 和 /reg:32 两边都看一遍更保险。
想在开发机上试的时候,先用 regsvr32 /u Vendor.CameraControl.dll 解除注册,再做确认。不过在其他产品也使用同一个 COM 的环境里会产生影响,因此准备干净环境更稳妥。
flowchart TB
accTitle: 在干净环境中确认的流程
accDescr: 说明把分发物整套集中到一个文件夹,准备目标 COM 从未注册过的验证环境,先确认未注册后再复制文件夹直接启动,并操作到会执行 CoCreateInstance 的功能来确认的流程。
cv1["把分发物整套集中起来"] --> cv2["准备干净的验证环境"]
cv2 --> cv3["先确认未被注册"]
cv3 --> cv4["复制过去直接启动"]
cv4 --> cv5["操作到会创建COM的功能"]
cv5 -.->|"失败时"| cv6["采集日志来排查"]
图15:插入“确认未注册”这一步,就能把恰好能跑排除出验证。
11. 容易踩坑的地方
11.1 开发机上能跑,到分发目标却跑不起来
首先要怀疑的是 其实一直被注册表里的注册信息救着场 这种情况。 验证 Reg-Free COM 时,尽量在 干净环境 中做才稳妥。
11.2 出现“side-by-side configuration is incorrect”而无法启动
这一类问题会由 manifest 不一致、依赖 DLL 缺失、VC++ 运行库缺失、架构不符等原因引起。
只看表面的错误文字相当不友好,因此常规做法是用 事件日志 和 sxstrace 追查。
事件日志方面,打开事件查看器 > Windows 日志 > 应用程序,找来源为 SideBySide 的错误。是在解析哪个 assembly 时失败的,会显示在这里。
sxstrace 要一边复现失败一边采集。把命令提示符以管理员身份打开会更可靠。
rem 1. 开始跟踪。这个窗口保持打开
sxstrace trace -logfile:sxstrace.etl
rem 2. 在另一个窗口启动应用,复现失败
rem 3. 停止跟踪。在窗口 1 中按 Enter,或者从另一个窗口执行下面这条
sxstrace stoptrace
rem 4. 把原始的 .etl 转换成人能读的格式
sxstrace parse -logfile:sxstrace.etl -outfile:sxstrace.txt
如果不想出现停止提示,就在步骤 1 加上 -nostop。输出太长时,在步骤 4 加上 -filter:MyApp.exe,可以只筛出目标应用的部分。
转换后的 sxstrace.txt 会按顺序列出去找了哪些清单、在哪里没有匹配上。如果 10.4 的命名方式弄错了,看这里“去找的文件名”也能判断出来。
flowchart TB
accTitle: side-by-side 错误的排查方法
accDescr: 说明出现 side-by-side configuration is incorrect 无法启动时,先在事件查看器中确认来源为 SideBySide 的错误,再用 sxstrace 开始跟踪并复现失败,停止后用 parse 转换成人能读的格式来追查不匹配之处的流程。
sx1["在事件日志中确认SideBySide"] --> sx2["用sxstrace开始跟踪"]
sx2 --> sx3["启动应用复现失败"]
sx3 --> sx4["停止后用parse转换"]
sx4 --> sx5["读出查找与不匹配之处"]
图16:不要在表面的错误文字上纠结,用日志和跟踪去追解析的路径。
11.3 component manifest 与 application manifest 的对应关系错位
- name 不一样
- version 不一样
- processorArchitecture 不一样
- 以为复制过去的 manifest 其实是旧的
这些差别看上去非常小,但在启动时的影响相当大。
11.4 忘记放依赖 DLL
如果只盯着 Vendor.CameraControl.dll 就心满意足,接下来会被加载的 Vendor.Helper.dll、VC++ 运行库、proxy / stub DLL 就会漏掉。
Reg-Free COM 减少的是 COM 注册方面的问题,并不会连 原生依赖解析方面的问题 也一并消除。
11.5 把类型库和引用设置的运维往后拖
即使只把运行时的 activation 打通了,一旦出现
- 想从 VBA 做早期绑定
- 想在 C++ 里用
#import - 想在 .NET 一侧于设计时生成 interop
这些需求,就需要类型信息的分发方式。 Reg-Free COM 并不会自动把这部分全部安排好,因此 把 runtime 和 design-time 分开考虑 很重要。
12. 总结
用一句话概括 Reg-Free COM,那就是 把 COM 的注册信息从整机范围收拢到应用程序单位的机制。
由此带来
- 更容易把 COM DLL / OCX 封闭在应用本地
- 更容易减少版本冲突
- 更容易简化分发与回滚
这些好处。
另一方面,
- 32 位 / 64 位
- 依赖 DLL
- TLB / 引用设置
- 对非标准注册信息的依赖
- 在干净环境中的验证
依然重要。
所以,引入 Reg-Free COM 时的基本姿态是这样。
- 认定这是 activation 层面的话题
- 把 runtime 和 design-time 的议题分开
- 在干净环境中确认
- 先把位数和依赖 DLL 理顺
按这个顺序来看,就能大幅减少踩坑。
flowchart TB
accTitle: 引入时的基本姿态
accDescr: 说明引入 Reg-Free COM 时,认定这是激活层面的话题、把运行时与设计时的议题分开、在干净环境中确认、先把位数与依赖 DLL 理顺,按这个顺序来看就不容易踩坑。
bs1["认定是activation的话题"] --> bs2["分开runtime与design-time"]
bs2 --> bs3["在干净环境中确认"]
bs3 --> bs4["先理顺位数与依赖DLL"]
图17:按顺序守住这 4 条姿态,引入时的问题会少很多。
13. 相关文章
- 什么是 COM / ActiveX / OCX——差异与关系的整理与解说
- 现在该如何处理 ActiveX / OCX——保留、封装、替换的判断表
- 如何从 VBA 以类型化方式使用 .NET 8 的 DLL——通过 COM 公开与 dscom 生成 TLB
14. 参考资料
- Microsoft Learn - 创建 Registration-Free COM 对象
- Microsoft Learn - 应用程序清单
- Microsoft Learn - Assembly Manifests(嵌入时的资源 ID 与名称约束)
- Microsoft Learn - Assembly Searching Sequence(private assembly 的查找顺序)
- Microsoft Learn - Manifest File Schema
- Microsoft Learn - Mt.exe
- Microsoft Learn - 无需注册的 COM 互操作性 (.NET Framework)
- Microsoft Learn - 将 .NET Core 组件公开给 COM
- Microsoft Learn - sxstrace
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
Media Foundation 入门:用 COM 视角理解 API
本文按照最先需要掌握的顺序,结合 COM、HRESULT、IMFSourceReader、MFT 等 Windows 媒体 API 的基础术语,整理 Media Foundation 到底是什么。
WinRT 就是 COM —— IInspectable、.winmd、语言投影,以及 WinUI 至今仍立在二进制契约之上的原因
WinRT 不是托管运行时,而是在 COM 之上加了元数据(.winmd)与语言投影的 ABI。本文从 IUnknown 与 IInspectable 的关系,一直讲到桌面应用里 HWND 初始化与 package identity 的卡点。
什么是 OLE 对象 —— 嵌入与链接的机制以及业务文档中的陷阱
在 Word 中嵌入 Excel 表格的功能,本质就是 OLE 对象。本文从嵌入与链接的区别、复合文件与结构化存储、In-Place Activation 的机制,一直讲到链接断开、文件膨胀与安全对策,全部立足于实务视角。
剪贴板与拖放的工作原理——在业务应用中正确处理 OLE 数据传输
粘贴 Excel 表格会散架、关掉复制源就贴不上,根源都是剪贴板把同一内容放成多种格式的机制。本文讲解标准格式、延迟渲染、OLE 拖放,直到剪贴板历史与云同步的策略。
今天的 Windows 外壳集成——右键菜单、文件关联与 Windows 11 的变化
解读 Windows 11 中右键菜单被藏进“显示更多选项”的原因,并梳理扩展名→ProgID→verb 的关联基础、传统外壳扩展的注意事项,以及 IExplorerCommand 与 MSIX/sparse package 这套新方式。
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
ActiveX 迁移
整理保留、包装或替换 COM / ActiveX / OCX 资产的阶段性判断的主题页面。
与本主题相关的服务
本文与以下服务页面相关联,欢迎从最接近的入口查看。
Windows 应用程序开发
COM DLL / OCX 的分发、清单配置、位数、依赖 DLL,都与 Windows 桌面应用的实现直接相关。
技术咨询 & 设计评审
也适合用来梳理这些判断:该采用 Reg-Free COM,还是保留基于注册的运维方式,以及类型库与设计时引用该如何分开处理。
常见问题
汇总了咨询这一主题时常见的问题。
- 什么是 Reg-Free COM?
- Reg-Free COM 是 Registration-Free COM 的简称,指把 COM 的注册信息保存在清单而不是注册表里的机制。运行时,CoCreateInstance 或 CLSIDFromProgID 在解析时会先查看激活上下文,再根据其中记录的清单信息解析出 DLL。这样就能让 COM DLL/OCX 按应用程序私有持有,带来便于 XCOPY 分发、更容易避免版本冲突、卸载时不易造成破坏这几个优点。
- 改用 Reg-Free COM 之后,32 位/64 位的问题也能解决吗?
- 解决不了。32 位进程只能加载 32 位的 in-proc COM DLL,64 位进程也只能加载 64 位 DLL,这一点在 Reg-Free 下和以往一样。此外,依赖 DLL 与 VC++ 运行库的分发、类型库、设计时的引用设置,以及对非标准注册信息的依赖,都需要另行考虑。Reg-Free COM 消除的主要只是被全局注册拖累的那部分麻烦。
- 为什么 Reg-Free COM 配置在开发机上能跑,到分发目标却跑不起来?
- 首先要怀疑的是:其实一直被注册表里的注册信息救了场。如果清单中缺少必要信息,COM 运行时会回退到常规的基于注册的解析方式,因此在开发机上可能靠本地注册恰好能跑。所以验证 Reg-Free COM 时,在干净环境中进行才稳妥。如果出现 side-by-side configuration is incorrect 而无法启动,多半是清单不一致或依赖 DLL 缺失等原因,常规做法是用事件日志和 sxstrace 追查。
- 用 .NET 8 编写的 COM 组件也能做成 Reg-Free COM 吗?
- 可以。在 .NET 5+/.NET 8 中,用 EnableComHosting 生成作为 COM 公开入口的 *.comhost.dll,再加上 EnableRegFreeCom=true,就会输出用于 Reg-Free COM 的 side-by-side manifest。不过 Reg-Free COM 与 TLB 策略是两个不同的议题。在 .NET Core/.NET 5+ 中,并不像 .NET Framework 时代那样能自然地从程序集得到 TLB,因此如果需要类型化使用(例如 VBA 的早期绑定),最好把 TLB 的生成、嵌入与注册另行梳理清楚。