让 VBA 以带类型的方式调用 .NET 8 DLL 的方法 - COM 公开与 dscom TLB
· 小村 豪 · C#, .NET 8, VBA, COM, Office, dscom
希望从 VBA 调用 .NET 8 处理逻辑的场景依然很常见。尤其是在想保留 Excel 或 Access 的既有资产的同时,只把繁重处理、字符串处理、HTTP、加密、业务逻辑这类部分转移到 C# 中的情况下。
不过,如果依赖 CreateObject 采用延迟绑定,VBA 一侧就会到处都是 Object。IntelliSense 的效果会变弱,方法名的拼写错误要到运行时才会暴露,逐渐陷入依赖字符串硬凑的困境。
因此本文将聚焦于把 .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)
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 进行早期绑定的结构。
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"]
各自的角色如下。
| 文件 | 角色 |
|---|---|
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 会更安全。
本文的代码以 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出来,所以要准备一个公共的无参构造函数
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 64bit 的情况
首先安装 dscom。
dotnet tool install --global dscom
接下来,从编译好的程序集生成 TLB。
dscom tlbexport .\bin\Release\net8.0-windows\VbaTypedComSample.dll --out .\bin\Release\net8.0-windows\VbaTypedComSample.tlb
6.2 面向 32bit Office 的情况
这里有一个小陷阱。如果要为 32bit Office 生成 TLB,使用 dscom32.exe 会更安全。
.\tools\dscom32.exe tlbexport .\bin\Release\net8.0-windows\VbaTypedComSample.dll --out .\bin\Release\net8.0-windows\VbaTypedComSample.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注册为类型库
8. 在 VBA 中添加引用设置并带类型使用
- 打开 Excel 或 Access
- 打开 VBA 编辑器
工具->引用- 如果列表中出现了该库,就勾选它
- 如果列表中没有出现,就从
浏览...中选择VbaTypedComSample.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
这样一来,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 Err.Description
End Sub
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 的运维方式。
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 - 类也可以同时实现两者
10.5 类型选择要保守一些
在暴露给 VBA 的边界上,不要用得太花哨,会更安全。
比较合适的类型大致有这些:
intdoubleboolstringDateTimedecimalenum
10.6 不要在 Office 处于打开状态时进行更新
如果 Excel 或 Access 一直占用着 DLL,在编译或重新注册时会遇到麻烦。
- 关闭 Office
- 如有必要,先取消注册
- 重新编译
- 再次注册
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
- 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...
Windows 的进程间通信该怎么选 ── 命名管道 / TCP / gRPC / 共享内存 / COM 判断表
整理 Windows 应用程序之间应该如何选择通信方式。用判断表整理命名管道、本地 TCP、gRPC、共享内存、文件对接、COM 各自的优势与陷阱,并从实务角度说明 UI+服务分离・32bit/64bit 桥接・权限边界等常见架构,以及命名管道的实现示例。
用 Power Automate 实现业务自动化 ── 云端流程与桌面流程的分工,以及错误处理设计
本文整理 Power Automate 云端流程与桌面流程的区别、与 PowerShell / VBA 的分工、许可证、错误处理、UI 自动化的稳定化,以及凭据信息的安全处理,提供把业务自动化落地到生产环境的实务设计指南。
委托开发 Windows 应用程序前该梳理的事项
在委托外包开发 Windows 应用程序之前,梳理现有软件改造、设备联动、COM/ActiveX、发布与更新、维护等需要注意的要点。
Office 2024/Microsoft 365 中 ActiveX 无法运行的原因与排查步骤
梳理在 Office 2024/Microsoft 365 中 ActiveX 无法运行时,按默认禁用、32bit/64bit、COM 注册、依赖 DLL、IE 模式、Click-to-Run 日志的顺序进行排查的方法。
常见问题
汇总了咨询这一主题时常见的问题。
- 要让 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 的运维方式。还要注意,如果之后更改了部署位置,也需要重新执行注册。
作者简介
本文作者的个人简介页面。
Go Komura
小村软件有限公司 代表
以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。