更新记录(1 条,最后更新 2026年08月30日)
本文的修改记录。已保存的更新前版本,可通过带有 DOI 的永久链接阅读。
- 补充了 DllSurrogate 的说明:为什么 regsvr32 成功之后 64 位进程仍然无法加载 32 位的 InprocServer32;如何在 CLSID 上加 AppID、并在该 AppID 键下写入空字符串的 DllSurrogate,从而不写新的 EXE 就把 DLL 放进 dllhost.exe;为什么起来的 dllhost.exe 的位数取决于 DLL 而不是客户端;为什么已注册的 LocalServer32 会优先于代理进程;以及如何验证结果。文中也明确指出代理进程并不会消除位数差异:消失的只是同一进程的约束,调用会变成跨进程,并带来封送和 IPC 的开销。 查看更新前的版本 (DOI: 10.5281/zenodo.21615574)
- 首次发布
引用本文(DOI: 10.5281/zenodo.21615573)
本文保存于 Zenodo。以下同时提供始终指向最新版本的 DOI,以及固定于您正在阅读版本的 DOI。
小村 豪(2026)。《COM/OCX/ActiveX 开发中容易踩坑的注册与位数陷阱》。小村软件有限公司。https://doi.org/10.5281/zenodo.21615573 https://comcomponent.com/zh-CN/blog/2026/04/15/001-com-ocx-activex-pitfalls-visual-studio-bitness-admin-rights/
- DOI(最新版本)
- 10.5281/zenodo.21615573
- DOI(此版本)
- 10.5281/zenodo.22170295
COM 组件、OCX / ActiveX 相关的项目,比起代码本身,更容易在 运行环境、注册、宿主、权限 这些边界上踩坑。
常见的症状大概是这些:
- 编译能通过,但启动时报
0x80040154。 - 在自己的开发机上能跑,换一台电脑就不行。
- 运行时正常,但 Visual Studio 的 Designer 却挂掉。
- 以管理员身份启动就能用,普通权限下就会出问题。
- 已经执行了
regsvr32,却怎么都修不好。
这些与其说是个别 bug,不如说是 COM 的前提条件在某个环节没有对齐 的状态。
如果想先厘清 COM / ActiveX / OCX 这些术语本身,可以先读一下 COM / ActiveX / OCX 是什么 - 区别与关系整理,这样更容易抓住整体脉络。 本文作为下一阶段,整理的是 实际容易卡住的地方,也会涉及 Visual Studio 的位数与管理员权限相关的问题。
1. 先说结论
先用实务中管用的说法来讲:
- COM / OCX / ActiveX 的问题,大多不是出在代码逻辑上,而是出在 位数(32bit / 64bit)、注册位置、宿主、权限 不一致上。
- Visual Studio 2022 是 64 位进程,所以过去建立在 32 位前提上的设计时(design-time)联动,原样搬过来就会出问题。12
regsvr32不是什么都能注册的万能命令,它是给原生 in-proc COM 服务器(DLL / OCX)用的。要把 .NET Framework 公开为 COM,用的是Regasm.exe;到了 .NET 5+ / .NET 6+ / .NET 8+,则是注册生成出来的.comhost.dll。3456- 「以管理员身份能跑就行」这种想法很危险。很多时候只是 恰好因为 per-user 注册而能看到,或者 本该由安装程序完成的注册,只是在开发机上手动做了一遍。378
也就是说,处理 COM / OCX / ActiveX 时,先从这 4 个维度来看会比较安全:
- 是哪个进程在做宿主
- 该进程是 32 位还是 64 位
- 注册在哪里(HKCU / HKLM,32 位 view / 64 位 view)
- 这个操作或运行是否需要管理员权限
2. 从症状反推,大致是这样
| 症状 | 首先怀疑的点 | 常见的真正原因 |
|---|---|---|
0x80040154 Class not registered |
未注册 | 实际上很多时候是「只注册在另一个位数那一侧」或「只注册给了该用户」 |
DllRegisterServer failed: 0x80070005 |
权限不足 | 用标准用户尝试注册、post-build 阶段没有提权就直接注册 |
| VS2022 中只有 Designer 挂掉 | Designer 的限制 | 64 位的 Visual Studio 无法直接读取 32 位 COM / ActiveX |
| 只有以管理员身份启动才能用 | 权限问题 | 本质上大多不是权限本身,而是注册范围或安装设计出现了错位 |
| 64 位应用无法调用 32 位 OCX | COM 机制的限制 | in-proc 服务器必须和宿主的位数一致才能加载 |
| UI 线程能用,后台线程却卡死 | 线程模型 | 违反了 STA / MTA、CoInitializeEx、消息循环相关的前提 |
0x80040154 官方定义是 REGDB_E_CLASSNOTREG,字面意思就是 Class not registered。9
另外,regsvr32 报 0x80070005,官方文档也说明是 因为没有管理员权限,无法写入注册表或 System32 的情况。10
这里重要的是,不要把错误名称的字面意思看得太绝对。
比如 Class not registered 不一定代表「完全没有注册」,也可能只是 注册在了另一个注册表 view 里,或者 注册的位置只有该用户能看到。117
3. 在 Visual Studio 的位数上踩坑
3.1. Visual Studio 2022 变成了 64 位
这是当前 COM / ActiveX 开发中最容易踩坑的地方。
Visual Studio 2022 的 devenv.exe 是 纯 64 位 的。1
因此在 WinForms 的设计时体验中,Visual Studio 一侧 无法直接加载 32 位组件。Microsoft 也明确说明,由于 Visual Studio 2022 是 64 位进程,所以无法加载 32 位的 .NET / COM / ActiveX 组件。2
也就是说,以前的做法是:
- 项目是 x86
- 引用的 ActiveX 也是 x86
- Visual Studio 本身也是 32 位
在这套前提下勉强能成立,但到了 VS2022,就会出现这样拧巴的状况:
- 运行时的应用能以 x86 方式工作
- 但 Designer 却运行在 64 位的 Visual Studio 一侧
结果就是 运行时明明活着,Designer 却单独挂掉 这种相当讨厌的状态。2
3.2. 改成 AnyCPU 不一定能解决问题
这也是常见的误解。
AnyCPU 不是能让依赖链一起变得中立的魔法。
Microsoft 的说明也提到,即便组件本身看起来是 AnyCPU,只要它下游引用了固定 32 位的 COM / ActiveX,在 Visual Studio 2022 的设计时环节依然会出问题。2
所以,如果改成 AnyCPU 之后错误还没消失,不如先怀疑:
- 该程序集下游是否存在 32 位原生依赖
- ActiveX / OCX 是否固定为 x86
- 是否存在只在 Designer 阶段才会加载的代码
这样排查起来更快。
3.3. System32 与 SysWOW64 的陷阱
在 Windows x64 上,名字给人的印象和实际情况是错开的,所以很容易搞混。
Microsoft Learn 中说明,x64 Windows 上的 %windir%\System32 是 给 64 位应用程序用的。32 位一侧会被 WOW64 的文件系统重定向器(redirector)引导到别的位置。12
注册表也是同样的道理,WOW64 的注册表重定向器会让 32 位 / 64 位分别看到 不同的逻辑 view。11
因此,在本地排查时,明确写出 用的是哪个位数的 regsvr32 会更安全。
# 想注册 64 位 DLL / OCX 时
C:\Windows\System32\regsvr32.exe vendor.ocx
# 在 x64 Windows 上想注册 32 位 DLL / OCX 时
C:\Windows\SysWOW64\regsvr32.exe vendor.ocx
这个陷阱讨厌的地方在于,如果注册到了错误的一侧,明明以为注册好了,但目标进程却看不到。 结果就变成:
regsvr32执行成功- 但应用还是报
0x80040154 - 查看注册表,看起来确实存在
- 但看到的其实是另一个 view
3.4. regsvr32 并不是什么都能注册
这里也是误解比较多的地方。
原生的 in-proc COM 服务器通常会导出 DllRegisterServer / DllUnregisterServer,以支持自我注册。3
regsvr32 就是用来对付这类 DLL / OCX 的工具。4
另一方面,如果想让 .NET Framework 程序集能被 COM 使用,基本上要用 Regasm.exe。Microsoft Learn 也说明,注册要供 COM 使用的程序集,应该使用 Regasm.exe。514
另外,到了 .NET 5+ / .NET 6+ / .NET 8+ 的 COM 公开方式,情况会有点变化:只要设置 <EnableComHosting>true</EnableComHosting> 并构建,就会生成 *.comhost.dll,然后用 regsvr32 注册它。6
粗略地分类,大致是这样:
| 想公开的对象 | 典型的注册手段 |
|---|---|
| 原生 C++ 的 DLL / OCX | regsvr32 |
| 将 .NET Framework 程序集公开为 COM | Regasm.exe |
| .NET 5+ / 6+ / 8+ 的 COM 公开 | 对生成的 .comhost.dll 使用 regsvr32 |
把这些区别搞混之后,常见的模式是:
- 对托管 DLL 直接跑
regsvr32 - 自然找不到
DllRegisterServer - 于是误以为「DLL 坏了」
在需要类型库的场景(VBA / VB6,或部分早期绑定的对象)中,TLB 的生成与注册 又是另一个独立的问题。.NET Framework 可以用 Regasm.exe /tlb 来生成并注册类型库,Microsoft 也说明「类型的注册」和「类型库的注册」是两件不同的事。15
3.5. 想从 64 位进程使用 32 位 in-proc 时 —— DllSurrogate
3.3 和 3.4 讲的是「注册的位置和手段有没有对上」。 这里再补上一条出路,用来应对 注册明明是对的,位数却对不上 的情况。
先说结论。
- 哪怕
regsvr32执行成功了,64 位进程也读不了 32 位的InprocServer32。这不是注册没做好,而是 in-proc 这个前提本身决定的。 - 如果不想新写一个 EXE,又想从 64 位一侧使用那个 DLL,那么最小的处理办法就是 给 CLSID 加上
AppID,并在对应的AppID键下写入空字符串的DllSurrogate。16 - 这时启动起来的,不是客户端一侧,而是
InprocServer32所指向的 DLL 一侧位数的dllhost.exe。
为什么保持 in-proc 就是行不通
第 2 章症状表里列出的「64 位应用无法调用 32 位 OCX」,虽然长得和 0x80040154 一模一样,内容却是两回事。
InprocServer32 字面上就是「在调用方的进程内部运行的服务器」这一指定。64 位进程要去加载写在那里的 32 位 DLL,无论怎么修注册表都不会发生。3.3 里注册表的 view 之所以是分开的,追根溯源也是因为这个限制。
所以从这里开始,话题就不再是「把注册修好」,而是 「挪到另一个进程里去」。
代理进程(surrogate)做的事
DllSurrogate 是一项指定,用来 把 in-proc 的 DLL 服务器放到代理进程里,以 Local Server 的形式对外提供。17
把值设为空字符串,就会使用 Windows 自带的默认代理进程(dllhost.exe)。18
如果是 32 位的 DLL,就会启动 32 位的代理进程,DLL 在其中 照旧以 in-proc 方式 被加载。64 位的客户端则从那个进程外面,通过 proxy 来访问对象。
这里很容易被误解,所以明确写出来。
代理进程并不会消除位数的差异。 消失的只是「必须放在同一个进程里」这一约束。调用会变成 out-of-proc,还要额外背上封送(marshaling)和进程间通信的开销。作为前提,还要求 来往的类型是可以封送的(IDispatch、已注册的 proxy / stub、标准封送器能处理的类型,三者之一)。
还有一点。用代理进程比较好对付的,是靠方法调用和事件就能闭环的自动化对象。贴到表单上负责绘制的可视化 OCX,前提是窗口要位于宿主进程内部,所以光靠这一层处理是搞不定的。
flowchart TB
accTitle: 激活最终落到 in-proc 还是代理进程
accDescr: 该图表示,以包含 in-proc 的 CLSCTX 发起请求时,若与客户端同一 view 的 CLSID 下有 InprocServer32,就先以 in-proc 方式加载;若没有,而存在 EXE 服务器的注册,则 EXE 会先于代理进程启动;若没有 EXE 注册而存在空的 DllSurrogate,则启动 DLL 一侧位数的 dllhost;若以上都没有,就无法激活。
req["客户端请求激活"] --> ctx["以包含 in-proc 的 CLSCTX 发起请求"]
ctx --> q1{"同一 view 的 CLSID 下有 InprocServer32 吗"}
q1 -->|"有"| inproc["照旧以 in-proc 方式加载"]
q1 -->|"没有"| q2{"有 EXE 服务器的注册吗"}
q2 -->|"有"| exe["EXE 先于代理进程启动"]
q2 -->|"没有"| q3{"有空的 DllSurrogate 吗"}
q3 -->|"有"| host["启动 DLL 一侧位数的 dllhost"]
q3 -->|"没有"| err["无法激活,0x80040154"]
图 1:激活首先按是否能看到同位数的 InprocServer32 分支,只有看不到时才依次落到 EXE 服务器和空的 DllSurrogate。
注册的最小集合
Microsoft Learn 把 DLL 服务器能被放进代理进程的条件列成了下面这样。16
- CLSID 键下有
AppID这个值,并且存在对应的AppID键 - 激活调用中设置了
CLSCTX_LOCAL_SERVER,并且 CLSID 键下 没有LocalServer32/LocalServer/LocalService - CLSID 键下有
InprocServer32 InprocServer32所指向的 DLL 实际存在AppID键下面有DllSurrogate这个值
如果只是想让那个 DLL 单独跑在一个代理进程里,Microsoft 推荐的写法是 把 AppID 设成和 CLSID 相同的 GUID。16
注册只要 3 行就够。下面是从 64 位宿主使用 32 位 COM DLL 的情形,GUID 是占位符。前提是 已经先用 SysWOW64 一侧的 regsvr32 把 InprocServer32 写进去了(参见 3.3)。
:: 在管理员权限的命令提示符中执行
set CLSID={11111111-2222-3333-4444-555555555555}
set APPID={11111111-2222-3333-4444-555555555555}
:: 1) 给 CLSID 加上 AppID。CLSID 下面 32/64 是分开的,所以要显式指定 view
:: 如果是反过来要从 32 位宿主使用 64 位的 COM DLL,就把这里读作 /reg:64
reg add "HKLM\SOFTWARE\Classes\CLSID\%CLSID%" /v AppID /t REG_SZ /d "%APPID%" /f /reg:32
:: 2) 创建对应的 AppID 键。Classes\AppID 在 32/64 之间是共享的,所以不需要指定 view
reg add "HKLM\SOFTWARE\Classes\AppID\%APPID%" /f
:: 3) 把 DllSurrogate 设为空的 REG_SZ。省略 /d 就会以空字符串创建
reg add "HKLM\SOFTWARE\Classes\AppID\%APPID%" /v DllSurrogate /t REG_SZ /f
为什么要设成空字符串
DllSurrogate 是 REG_SZ,值本身就是代理进程的路径。设为空字符串(或者 NULL)就会使用 系统默认的代理进程,而写上路径就会尝试启动该路径下的自定义代理进程。1718
也就是说,出于好心把 dllhost.exe 的路径写进去反而会适得其反,留空这件事本身就是「请使用默认那个」的指定。
另外,HKLM\SOFTWARE\Classes\AppID 从 Windows 7 / Windows Server 2008 R2 开始,是 32 位 / 64 位 共享 的键,不区分 view。19
而 CLSID 下面的 view 是分开的,所以如果是 32 位的服务器,AppID 值就必须加在 32 位 view 的 CLSID 一侧。不生效的时候,请务必把这两个地方成对确认一遍。
这里看上去和 3.3、6.3 中「只注册在 32 位一侧的话,从 64 位进程看就是 0x80040154」相矛盾,但那边讲的是 in-proc 的情况。在 out-of-proc 的激活中,当客户端和服务器谁都没有提出位数上的要求时,COM 会去找和客户端相匹配位数的服务器,找不到就启动另一个位数的服务器。20
所以这套步骤,在 CLSID 一侧仍然放在 32 位 view 的状态下就能成立。不需要把 InprocServer32 复制到 64 位 view 去。
启动起来的 dllhost.exe 的位数
这里是最常被误解的地方。
代理进程的位数,不是由客户端一侧、而是由 InprocServer32 所指向的 DLL 一侧 决定的。既然 DLL 是在代理进程内部以 in-proc 方式被加载的,那自然就是这样。
| 在其中被加载的 COM DLL | 启动的 dllhost.exe |
|---|---|
| 32 位 | %SystemRoot%\SysWOW64\dllhost.exe |
| 64 位 | %SystemRoot%\System32\dllhost.exe |
在任务管理器或 Process Explorer 里查看命令行,可以看到它是以 dllhost.exe /Processid:{...} 的形式启动的。这里的 GUID 是 AppID 而不是 CLSID。如果当成 CLSID 去找是找不到的,请注意这一点。
有 LocalServer32 时 EXE 会赢
Microsoft Learn 明确写道,当存在 LocalServer / LocalServer32 / LocalService 时,启动 EXE 服务器或服务,总是优先于把 DLL 放进代理进程。16
也就是说,代理进程是「没有 EXE 服务器时的出路」。对已经注册了 EXE 服务器的 CLSID 再加上 DllSurrogate,那边也不会被用到。这里被优先的,终究只是 相对于代理进程 而言,并不是相对于 in-proc。
把对现有调用方的影响也一并弄清楚会更安心。
即便加上了 DllSurrogate,用包含 in-proc 的 CLSCTX 来创建对象的客户端,只要位数是对得上的,就仍然照旧走 in-proc。因为多个 CLSCTX 用 OR 传进去时,会 按枚举顺序(in-proc → local → remote)依次尝试,而在 in-proc 这一步,InprocServer32 键 只要存在就会被使用。20
像 CLSCTX_ALL、CLSCTX_SERVER 这种同时传了 in-proc 和 local 的调用,就属于这种情况。反过来,只用 CLSCTX_INPROC_SERVER 来调用的地方,就算加上 DllSurrogate 也不会转到代理进程去。
而且从 64 位的客户端来看,放进 32 位 view 的 InprocServer32 在 64 位 view 里并不存在。in-proc 这一步会以「没有对应的键」直接跳过,于是就照原样进入 local 一侧(代理进程)的激活。这并不是在尝试加载位数不同的 DLL 然后失败了。2019
确认方法
有没有写进去,明确指定 view 再读回来是最可靠的。
reg query "HKLM\SOFTWARE\Classes\CLSID\{11111111-2222-3333-4444-555555555555}\InprocServer32" /ve /reg:32
reg query "HKLM\SOFTWARE\Classes\CLSID\{11111111-2222-3333-4444-555555555555}" /v AppID /reg:32
reg query "HKLM\SOFTWARE\Classes\AppID\{11111111-2222-3333-4444-555555555555}" /v DllSurrogate
请不要省掉第 1 行。reg add 在指定的键不存在时会创建它,所以如果 GUID 打错了,或者注册到了别的 view,就会产生一个只挂着 AppID 值的空 CLSID 键,而第 2 行照样能通过。只有连 InprocServer32 也在同一个 view 里这一点都确认到了,才算是真的确认过。
DllSurrogate 只要以 值保持为空 的状态显示出来就是对的。之后再从 64 位的客户端创建对象,看看 SysWOW64 一侧的 dllhost.exe 会不会启动起来。
- 仍然是
0x80040154→ 重新检查AppID值是不是加在了 32 位 view 的 CLSID 一侧,以及InprocServer32指向的 DLL 是否实际存在 dllhost.exe起来了,但一调用就挂 → 确认封送的前提(IDispatch/ proxy-stub / 标准封送器)- 起来的是另一个 EXE → 那个 CLSID 上还残留着
LocalServer32之类的项
到这里为止都是「注册」的话题
代理进程够不够用,还是应该在 32 位一侧自己写一个辅助用的 EXE,这不是注册的问题,而是 如何选择构成 的问题。 这部分整理在 ActiveX / OCX 现在该如何处理 - 保留、封装、替换的判断表 的 5.2「想把 32bit OCX 搬到 64bit 一侧」里。
4. 在管理员权限上踩坑
4.1. 在 post-build 阶段做注册,结果只有管理员才能成功
在 Visual Studio 的 C++ 构建中,通过 build event 或 custom build step 调用 regsvr32.exe 本身是很正常的做法。Microsoft Learn 也把用 regsvr32.exe 注册作为 post-build event 的示例列了出来。21
但是,能做到 和 安全 是两件事。
用标准用户执行 regsvr32,可能因为无法写入注册表或 System32 而报 0x80070005。Microsoft 的 KB 也把这个原因归结为 没有管理员权限。10
这里容易发生的事故是:
- 以普通权限启动 Visual Studio,build 能通过
- 但 post-build 的注册环节单独失败
- 失败日志被忽略掉了
- 由于之前留下的旧注册还在,本机恰好能跑
- 在干净环境下自然就不行了
这类项目的基本原则是 把构建和注册分开:
- 构建只负责生成二进制文件
- 注册通过明确的 install step / script / installer 来完成
- 在 CI 中,把「需要注册的步骤」和 build 拆成不同的 job
仅靠这一层分离,就能大幅减少事故。
4.2. 混用 per-user 注册和 per-machine 注册会出问题
如果只记住「COM 看的是 HKCR」,在这里就会卡住。
实际上,正如 Microsoft Learn 所说,COM 会先查看 HKEY_CURRENT_USER\Software\Classes,之后再处理整台机器范围的信息。3
而 HKEY_CLASSES_ROOT 是 HKLM\Software\Classes 与 HKCU\Software\Classes 的合并视图(merged view)。722
也就是说,很常见会出现这种情况:
- 用开发者 A 的用户手动注册过
- A 的账户下能用
- 开发者 B 用不了
- 服务账户也用不了
- 用管理员运行时行为又会变
另外 Microsoft 还指出,需要管理员权限的应用程序,应该在安装时把所依赖的 COM 对象注册到 per-machine 的 COM 配置存储中。87
所以,开发现场应该把这些区别弄清楚:
- 这是 只给自己账户用的开发用注册
- 还是 供该机器全部用户使用的正式注册
- 或者是 供服务、提权应用引用的注册
如果把这些混为一谈,就会出现「不知道为什么只有管理员能用」「Explorer 扩展能用,服务却用不了」之类的问题。
4.3. 让 Visual Studio 一直「以管理员身份运行」并不是解决方案
确实存在需要这样做的场景。 但如果一直用管理员身份启动 Visual Studio,本该由安装程序或注册脚本解决的问题,就可能被 IDE 一侧的提权给掩盖掉。
而且 Visual Studio 本身在提权状态下,per-user extension 的处理方式也会变。Microsoft Learn 也说明,存在一项设置:以 elevated 方式运行 Visual Studio 时 per-user extensions 会被禁用。23
所以在运维上,比较不会后患的做法是:
- 平时开发用普通权限
- 只有需要注册的步骤,才在明确提权后的 Developer Command Prompt / PowerShell / installer 中执行
- 如果「只有管理员才能复现」,就把这个前提本身当作规格整理清楚
5. ActiveX / OCX 特有的坑
5.1. 设计时授权与运行时授权是分开的
OCX / ActiveX 中一个不起眼却很麻烦的问题是 授权(License)。
尤其是老旧的 ActiveX 控件,有时会把 design-time license 和 run-time license 分开处理。MFC 的 ActiveX 文档也说明了通过授权文件或授权密钥来区分 design-time / run-time 的机制。2425
在这个世界里,会出现这样的情况:
- 运行时能用
- 但想拖到表单上时却提示「没有授权」
- 开发机 A 能拖上去
- 开发机 B 却拖不上去
「找不到该组件的授权」「没有合适的授权」这类错误,在老旧的 ActiveX 中并不罕见。26
5.2. 一旦搭到 WinForms 上,就已经套了一层包装
在 WinForms 中使用 ActiveX 时,Windows Forms 并不是直接原样托管 ActiveX。
正如 Microsoft Learn 的 Aximp.exe 文档所说,ActiveX Control Importer 会 根据 COM 类型库生成供 WinForms 使用的包装(wrapper),再把它当作基于 AxHost 的控件来处理。2728
也就是说,问题不只在一层,而是分布在多个层次上:
- 原始的 OCX / ActiveX 本体
- 类型库
- 生成出来的 interop / wrapper
- WinForms Designer / runtime
因此会出现这些情况:
- 换了一版供应商 OCX,事件签名就变了
- 重新添加引用导致 wrapper 被重新生成,产生大量差异
- 每台开发机生成出来的 interop 结果都有些微妙的不同
在 Choose Toolbox Items 里能看到,并不代表就万事大吉。
Designer 能否放上去、运行时事件能否触发、部署到目标环境后连 wrapper 一起能否成立,最好分开来看,这样更安全。
5.3. 轻视 STA / MTA 与消息循环,会导致卡死
COM 要求每个使用它的线程都用 CoInitializeEx 单独初始化。Microsoft Learn 也明确说明,每个使用 COM 的线程都需要各自调用 CoInitializeEx。29
另外,STA(single-threaded apartment,单线程套间)需要有消息循环。2930
尤其是 UI 类的 OCX / ActiveX,大多以 STA 为前提,所以会出现这些讨厌的问题:
- 在 UI 线程上能正常运行
- 一旦丢给
Task.Run或 ThreadPool 就会卡死 - 事件回不来
- 只是偶尔才能复现
此外在 STA 中还有一个前提:不能把接口指针原样复制到别的线程去用,必要时要做封送(marshaling)。2930
这类问题不像 0x80040154 那么「友好」,表现出来就只是 卡死、回不来、偶尔崩溃,所以排查起来和注册表类问题一样费时间。
6. 现场实用的排查顺序
在实务中,与其一开始就深挖,按下面这个顺序切入会更快。
6.1. 先固定「哪个进程是多少位」
首先要看的是这里:
- 宿主是什么(Visual Studio Designer / 自研应用 / Office / Access / Explorer / 浏览器兼容环境)
- 该宿主是 32 位还是 64 位
- 目标 DLL / OCX 是 32 位还是 64 位
- 它是 in-proc 还是 out-of-proc
如果这里没搞清楚就开始查注册表,基本上会迷路。
6.2. 接着确认「注册的种类」
接下来要确认的是 什么该用什么来注册:
- 原生 DLL / OCX →
regsvr32 - .NET Framework COM 公开 →
Regasm.exe - .NET 5+ / 6+ / 8+ COM 公开 →
.comhost.dll - 本身不具备 self-registration 的 DLL → 不该用
regsvr32
仅凭这个分类,就能挡掉相当多的误伤。
6.3. 然后再看「注册在了哪里」
要看的地方,光看 HKCR 是不够的:
HKCU\Software\ClassesHKLM\Software\Classes- 必要时还有 32 位 / 64 位的 registry view
- 目标的 ProgID / CLSID / TypeLib
InprocServer32/LocalServer32- ThreadingModel
- 引用的 DLL 实体路径
仅凭「HKCR 里有」是不够的,只有连同 是谁在看、以什么位数、看的是哪个 view 都对上了,才有意义。711
6.4. 最后看「是不是被权限掩盖了」
最后要确认的是,问题究竟是不是真的出在权限上,还是 只是权限不同导致看到了不同的注册结果:
- 标准用户 / 管理员之间行为是否会变
- 提权运行 Visual Studio 之后会有什么变化
- 换成服务账户或其他用户是否还能复现
- 走安装程序部署到干净环境下是否依然成立
如果只在一台开发机上验证,这里很容易判断失误。
7. 提前定好能减少事故的运维方式
在 COM / ActiveX / OCX 的开发与维护中,有时候 运维层面的约定方式 比实现技巧更管用。
7.1. 先定好位数方针
最先要定的是:
- 是坚持固定 x86
- 还是以 x64 为准
- 还是两者都支持
- 该组件是否必须是 in-proc
尤其是当供应商 OCX 固定为 x86 时,如果无视这一点只把应用改成 x64,之后就会卡住。 这个话题,作为更大规模架构的问题,也和 从 32 位应用调用 64 位 DLL 的方法 - COM 桥接派上用场的案例研究 相关。
7.2. 定好注册策略
注册这件事,也不建议临时应付。
- 供整台机器使用 → 通过 installer 做 per-machine 注册
- 只供该用户使用 → 有意识地使用 per-user
- 只在自己的应用内部闭环 → 考虑 registration-free COM
- 只是开发时需要 → 收进明确的 dev setup script 里
registration-free COM 把激活信息放进 manifest 而不是注册表,是减少「注册地狱」的有效手段。Win32 一侧的 registration-free COM,以及 .NET 一侧的 RegFree COM,官方都有相应说明。31632
7.3. 不要把生成物过多地放在 source control 之外
OCX / ActiveX 系统常见的问题是依赖物散落各处:
- OCX 本体
- 依赖 DLL
- TLB
.lic- interop DLL
- AxHost wrapper
- 注册脚本
- 示例宿主
如果这些东西只存在于某个人的本机上,几个月后必然会出事。
至少下面这些内容,最好和代码放在同一个地方留存:
- 前提是哪个版本
- 按什么顺序安装什么
- 用什么命令注册
- 面向 x86 还是 x64
8. 这类咨询比较合适
这个主题,哪怕只是在全面改造之前先做 排查 和 方针整理,也很容易产生价值。
比如,以下这类咨询就特别合适:
- 想把
0x80040154、0x80070005的原因,按位数 / 注册 / 权限拆开来整理 - 升级到 Visual Studio 2022 后 Designer 挂了,想看看能救到什么程度
- 想在保留供应商 OCX 的前提下,把周边逐步转向 .NET 或 C#
- 想确定 x86 固定资产还能延续到哪一步,从哪里开始 bridge / wrap / replace
- 想摆脱手动
regsvr32的依赖,重新设计 install / deploy 流程
关于替换、封装、保留的判断,也可以参考 ActiveX / OCX 现在该如何处理 - 保留、封装、替换的判断表。
9. 总结
在 COM 组件、OCX / ActiveX 开发中踩坑时,原因大致都能归结到这 4 点:
- 位数没对齐
- 注册方式用错了
- 注册范围(HKCU / HKLM、32 位 / 64 位 view)出现了偏差
- 把「恰好因为权限而能看到」的状态误当成了正常
随着 Visual Studio 2022 变为 64 位,过去那种「说不清为什么就能跑」的设计,变得相当容易暴露出来。12 正因为如此,接触 COM / OCX / ActiveX 时,在写代码之前先把环境前提统一好,才是捷径。
与其纠结 regsvr32 要敲几次,不如先把这些理清楚,解决速度会快得多:
- 是哪个进程在做宿主
- 该进程是多少位
- 应该注册在哪里
- 这个注册真的需要管理员权限吗
- 有没有把 Designer 和 runtime 分开来看
参考
-
Microsoft Learn, Visual Studio 2022 version 17.0 Release Notes —
devenv.exe is now 64-bit only. ↩ ↩2 ↩3 -
Microsoft Learn, Troubleshoot 32-bit problems - Windows Forms — Visual Studio 2022 是 64 位进程,无法直接加载 32 位的 .NET / COM / ActiveX,以及 out-of-process designer 的限制。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Classes and Servers — COM 的注册、HKCU / HKCR、self-registration 与
DllRegisterServer。 ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, 向 COM 注册程序集 — .NET Framework 的 COM 注册使用
Regasm.exe。 ↩ ↩2 -
Microsoft Learn, 向 COM 公开 .NET Core 组件 —
EnableComHosting、生成的.comhost.dll、regsvr32、EnableRegFreeCom。 ↩ ↩2 ↩3 -
Microsoft Learn, Merged View of HKEY_CLASSES_ROOT — HKCR 是 HKLM 与 HKCU 的合并视图(merged view)。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, HKEY_CLASSES_ROOT Key — 需要管理员权限的应用推荐注册到 per-machine COM 配置中。 ↩ ↩2
-
Microsoft Learn, COM Error Codes (Generic) (Winerror.h) —
REGDB_E_CLASSNOTREG (0x80040154)等。 ↩ -
Microsoft Learn, You receive 0x80070005 error when you try to register a DLL by using Regsvr32.exe — 权限不足导致 DLL 注册失败的典型案例。 ↩ ↩2
-
Microsoft Learn, Registry Redirector — WOW64 中 32 位 / 64 位的注册表 view。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, File System Redirector — x64 Windows 的
%windir%\System32与 WOW64 的文件系统重定向。 ↩ -
Microsoft Learn, 64 位版本 Windows 上 32 位程序兼容性注意事项概述 — WOW64 带来的文件 / 注册表重定向。 ↩
-
Microsoft Learn, Regasm.exe (Assembly Registration Tool) —
Regasm.exe的作用及/tlb等选项。 ↩ -
Microsoft Learn, Packaging a .NET Framework Assembly for COM — 类型库与
Regasm.exe /tlb。 ↩ -
Microsoft Learn, Registering the DLL Server for Surrogate Activation — 放进代理进程的条件、存在
LocalServer/LocalServer32/LocalService时优先启动 EXE 服务器或服务、把AppID设成与 CLSID 相同 GUID 的构成。 ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, DllSurrogate —
AppID下的DllSurrogate是REG_SZ,为空字符串时使用系统默认的代理进程,写上路径则使用该路径下的自定义代理进程。 ↩ ↩2 -
Microsoft Learn, Using the system-supplied surrogate — 指定空字符串或
NULL时会启动系统默认的代理进程,以及代理进程内部的线程模型与进程生存期的处理。 ↩ ↩2 -
Microsoft Learn, CLSCTX enumeration — 多个
CLSCTX用 OR 传入时会按枚举顺序依次尝试,在 in-proc 这一步只要存在InprocServer32键就会使用它,以及客户端和服务器都没有提出位数要求时会选择与客户端相匹配位数的服务器、没有则启动另一侧。 ↩ ↩2 ↩3 -
Microsoft Learn, Understanding Custom Build Steps and Build Events — 在 post-build event 中使用
regsvr32.exe的示例。 ↩ -
Microsoft Learn, 面向高级用户的 Windows 注册表 —
HKCU\Software\Classes与HKLM\Software\Classes、以及 HKCR 的行为。 ↩ -
Microsoft Learn, Find, install, and manage extensions for Visual Studio — elevated 运行时对 per-user extension 的处理方式。 ↩
-
Microsoft Learn, MFC ActiveX Controls: Licensing an ActiveX Control — design-time / run-time license、
.LIC。 ↩ -
Microsoft Learn, Application Settings, MFC ActiveX Control Wizard — 运行时授权(license)的生成与
.lic文件。 ↩ -
Microsoft Learn, License information for this component not found. You don’t have an appropriate license to use this functionality in the design environment. ↩
-
Microsoft Learn, Aximp.exe (Windows Forms ActiveX Control Importer) — 将 ActiveX 转换为供 WinForms 使用的包装(wrapper)。 ↩
-
Microsoft Learn, AxHost Class — ActiveX Control Importer 生成的基于 AxHost 的包装。 ↩
-
Microsoft Learn, 初始化 COM 库 —
CoInitializeEx、按线程各自初始化、STA 的消息循环。 ↩ ↩2 ↩3 -
Microsoft Learn, Single-Threaded Apartment(单线程套间) — STA 的消息循环、封送(marshaling)、
ThreadingModel。 ↩ ↩2 -
Microsoft Learn, 创建 Registration-Free COM 对象 — 通过 activation context 实现的免注册 COM。 ↩
-
Microsoft Learn, 无需注册的 COM 互操作功能 — .NET Framework 的 registration-free COM interop。 ↩
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
Office 2024/Microsoft 365 中 ActiveX 无法运行的原因与排查步骤
梳理在 Office 2024/Microsoft 365 中 ActiveX 无法运行时,按默认禁用、32bit/64bit、COM 注册、依赖 DLL、IE 模式、Click-to-Run 日志的顺序进行排查的方法。
COM / ActiveX / OCX 是什么 - 区别与关系一并梳理
从实务视角梳理什么是 COM、什么是 ActiveX、什么是 OCX,一直讲到三者的区别与关系、与 OLE 的关联、它们被用在哪里,以及现在应该怎么看待它们。
ActiveX / OCX 现在该如何处理 - 保留、封装、替换的判断表
本文整理发现 ActiveX / OCX 时应该选择保留、封装还是替换,涵盖 32bit / 64bit、注册、浏览器依赖、供应商维护等因素。
32 位应用调用 64 位 DLL 的 COM 桥接实例
本文整理当 32 位应用无法直接调用 64 位 DLL 时,通过 COM 桥接进行集成的思路,并结合 Windows 的限制、架构构成与处理流程加以说明。
委托开发 Windows 应用程序前该梳理的事项
在委托外包开发 Windows 应用程序之前,梳理现有软件改造、设备联动、COM/ActiveX、发布与更新、维护等需要注意的要点。
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
ActiveX 迁移
整理保留、包装或替换 COM / ActiveX / OCX 资产的阶段性判断的主题页面。
32 位 / 64 位互通
整理 32 位 / 64 位互通、原生边界与相关 Windows 设计判断的主题页面。
与本主题相关的服务
本文与以下服务页面相关联,欢迎从最接近的入口查看。
Windows 应用程序开发
支持包含常驻处理、设备联动、运行日志与可维护结构的 Windows 桌面应用程序。
既有资产活用 & 迁移支持
在持续活用 COM / ActiveX / OCX 资产、原生代码与 32 位依赖的同时,协助规划阶段性的迁移。
常见问题
汇总了咨询这一主题时常见的问题。
- 0x80040154(Class not registered)应该怎么排查?
- 官方定义是 REGDB_E_CLASSNOTREG,字面意思是「类未注册」,但并不代表完全没有注册。也可能是只注册在另一个位数的注册表 view 里,或者只注册在只有该用户可见的 HKCU 一侧。最快的排查方式是:先确定宿主进程是 32 位还是 64 位,再检查注册手段是否正确,最后依次确认注册位置到底落在 HKCU / HKLM 以及 32 位 / 64 位 view 的哪一处。
- 用 regsvr32 注册之后还是不能用,是为什么?
- regsvr32 是给导出 DllRegisterServer 的原生 in-proc COM 服务器(DLL / OCX)用的,并不是什么都能注册的万能命令。.NET Framework 程序集要公开为 COM 组件,走的是 Regasm.exe;.NET 5 及之后版本则是先通过 EnableComHosting 生成 .comhost.dll,再用 regsvr32 注册它。另外,在 x64 Windows 上,64 位组件要用 System32 下的 regsvr32,32 位组件要用 SysWOW64 下的 regsvr32,如果注册到了错误的一侧,就会出现「注册显示成功,但目标进程却看不到」的情况。
- 为什么升级到 Visual Studio 2022 之后,只有 Designer 会挂掉?
- 因为 Visual Studio 2022 的 devenv.exe 是 64 位进程,无法直接加载 32 位的 COM / ActiveX 组件。哪怕运行时的应用本身能以 x86 方式正常工作,Designer 却是在 Visual Studio 那一侧的 64 位进程里运行的,于是就出现「运行时明明正常,却只有 Designer 挂掉」这种拧巴的状况。即便把项目设为 AnyCPU,只要引用链的下游还依赖固定 32 位的 COM / ActiveX,问题依然存在。
- 为什么以管理员身份运行就能用,普通权限却不行?
- 本质上往往不是权限本身的问题,而是注册范围或安装设计出现了错位。COM 会先查看 HKCU\Software\Classes,而 HKEY_CLASSES_ROOT 其实是 HKLM 与 HKCU 的合并视图(merged view)。如果是开发者 A 用自己的账户手动注册过,那么在 A 的账户下能用,换成其他用户或服务账户就用不了,这是很常见的现象。基本原则是要区分「仅供开发用的 per-user 注册」和「由安装程序完成的正式 per-machine 注册」,并把构建与注册这两个步骤分开。