引用本文(DOI: 10.5281/zenodo.21615484)
本文保存于 Zenodo。以下同时提供始终指向最新版本的 DOI,以及固定于您正在阅读版本的 DOI。
小村 豪(2026)。《让 VBA 以带类型的方式调用 .NET 8 DLL 的方法 - COM 公开与 dscom TLB》。小村软件有限公司。https://doi.org/10.5281/zenodo.21615484 https://comcomponent.com/zh-CN/blog/2026/03/16/007-dotnet8-dll-typed-vba-com-dscom-tlb/
- DOI(最新版本)
- 10.5281/zenodo.21615484
- DOI(此版本)
- 10.5281/zenodo.22282002
希望从 VBA 调用 .NET 8 处理逻辑的场景依然很常见。尤其是在想保留 Excel 或 Access 的既有资产的同时,只把繁重处理、字符串处理、HTTP、加密、业务逻辑这类部分转移到 C# 中的情况下。
不过,如果依赖 CreateObject 采用延迟绑定,VBA 一侧就会到处都是 Object。IntelliSense 的效果会变弱,方法名的拼写错误要到运行时才会暴露,逐渐陷入依赖字符串硬凑的困境。
flowchart TB
accTitle: 延迟绑定的困境
accDescr: 展示依赖 CreateObject 采用延迟绑定后,VBA 一侧到处都是 Object,IntelliSense 效果变弱,方法名拼写错误要到运行时才会暴露的示意图。
lb1["用 CreateObject 做延迟绑定"] --> lb2["VBA 一侧到处都是 Object"]
lb2 --> lb3["IntelliSense 效果变弱"]
lb2 --> lb4["拼写错误要到运行时才暴露"]
图1:越是依赖延迟绑定,就越会陷入依赖字符串硬凑的困境。
因此本文将聚焦于把 .NET 8 的 DLL 以 COM 形式公开,用 dscom 生成类型库(TLB),并在 VBA 中通过早期绑定进行带类型调用这一主题。
.NET Framework + RegAsm 的旧做法、手写 IDL 再用 MIDL 编译的方式,以及 Reg-Free COM 的话题,本文暂且不谈。这里只讨论 .NET 8 / COM host / dscom / VBA early binding 这一条主线。
另外,本文中出现的代码已作为可构建、可验证的完整示例(COM 公开库、TLB 生成/注册脚本、VBA 模块、单元测试)发布在 GitHub 上。
dotnet8-dll-typed-vba-com-dscom-tlb - komurasoft-blog-samples (GitHub)
所需环境
| 项目 | 需要准备的内容 |
|---|---|
| 操作系统 | Windows。由于要进行 COM 注册,需要能以管理员权限运行 regsvr32 |
| .NET SDK | .NET 8 SDK。EnableComHosting 是 .NET 5 之后才提供的功能 |
| Office | Excel 或 Access。先确认使用的是 32bit 版还是 64bit 版(第 3 章) |
| TLB 生成工具 | dscom。64bit 用和 32bit 用的获取方式不同(第 6 章) |
| 客户端电脑 | 与 Office 位数相同的 .NET 8 运行时(第 9 章) |
在进入操作步骤之前,强烈建议先把自己环境的各个版本记录下来。因为日后一旦出现“步骤明明一样却跑不起来”的情况,能用来对比的信息只有这些。
# .NET SDK 与运行时的列表(也能看出安装的是 x64 还是 x86)
dotnet --info
# Windows 的内部版本号
winver
Office 的版本和位数可以在 Excel 的 文件 > 账户 > 关于 Excel 中确认。对话框标题行的末尾会显示 32 位 或 64 位。
1. 先说结论
先把结论整理成流程,大致是这样:
- 用
EnableComHosting=true编译 .NET 8 类库 - 为 COM 创建明确的接口与类
- 类使用
ClassInterfaceType.None,不要依赖AutoDual - 供 VBA 使用的接口设为
InterfaceIsDual - 从编译后得到的
*.dll,用dscom tlbexport生成*.tlb - 用
regsvr32注册*.comhost.dll - 用
dscom tlbregister注册*.tlb - 在 VBA 中添加引用设置,以
Dim x As 库名.IYourInterface的形式带类型使用
简单来说,这是一种COM 的入口是 .NET SDK 生成的 *.comhost.dll,类型信息是 dscom 生成的 *.tlb,VBA 通过该 TLB 进行早期绑定的结构。
flowchart TB
accTitle: 通往带类型调用的一条主线
accDescr: 展示用 EnableComHosting 编译、用 dscom tlbexport 生成 TLB、用 regsvr32 注册 comhost、用 dscom tlbregister 注册 TLB,最后在 VBA 的引用设置中带类型使用这一流程的示意图。
st1["用 EnableComHosting 编译"] --> st2["用 dscom tlbexport 生成 TLB"]
st2 --> st3["用 regsvr32 注册 comhost"]
st3 --> st4["用 dscom tlbregister 注册 TLB"]
st4 --> st5["在 VBA 中添加引用并带类型使用"]
图2:按照编译、生成 TLB、两次注册、添加引用设置的顺序推进,就能带类型调用。
图中实线表示始终成立的关系,虚线表示带条件的关系(成立条件写在详情页各关系的说明中)。关系的完整列表(共 27 条,附依据与确信度)以及主要概念的定义,汇总在知识地图详情页(日文)。数据:JSON-LD / Turtle
2. 整体结构一览
首先用一张图来看各部分分别承担什么角色。
flowchart LR
VBA["VBA / Excel / Access"] -->|通过引用设置的 TLB 获取类型信息| TLB["VbaTypedComSample.tlb"]
VBA -->|COM 调用| COMHOST["VbaTypedComSample.comhost.dll"]
COMHOST --> DOTNET["VbaTypedComSample.dll (.NET 8)"]
DOTNET --> RUNTIME[".NET 8 Runtime"]
图3:VBA 从 TLB 获取类型信息,并经由 comhost 调用 .NET 8 的实现主体。
各自的角色如下。
| 文件 | 角色 |
|---|---|
VbaTypedComSample.dll |
.NET 8 的实现主体 |
VbaTypedComSample.comhost.dll |
被 COM 调用的入口 |
VbaTypedComSample.tlb |
VBA 看到的类型信息 |
VbaTypedComSample.deps.json |
依赖关系的解析信息 |
VbaTypedComSample.runtimeconfig.json |
.NET 运行时的启动信息 |
这里重要的是,VBA 要了解类型所需要的是 TLB,而作为 COM 启动入口所需要的是 comhost。
不能只给一个 .dll 就了事,这正是 COM 世界不够直观的地方。
3. 首先要确定的事 - 统一 32bit / 64bit
如果在这一步出错,很有可能会滑向“ActiveX 组件不能创建对象。”这类问题。
请让 Office / VBA 与 COM 服务器的 bitness 保持一致。
| 使用方 | .NET 侧的参考设置 | TLB 生成 | 注册命令 |
|---|---|---|---|
| 64bit Office | x64 / win-x64 |
dscom |
C:\Windows\System32\regsvr32.exe |
| 32bit Office(运行在 64bit Windows 上) | x86 / win-x86 |
dscom32.exe |
C:\Windows\SysWOW64\regsvr32.exe |
在 .NET 5+ 之后的 COM host 中,如果保持 AnyCPU,*.comhost.dll 往往会偏向 64bit,有时会与 32bit Office 不匹配。因此,按照 Office 明确指定 x86 / x64 会更安全。
flowchart TB
accTitle: 放任 AnyCPU 导致的不匹配
accDescr: 展示保持 AnyCPU 时 comhost 往往偏向 64bit,可能与 32bit Office 不匹配,因此按照 Office 明确指定 x86 或 x64 更安全的示意图。
b1["保持 AnyCPU"] --> b2["comhost 往往偏向 64bit"]
b2 --> b3["与 32bit Office 不匹配"]
b3 -.-> b4["按照 Office 明确指定位数"]
图4:位数不一致是通往“不能创建对象”的捷径。
本文的代码以 64bit Office 为例。如果是 32bit Office,请把后面出现的 x64 替换为 x86,把 win-x64 替换为 win-x86。
4. 构建 .NET 8 部分
这里以一个可以从 VBA 调用 Add、Divide、Hello 的最小示例来说明。
4.1 .csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<EnableComHosting>true</EnableComHosting>
<PlatformTarget>x64</PlatformTarget>
<NETCoreSdkRuntimeIdentifier>win-x64</NETCoreSdkRuntimeIdentifier>
</PropertyGroup>
</Project>
关键在于 EnableComHosting。加上它之后,编译时就会生成 VbaTypedComSample.comhost.dll。
4.2 默认让整个程序集对 COM 不可见
因为只想让暴露给 COM 的类型设置为 ComVisible(true),所以让整个程序集默认设为 false 会更省事。
using System.Runtime.InteropServices;
[assembly: ComVisible(false)]
4.3 编写要公开的接口和类
using System.Runtime.InteropServices;
namespace VbaTypedComSample;
[ComVisible(true)]
[Guid("2A1BBEDE-DE6E-4C34-AD60-2E9E0E33E999")]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface ICalculator
{
[DispId(1)]
int Add(int x, int y);
[DispId(2)]
double Divide(double x, double y);
[DispId(3)]
string Hello(string name);
}
[ComVisible(true)]
[Guid("FAD1C752-0BB6-4DDD-889F-FE446350847A")]
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(ICalculator))]
public class Calculator : ICalculator
{
public Calculator()
{
}
public int Add(int x, int y) => checked(x + y);
public double Divide(double x, double y)
{
if (y == 0)
{
throw new ArgumentOutOfRangeException(nameof(y), "不能除以 0。");
}
return x / y;
}
public string Hello(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
return "Hello";
}
return $"Hello, {name}";
}
}
这段代码需要注意的要点如下:
Guid分别为接口和类单独分配- 设置
ClassInterfaceType.None,不依赖自动生成的类接口 - 为了方便 VBA 使用,设为
InterfaceIsDual - 预先分配
DispId,有助于减少公开后调整方法顺序时出错的风险 - 因为会被 COM
New出来,所以要准备一个公共的无参构造函数
flowchart TB
accTitle: 公开类型的组织方式
accDescr: 展示给明确接口 ICalculator 加上 InterfaceIsDual 与 DispId,类 Calculator 用 ClassInterfaceType.None 实现该接口,并给接口和类分别分配 Guid 的结构示意图。
if1["ICalculator〔明确接口〕"] --> d1["InterfaceIsDual 与 DispId"]
cl1["Calculator〔类〕"] -->|"实现"| if1
cl1 --> d2["ClassInterfaceType.None"]
if1 -.-> g1["Guid 分别单独分配"]
cl1 -.-> g1
图5:带类型公开的关键,在于明确接口与指定 None 的类这一组合。
5. 进行编译
以 Release 模式进行编译。
dotnet build -c Release
编译完成后,输出文件夹中至少会出现以下这些文件。
bin/
Release/
net8.0-windows/
VbaTypedComSample.dll
VbaTypedComSample.comhost.dll
VbaTypedComSample.deps.json
VbaTypedComSample.runtimeconfig.json
分发和注册时使用的就是这个文件夹。如果之后更改部署位置,也需要重新注册。
6. 用 dscom 生成 TLB
6.1 dscom 是什么
dscom 是一款用于从 .NET 程序集生成并注册 COM 类型库(TLB)的开源命令行工具。它由 dSPACE 公司发布,许可证为 Apache-2.0。
之所以需要它,是因为从 .NET 5 开始,tlbexp.exe 与 RegAsm.exe 已经被废弃。在 .NET Framework 时代,这两个工具可以完成 TLB 生成和程序集注册,但 .NET 5+ 并没有内置它们的后继工具。dscom 正是为了填补这个空缺而开发的。
flowchart TB
accTitle: dscom 填补的空缺
accDescr: 展示 .NET Framework 时代可以用 tlbexp.exe 与 RegAsm.exe 完成 TLB 生成和程序集注册,而 .NET 5 之后二者被废弃且没有内置后继,因此由 dscom 填补空缺的示意图。
old1[".NET Framework 时代"] --> old2["tlbexp.exe 与 RegAsm.exe"]
new1[".NET 5 之后"] --> new2["两者均废弃且无后继"]
new2 --> ds1["由 dscom 填补空缺"]
图6:生成 TLB 的工具,在 .NET 5 之后已经换成了 dscom。
主要的子命令只需要记住这些就够了。
| 子命令 | 作用 |
|---|---|
tlbexport |
从程序集导出 TLB |
tlbregister |
把 TLB 注册到系统 |
tlbunregister |
取消 TLB 的注册 |
tlbdump |
输出 TLB 的内容以便确认 |
tlbembed |
把 TLB 嵌入到文件中 |
tlbdump 很适合在打开 VBA 之前,先确认生成的 TLB 里是否包含了预期的类型。
6.2 64bit 的情况
如果只需要生成 64bit 的 TLB,用 dotnet tool 安装即可。
dotnet tool install --global dscom
接下来,从编译好的程序集生成 TLB。
dscom tlbexport .\bin\Release\net8.0-windows\VbaTypedComSample.dll --out .\bin\Release\net8.0-windows\VbaTypedComSample.tlb
6.3 面向 32bit Office 的情况 - 从哪里获取 dscom32.exe
这里是适配 32bit Office 时最容易卡住的地方。
通过 dotnet tool install 安装的 dscom 只能处理 AnyCPU 或 64bit 的程序集,能生成的也只有 64bit 的 TLB。 要生成 32bit 的 TLB,需要另一个可执行文件 dscom32.exe,而它不在 NuGet 上,需要从 GitHub 的发布页面下载。
- 获取地址:https://github.com/dspace-group/dscom/releases
dscom.exe…… 从 AnyCPU 或 64bit 程序集生成 64bit TLBdscom32.exe…… 从 AnyCPU 或 32bit 程序集生成 32bit TLB
下载得到的 dscom32.exe,在本文的示例中放在项目根目录下的 tools 文件夹里。放在哪里都可以,但请不要和构建输出一起分发。它是开发阶段的工具,运行时并不需要。
还有一个容易被忽略的前提。要运行 dscom32.exe,必须安装 x86 版的 .NET 运行时。 这是因为 dscom 需要加载 hostfxr.dll,在只安装了 x64 版的环境中无法运行。请在 dotnet --info 的输出列表中确认是否存在 x86 的运行时。
flowchart TB
accTitle: 生成 32bit TLB 的准备
accDescr: 展示用 dotnet tool 安装的 dscom 只能生成 64bit TLB,因此 32bit TLB 要用从 GitHub 发布页面获取的 dscom32.exe 生成,而运行它需要 x86 版 .NET 运行时的示意图。
p1["需要 32bit 的 TLB"] --> p2["从发布页面获取 dscom32.exe"]
p2 --> p3["确认是否有 x86 版运行时"]
p3 --> p4["用 dscom32.exe 执行 tlbexport"]
p1 -.-> p5["dotnet tool 版只能生成 64bit TLB"]
图7:适配 32bit 时连工具的获取途径都不一样,所以这里最容易卡住。
.\tools\dscom32.exe tlbexport .\bin\Release\net8.0-windows\VbaTypedComSample.dll --out .\bin\Release\net8.0-windows\VbaTypedComSample.tlb
另外,dscom 自己的文档中也推荐:如果保持 AnyCPU,*.comhost.dll 会被生成为 64bit,因此要在 32bit 环境下使用时,应该把程序集本身按 32bit 编译。这和第 3 章的结论是一致的。
如果觉得每次编译都手动敲命令很麻烦,可以引入 dSPACE.Runtime.InteropServices.BuildTasks 包,在编译时自动生成 TLB。
7. 注册 COM host 和 TLB
请在具有管理员权限的命令提示符 / PowerShell中执行以下操作。
7.1 64bit Office / 64bit COM 的情况
$out = Resolve-Path .\bin\Release\net8.0-windows
C:\Windows\System32\regsvr32.exe "$out\VbaTypedComSample.comhost.dll"
dscom tlbregister "$out\VbaTypedComSample.tlb"
7.2 32bit Office(运行在 64bit Windows 上)的情况
$out = Resolve-Path .\bin\Release\net8.0-windows
C:\Windows\SysWOW64\regsvr32.exe "$out\VbaTypedComSample.comhost.dll"
.\tools\dscom32.exe tlbregister "$out\VbaTypedComSample.tlb"
这里进行的操作有 2 个:
- 用
regsvr32把*.comhost.dll注册为 COM 服务器 - 用
tlbregister把*.tlb注册为类型库
flowchart TB
accTitle: 注册分为两条
accDescr: 展示在管理员权限下,由 regsvr32 把 comhost 注册为 COM 服务器,由 dscom tlbregister 把 TLB 注册为类型库这两次注册的示意图。
adm["以管理员权限执行"] --> r1["用 regsvr32 注册 comhost"]
adm --> r2["用 tlbregister 注册 TLB"]
r1 -.-> m1["注册 COM 的启动入口"]
r2 -.-> m2["注册 VBA 看到的类型信息"]
图8:启动入口和类型信息是两回事,所以注册也要两次配成一组。
8. 在 VBA 中添加引用设置并带类型使用
- 打开 Excel 或 Access
- 用
Alt+F11打开 VBA 编辑器(VBE)。如果从功能区打开,则是开发工具选项卡 >Visual Basic。如果没有显示开发工具选项卡,可以在文件>选项>自定义功能区中勾选开发工具 - 在 VBE 的菜单中选择
工具>引用 可使用的引用列表按字母顺序排列。如果注册成功,其中会出现库名(默认与程序集名相同,即VbaTypedComSample),勾选左侧的复选框后点确定- 如果列表中看不到,就通过
浏览...按钮直接选择VbaTypedComSample.tlb
如果列表中没有出现,原因大多是第 3 章的 bitness 不一致,或者第 7 章的 tlbregister 没有执行成功。从 32bit Office 中看不到以 64bit 注册的 TLB。
flowchart TB
accTitle: 引用列表中不显示时的排查
accDescr: 展示引用列表中看不到该库时,原因大多是 bitness 不一致或 tlbregister 未成功,并且从 32bit Office 看不到以 64bit 注册的 TLB 的示意图。
q1["列表中不显示该库"] --> a1["怀疑第 3 章的 bitness 不一致"]
q1 --> a2["怀疑第 7 章的 tlbregister 失败"]
a1 -.-> nt1["32bit Office 看不到 64bit 的 TLB"]
图9:不显示的原因,基本可以缩小到位数或注册这两者之一。
引用设置是否生效,可以打开 视图 > 对象浏览器(F2),看左上角的库选择框中能否选到 VbaTypedComSample 来确认。如果在这里能看到 ICalculator 和 Calculator,以及 Add / Divide / Hello,说明 TLB 已经正确生成。
Option Explicit
Public Sub UseCalculator()
Dim calc As VbaTypedComSample.ICalculator
Set calc = New VbaTypedComSample.Calculator
Debug.Print calc.Add(10, 20)
Debug.Print calc.Divide(10, 4)
Debug.Print calc.Hello("VBA")
End Sub
把光标放在这个过程中按 F5 执行,再按 Ctrl + G 打开立即窗口,就会按第 4 章的实现输出 3 行内容。
30
2.5
Hello, VBA
如果这里的结果符合预期,说明引用设置、COM 注册、运行时启动、参数与返回值的封送处理全都跑通了。反过来,如果这里的值不对,就要怀疑 .NET 一侧的实现;如果根本无法执行,则要怀疑第 3 章和第 7 章。
flowchart TB
accTitle: 用执行结果做排查
accDescr: 展示示例的执行结果符合预期时说明从引用设置到封送处理全部跑通,值不对时怀疑 .NET 一侧的实现,根本无法执行时怀疑第 3 章的 bitness 和第 7 章注册的排查思路示意图。
r1["执行 VBA 示例"] --> q1{"结果如何"}
q1 -->|"符合预期"| ok1["整条路径都已跑通"]
q1 -->|"值不对"| ng1["怀疑 .NET 一侧的实现"]
q1 -->|"无法执行"| ng2["怀疑位数与注册"]
图10:仅凭这 3 行输出,就能确定应该怀疑哪一层。
这样一来,VBA 一侧就能获得以下好处:
- IntelliSense 可以正常发挥作用
- 方法名的拼写错误在执行前就容易被发现
- 可以在 Object Browser 中确认公开的 API
- 比直接写
Object更容易阅读
8.1 异常在 VBA 一侧会表现为 COM 错误
例如像 Divide(10, 0) 这样在 .NET 一侧抛出异常时,在 VBA 一侧会以 COM 错误的形式出现。
Option Explicit
Public Sub UseCalculatorWithErrorHandling()
On Error GoTo EH
Dim calc As VbaTypedComSample.ICalculator
Set calc = New VbaTypedComSample.Calculator
Debug.Print calc.Divide(10, 0)
Exit Sub
EH:
Debug.Print Err.Number
Debug.Print Hex$(Err.Number)
Debug.Print Err.Description
End Sub
先掌握这里输出值的读法,排查起来会快很多。
| 项目 | 里面是什么 |
|---|---|
Err.Number |
存放的是与 .NET 异常对应的 HRESULT,形式为有符号 Long。十进制不便阅读,可以用 Hex$(Err.Number) 转成十六进制 |
Err.Description |
通过 COM 的 IErrorInfo,原样存放 .NET 的异常消息。对于上面的代码,就是包含 不能除以 0。 的字符串 |
HRESULT 的值按异常类型固定。与 ArgumentOutOfRangeException 对应的是 COR_E_ARGUMENTOUTOFRANGE,值为 0x80131502。也就是说,如果 Hex$(Err.Number) 是 80131502,就说明按预期收到了 .NET 一侧的 ArgumentOutOfRangeException。
把主要的几个列出来,就是这些。
| .NET 的异常 | HRESULT 常量 | 值 |
|---|---|---|
ArgumentException |
COR_E_ARGUMENT |
0x80070057 |
ArgumentOutOfRangeException |
COR_E_ARGUMENTOUTOFRANGE |
0x80131502 |
InvalidOperationException |
COR_E_INVALIDOPERATION |
0x80131509 |
NotSupportedException |
COR_E_NOTSUPPORTED |
0x80131515 |
| 上述之外的一般异常 | COR_E_EXCEPTION |
0x80131500 |
如果想在 VBA 一侧按异常类型分别处理,就要根据这个 HRESULT 来分支。不过,按 HRESULT 分支的设计对 .NET 一侧异常类型的变更很脆弱,所以把业务上的失败用返回值或错误码来表示,而不是用异常,作为边界会更稳定。
flowchart TB
accTitle: .NET 异常传到 VBA 的过程
accDescr: 展示 .NET 一侧抛出的异常在 COM 边界被转换为 HRESULT,在 VBA 中 Err.Number 存放该 HRESULT 的有符号 Long 值,Err.Description 存放经由 IErrorInfo 传来的异常消息的示意图。
x1[".NET 一侧抛出异常"] --> x2["在 COM 边界转换为 HRESULT"]
x2 --> x3["以有符号形式存入 Err.Number"]
x2 --> x4["消息存入 Err.Description"]
x3 -.-> h1["用 Hex$ 转成十六进制阅读"]
图11:异常在 COM 边界变身为 HRESULT,再传到 VBA 的 Err。
9. 分发时的思路
分发时重要的是,不要只分发单个 DLL,而是把整套输出文件都放上去。
VbaTypedComSample.dll
VbaTypedComSample.comhost.dll
VbaTypedComSample.deps.json
VbaTypedComSample.runtimeconfig.json
VbaTypedComSample.tlb
(必要时还需附带依赖 DLL)
此外,客户端 PC 还需要安装对应版本的 .NET 8 运行时。COM host 并不是 self-contained 发布,基本上属于 framework-dependent 的运维方式。
具体要安装的内容如下。
- 下载页面是 .NET 8 的下载页
- 需要的不是 SDK,而是运行时。本文的示例是没有界面的类库,所以
.NET Runtime就够了。如果要使用 WPF 或 Windows 窗体的类型,则需要.NET Desktop Runtime - 位数要与 Office 保持一致。 64bit Office 用 x64,32bit Office 用 x86 的运行时。理由和第 3 章明确指定
x64/x86一样,这里不匹配同样启动不了 - 是否已经安装,可以在客户端 PC 上执行
dotnet --list-runtimes,看有没有Microsoft.NETCore.App 8.x这一行
请务必在分发资料中写清楚“所需运行时的种类、版本和位数”。如果在部署步骤里漏写,到了现场就会看着“ActiveX 组件不能创建对象。”去怀疑位数,白白耗费时间。
flowchart TB
accTitle: 分发时需要备齐的东西
accDescr: 展示分发时不是只放单个 DLL 而要放整套输出文件,客户端 PC 要安装与 Office 位数相同的 .NET 8 运行时,并在分发资料中写明所需运行时的种类、版本和位数的示意图。
h1["部署整套输出文件"] --> u1["能在客户端运行"]
h2["位数相同的 .NET 8 运行时"] --> u1
h3["分发资料中写明运行时信息"] -.-> u1
图12:只有单个 DLL 跑不起来,整套文件与运行时齐备了才能运行。
10. 容易踩的坑
10.1 不要一直保持 AnyCPU
如果 VBA / Office 的 bitness 与 COM host 的 bitness 不一致,会出现相当令人费解的失败方式。
- 64bit Office 应使用
x64/win-x64 - 32bit Office 应使用
x86/win-x86
10.2 不要使用 ClassInterfaceType.AutoDual
虽然看起来很省事,但公开之后一旦调整成员顺序或结构,就很容易出问题。
如果想让 VBA 以带类型的方式稳定使用,标准做法是定义明确的接口,并让类使用 ClassInterfaceType.None。
10.3 不要轻率地重新生成 GUID
在 COM 中,GUID 本身就是契约。如果在公开之后轻率地更换 IID 或 CLSID,会破坏现有的 VBA 引用和注册。
10.4 不要破坏已经公开的接口
在 COM 中,即使只是“事后追加一个方法”,也可能引发麻烦。
- 保留
ICalculator - 如果改动较大,就新建
ICalculator2 - 类也可以同时实现两者
flowchart TB
accTitle: 保护已公开接口的方式
accDescr: 展示已公开的 ICalculator 原样保留,改动较大时新建 ICalculator2,类可以同时实现两者这一兼容性保护方式的示意图。
k1["已公开的 ICalculator"] --> k2["原样保留"]
k3["想做较大的改动"] --> k4["新建 ICalculator2"]
k2 --> k5["类可以同时实现两者"]
k4 --> k5
图13:保留既有契约,把新契约并排加上去,这才是 COM 的做法。
10.5 类型选择要保守一些
在暴露给 VBA 的边界上,不要用得太花哨,会更安全。
比较合适的类型大致有这些:
intdoubleboolstringDateTimedecimalenum
10.6 不要在 Office 处于打开状态时进行更新
如果 Excel 或 Access 一直占用着 DLL,在编译或重新注册时会遇到麻烦。
- 关闭 Office
- 如有必要,先取消注册
- 重新编译
- 再次注册
取消注册要按照与注册时相反的顺序,并使用与注册时位数相同的命令。和注册时一样,同样需要管理员权限。
# 64bit Office / 64bit COM 的情况
$out = Resolve-Path .\bin\Release\net8.0-windows
dscom tlbunregister "$out\VbaTypedComSample.tlb"
C:\Windows\System32\regsvr32.exe /u "$out\VbaTypedComSample.comhost.dll"
# 32bit Office(运行在 64bit Windows 上)的情况
$out = Resolve-Path .\bin\Release\net8.0-windows
.\tools\dscom32.exe tlbunregister "$out\VbaTypedComSample.tlb"
C:\Windows\SysWOW64\regsvr32.exe /u "$out\VbaTypedComSample.comhost.dll"
regsvr32 的 /u 就是取消注册的选项。使用与注册时不同的 regsvr32 是无法取消注册的(以 64bit 注册的内容,无法用 SysWOW64 下的 regsvr32 卸载)。如果在整个文件夹移动或删除之前没有先取消注册,注册表中就会残留指向不存在路径的注册项。
flowchart TB
accTitle: 更新前后如何收拾注册
accDescr: 展示更新时先关闭 Office,必要时按与注册相反的顺序并用位数相同的命令取消注册,然后重新编译并再次注册这一流程的示意图。
u1["关闭 Office"] --> u2["必要时按逆序取消注册"]
u2 --> u3["重新编译"]
u3 --> u4["再次注册"]
u2 -.-> u5["使用与注册时位数相同的命令"]
图14:为了避免 DLL 被占用和注册残留,取消注册与重新注册要成对进行。
11. 总结
“让 VBA 以带类型的方式使用 .NET 8 的 DLL”这件事,只要把范围锁定在COM 公开 + 用 dscom 生成 TLB上,就不是一个多么可怕的流程。.NET 8 一侧只需将 EnableComHosting 设为 true,准备好明确的接口(类使用 ClassInterfaceType.None,供 VBA 使用的接口使用 InterfaceIsDual),用 dscom tlbexport 生成 TLB,再用 regsvr32 注册 *.comhost.dll、用 dscom tlbregister 注册 *.tlb。剩下的只需在 VBA 中添加引用设置并进行早期绑定即可。
如果感到困惑,一个技巧是把 COM host 和 TLB 分开来思考。
- 启动入口是
*.comhost.dll - 类型信息是
*.tlb - 实现主体是
*.dll
12. 参考资料
- 本文的示例代码全集(COM 公开库、脚本、VBA、测试) - komurasoft-blog-samples (GitHub)
- Expose .NET components to COM - Microsoft Learn
- 为实现 COM 互操作而限定 .NET 类型 - Microsoft Learn
- ComInterfaceType 枚举 - Microsoft Learn
- ClassInterfaceType 枚举 - Microsoft Learn
- COM 可调用包装器 - Microsoft Learn
- DispIdAttribute 类 - Microsoft Learn
- dscom - NuGet Gallery
- dspace-group/dscom - GitHub(dscom 本体。包含子命令列表和 32bit 支持的说明)
- dscom 的发布页面(
dscom32.exe的获取地址) - 如何将 HRESULT 与异常对应起来 - Microsoft Learn
- How to use the Regsvr32 tool and troubleshoot Regsvr32 error messages - Microsoft Support
- .NET 8 downloads
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
C# 操作 Excel 时 EXCEL.EXE 残留的问题 ── COM 引用释放模式与替换判断
本文从 COM 引用计数与 RCW 的运行原理,整理 C# 通过 Microsoft.Office.Interop.Excel 操作 Excel 时 EXCEL.EXE 进程残留的问题。内容涵盖「两个点规则」的陷阱、Marshal.ReleaseComObject 派与 G...
什么是 OLE 对象 —— 嵌入与链接的机制以及业务文档中的陷阱
在 Word 中嵌入 Excel 表格的功能,本质就是 OLE 对象。本文从嵌入与链接的区别、复合文件与结构化存储、In-Place Activation 的机制,一直讲到链接断开、文件膨胀与安全对策,全部立足于实务视角。
把 Excel VBA 宏迁移到 Power Automate ── 用 Office Scripts 替换的范围,与继续保留为 VBA 的范围
本文梳理 Excel VBA 宏能否迁移到 Power Automate,涵盖 Office Scripts 可以替换的范围、只有 VBA 才能做到的事情、连接器的限制值、许可证要求,以及从盘点开始的分阶段迁移推进方式。
DLL・COM 接口的向后兼容性 ── 判断哪些改动会破坏调用方的对照表
DLL 或 COM 组件的哪些改动会破坏调用方?本文整理二进制兼容、源代码兼容、行为兼容这三层概念,给出按改动类型划分的判断表、COM 接口不可变的铁律,以及 semver 的实务运用方法,作为一份实务指南。
Arm 版 Windows 上业务应用能运行吗 ── x64 仿真(Prism)与原生 DLL・COM 的现实
面向开发者与信息系统部门,回答「Arm 版 Windows 上业务应用能运行吗」这一问题。梳理 x64 仿真(Prism)的原理、驱动程序等无法运行的层面、.NET 中 AnyCPU 与 P/Invoke 的组合问题,以及 Arm 适配自查清单。
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
ActiveX 迁移
整理保留、包装或替换 COM / ActiveX / OCX 资产的阶段性判断的主题页面。
与本主题相关的服务
本文与以下服务页面相关联,欢迎从最接近的入口查看。
Windows 应用程序开发
涵盖 VBA、COM、Office、.NET 8 以及类型库生成的对接面设计,与 Windows 应用开发结合紧密,因此非常适合作为 Windows 应用开发 的主题来推进。
技术咨询 & 设计评审
如果希望把既有 VBA 资产与 .NET 8 对接的边界设计、位数、注册、TLB 生成以及分发方式一并梳理清楚,可以作为技术咨询与设计评审来推进。
常见问题
汇总了咨询这一主题时常见的问题。
- 要让 VBA 以带类型(早期绑定)的方式调用 .NET 8 的 DLL,需要做什么?
- 需要把 .NET 8 类库用 EnableComHosting=true 编译以生成 *.comhost.dll,再用 dscom tlbexport 生成 *.tlb。接下来用 regsvr32 注册 *.comhost.dll,用 dscom tlbregister 注册 *.tlb,然后在 VBA 的引用设置中添加该 TLB,就可以用 Dim x As 库名.IYourInterface 的形式带类型使用。角色分工是:COM 的启动入口是 *.comhost.dll,VBA 看到的类型信息是 *.tlb,实现主体是 *.dll。
- 出现“ActiveX 组件不能创建对象”错误的原因是什么?
- 典型原因是 Office/VBA 与 COM 服务器的 bitness 不一致。如果是 64bit Office,需要用 x64/win-x64 编译并通过 System32 下的 regsvr32 注册;如果是 32bit Office(运行在 64bit Windows 上),则需要用 x86/win-x86 编译并通过 SysWOW64 下的 regsvr32 注册,生成 TLB 时也要用 dscom32.exe。在 .NET 5+ 的 COM host 中,如果保持 AnyCPU,*.comhost.dll 往往会偏向 64bit,可能与 32bit Office 不匹配,因此按照 Office 的位数明确指定 x86 或 x64 会更安全。
- 不能使用 ClassInterfaceType.AutoDual 吗?
- 虽然看起来省事,但公开之后一旦调整成员顺序或结构就很容易出问题,因此应该避免使用。如果想让 VBA 以带类型的方式稳定使用,标准做法是定义明确的接口,类使用 ClassInterfaceType.None,供 VBA 使用的接口则设为 InterfaceIsDual。预先分配 DispId,可以减少日后调整方法顺序时出错的风险。另外,在 COM 中 GUID 本身就是契约,公开之后若轻率地重新生成 IID 或 CLSID,会破坏现有的 VBA 引用和注册。
- 分发时只提供单个 DLL 就可以了吗?
- 只有 DLL 是不能运行的。需要把实现主体 *.dll、*.comhost.dll、*.deps.json、*.runtimeconfig.json、*.tlb,以及必要的依赖 DLL 一起打包部署。此外,客户端电脑还需要安装对应版本的 .NET 8 运行时,因为 COM host 并不是 self-contained 发布,基本上属于 framework-dependent 的运维方式。还要注意,如果之后更改了部署位置,也需要重新执行注册。