什么是 Reg-Free COM——无需注册使用 COM 的机制

· · COM, Reg-Free COM, Registration-Free COM, Windows开发, 老旧技术

在 COM / ActiveX / OCX 相关的项目中,每次分发和更新都会遇到同样的老问题:

  • 需要 regsvr32
  • 往往还需要管理员权限
  • 与其他应用安装的不同版本相互冲突
  • 卸载后连累到其他产品
  • 在开发机上能跑,到干净环境却跑不起来

能大幅减少这些麻烦的就是 Reg-Free COM。 不过,名字听起来虽然厉害,但它并不是那种「COM 的所有麻烦都会消失」的魔法。它主要消除的是 被全局注册拖累的麻烦,而位数(bitness)、依赖 DLL、类型库、线程模型等方面的难题依然存在。

本文将围绕 在 Windows 桌面应用中把 COM DLL / OCX 限定在应用本地使用 这一场景,来梳理 Reg-Free COM。

1. 先说结论(一句话)

先用一种比较粗略但实用的说法来概括:

  • Reg-Free COM 是把 COM 的注册信息保存在清单中,而不是注册表里的方式
  • 运行时,CoCreateInstanceCLSIDFromProgID 的解析会先查看 激活上下文(activation context)
  • 因此,COM DLL / OCX 就能够 按应用程序私有化
  • 主要优点是 便于 XCOPY 部署更容易避免版本冲突卸载时不易破坏其他程序
  • 但是,32 位/64 位的问题依然存在,这一点靠意志力也跨不过去
  • 此外,依赖 DLL、类型库、设计时引用、非标准的注册依赖 都需要另行考虑
  • 在实务中,当你希望 把应用专用的 COM 组件放在一旁独立管理 时,这种方式相当合适

总而言之,Reg-Free COM 是一种 把 COM 的激活方式拉回到应用程序单位 的机制。

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 进行的公开

反过来,本文想强调的重点有两个:

  1. Reg-Free COM 讨论的是「激活(activation)」这件事
  2. 类型信息的分发和设计时的引用设置,可能仍是另一个独立话题

如果把这两点混在一起讨论,事情就会变得相当混乱。

3. 先用一张图整理

先看一张整体示意图会更直观:

MyApp.exeApplication ManifestdependentAssemblyComponent / Assembly Manifestfile / comClass / typelibVendorControl.dll / .ocxActivation ContextCLSIDFromProgID / CoCreateInstance

在普通的 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 正是为了减轻这种分发模型带来的痛苦而设计的机制。

5. Reg-Free COM 的工作原理

5.1 在应用程序清单中记录依赖关系

首先,应用一侧需要把 自己依赖哪些 side-by-side assembly 写入应用程序清单(application manifest)。

这份清单可以:

  • MyApp.exe.manifest 的形式放在 EXE 旁边
  • 作为资源嵌入到 EXE 中

两种方式都可以。 在实务中,如果希望方便查看和替换分发内容,通常采用外部文件;如果更看重不易损坏和分发的简单性,则倾向于嵌入方式。

需要注意的是,如果外部文件版和嵌入版同时存在,文件系统上的清单会被优先使用

5.2 在组件清单中记录 COM 信息

接下来,COM 一侧需要把 原本保存在注册表中的信息 保存到组件清单(component manifest)中。

其中通常包含如下信息:

  • comClass
  • clsid
  • progid
  • threadingModel
  • typelib
  • 需要的话还有 proxy / stub 或 window class 等

也就是说,这相当于用 XML 来描述 COM 的「身份信息」,取代了注册表的作用。

这份清单可以:

  • 作为独立文件与 DLL 放在一起
  • 作为资源嵌入到 DLL 中

两种方式都可行。

在实务中,把清单作为 private assembly 嵌入到 DLL 中 往往更不容易出事故。以独立文件方式管理虽然直观,但容易在文件名与 assemblyIdentity 的对应关系、放置位置、复制遗漏等环节上出问题。

5.3 运行时会先查看激活上下文

Reg-Free COM 的关键就在这里。

当应用调用 CLSIDFromProgIDCoCreateInstance 时,COM 运行时会查看 当前处于激活状态的上下文(activation context)。 如果其中已经具备所需的 ProgID → CLSID、CLSID → DLL 信息,就可以在不使用注册表的情况下完成解析。

反过来,如果清单中缺少必要信息,就会回退到常规的基于注册表的解析方式。 这种行为会导致一个陷阱——在开发机上恰好能跑。原本以为已经实现了 Reg-Free,实际上却是被本地注册信息「救场」了。

这正是 Reg-Free COM 最令人头疼的陷阱。

6. 好处是什么

Reg-Free COM 的优点在实务中相当明确。

6.1 便于 XCOPY 部署

可以把所需文件一并放入应用文件夹,使安装程序和注册流程变得更轻量。 当然,如果要写入 Program Files 目录,权限问题依然存在,但至少 为 COM 注册所做的管理员操作 是可以减少的。

6.2 更容易减少版本冲突

即使同一台机器上存在多个版本的 COM 组件,也更容易让每个应用分别使用自己所需的版本。 可以在相当程度上避免「因为其他产品的安装而导致行为突然改变」这类事故。

6.3 往往不需要大幅改动现有代码

Reg-Free COM 改变的是 解析方式,而不是从根本上改变现有代码的调用方式。 因此,如果条件合适,往往可以在几乎不修改 CoCreateInstance 相关代码的情况下引入。

6.4 卸载和回滚更轻松

由于是按应用程序单位封闭管理的,更新和回滚会变得相当直接。 极端来说,甚至可以采用 整体替换文件夹 的思路。

7. 适合的场景与需要谨慎对待的场景

7.1 适合的场景

在以下情况下,Reg-Free COM 相当有优势:

情况 适配程度
希望随应用一起分发应用专用的 COM DLL / OCX 非常适合
希望在同一台 PC 上共存多个版本 非常适合
希望避免供应商组件带来的注册事故 适合
希望在现有桌面应用中私有地使用 ActiveX / OCX 适合
希望在不大幅改动现有调用方式的前提下简化分发 适合

典型的适用场景包括 业务桌面应用、设备联动工具,以及 VB6 / MFC / WinForms 的既有资产

7.2 不适合,或需要谨慎评估的场景

另一方面,也有一些需要谨慎评估的情况。

情况 说明
希望在整台机器范围内共享 COM Reg-Free 的优势不明显
位数(bitness)本身不匹配 Reg-Free 无法解决这个问题
强烈依赖非标准的注册信息或独有的安装流程 难以转化为清单形式
依赖 DLL 或 VC++ 运行库的分发尚未理清 最终会在别处出问题
设计时工具或 IDE 的引用设置以注册表为前提 需要另外设计运维方式

最后一点尤其重要。 Reg-Free COM 能帮助解决 运行时的激活,但并不会一次性改变 设计时引用设置界面所依赖的前提条件

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 首先解决的是 让程序能够启动 这件事。 如何以类型化方式进行开发,是接下来的另一个话题。

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 用清单

9.1 基于 .NET Framework 的 COM

在基于 .NET Framework 的 COM 中,会形成两层结构:COM 应用一侧的 Win32 风格 application manifest,以及 托管组件一侧的 component manifest

这部分比原生 COM 稍微复杂一些。 一旦「Reg-Free COM 本身理解了,但一旦涉及托管组件,又多出一份清单」这种情况出现,讨论就会再次变得棘手一点。

9.2 .NET 5+ / .NET 8 上的 COM 公开

在 .NET 5+ / .NET 8 中,COM 公开的入口是 *.comhost.dll。 再加上 EnableRegFreeCom=true,就会输出 用于 Reg-Free COM 的 side-by-side manifest

不过,这里同样重要的一点是,Reg-Free COM 和 TLB 策略是两个独立的话题。 在 .NET Core / .NET 5+ 中,并不像 .NET Framework 时代那样存在「TLB 会自然地从程序集生成」这种情况。如果需要类型化的使用方式,最好单独整理 TLB 的生成、嵌入与注册相关事宜。

10. 最小配置示例

下面展示一个最小示例:MyApp.exe 通过 Reg-Free COM 使用 Vendor.CameraControl.dll

10.1 文件结构示例

MyApp.exe
MyApp.exe.manifest
Vendor.CameraControl.dll
Vendor.Helper.dll

上面的例子中,假设 组件清单是嵌入在 DLL 一侧的。 也可以采用独立文件的方式,但一开始使用嵌入方式会更容易整理清楚。

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 正确填写。

11. 容易踩坑的地方

11.1 开发机上能跑,到部署环境却跑不起来

首先应该怀疑的是,其实是被注册表中的注册信息「顺手」救场了。 验证 Reg-Free COM 时,最好尽量在 干净环境 中进行。

11.2 出现「side-by-side configuration is incorrect」而无法启动

这类问题通常是由清单不一致、依赖 DLL 缺失、VC++ 运行库缺失、架构不匹配等原因造成的。 仅凭表面的错误信息往往不够直观,常见做法是结合 事件日志(Event Log)sxstrace 来追踪。

11.3 组件清单与应用程序清单的对应关系出现偏差

  • name 不一致
  • version 不一致
  • processorArchitecture 不一致
  • 以为已经复制过的清单其实是旧版本

这些差异在外观上看起来很小,但在启动时的影响却相当大。

11.4 忘记放置依赖 DLL

如果只关注 Vendor.CameraControl.dll,而忽略了它后续会加载的 Vendor.Helper.dll、VC++ 运行库、proxy / stub DLL,就会漏掉这部分依赖。 Reg-Free COM 能减少 COM 注册方面的问题,但并不会消除 原生依赖解析方面的问题

11.5 把类型库或引用设置的问题往后拖

即使运行时的激活已经打通,一旦出现以下需求:

  • 想在 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 时,基本姿态应该是:

  1. 把它理解为「激活(activation)」层面的问题
  2. 把运行时与设计时的议题分开考虑
  3. 在干净环境中进行确认
  4. 优先理清位数与依赖 DLL 的问题

按这个顺序来看,就能大幅减少踩坑的概率。

13. 相关文章

14. 参考资料

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

常见问题

汇总了咨询这一主题时常见的问题。

什么是 Reg-Free COM?
Reg-Free COM 是 Registration-Free COM 的简称,指的是把 COM 的注册信息保存在清单(manifest)中而不是注册表里的机制。运行时,CoCreateInstance 或 CLSIDFromProgID 在解析时会先查看激活上下文(activation context),再根据其中记录的清单信息来解析 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 的生成、嵌入与注册。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表