從 VBA 帶型別使用 .NET 8 的 DLL - 以 COM 發布 + dscom 產生 TLB

· · C#, .NET 8, VBA, COM, Office, dscom

想從 VBA 呼叫 .NET 8 的處理,情境其實還相當常見。尤其是想把 Excel 或 Access 的既有資產保留下來,只把重計算、字串處理、HTTP、加密、業務邏輯等搬到 C# 時。

但若用 CreateObject 往 late binding 那邊靠,VBA 裡就會滿是 Object:IntelliSense 會變弱、方法名的拼錯要等執行時才發現,整個開發就慢慢陷進靠字串硬湊的泥濘。

所以這次聚焦在:把 .NET 8 的 DLL 以 COM 發布,用 dscom 產出 type library(TLB),再從 VBA 用 early binding 帶型別使用

.NET Framework + RegAsm 的舊時代、手寫 IDL 再用 MIDL 打包、Reg-Free COM 的議題,這次都先擺一邊。本文只走 .NET 8 / COM host / dscom / VBA early binding 這一條直線。

1. 先下結論

先把流程整段列出:

  • EnableComHosting=true 編譯 .NET 8 類別庫
  • 為 COM 公開 明確的介面類別
  • 類別用 ClassInterfaceType.None,不要靠 AutoDual
  • 給 VBA 的介面用 InterfaceIsDual
  • 編譯後從 *.dlldscom tlbexport 產出 *.tlb
  • regsvr32 註冊 *.comhost.dll
  • dscom tlbregister 註冊 *.tlb
  • VBA 加上「參考設定」,再用 Dim x As 函式庫名.IYourInterface 帶型別使用

一句話:COM 的入口是 .NET SDK 產出的 *.comhost.dll,型別資訊由 dscom 產生為 *.tlb,VBA 看 TLB 做 early binding

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 要知道型別,需要的是 TLBCOM 的啟動入口則是 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 讀成 x86win-x64 讀成 win-x86

4. .NET 8 端實作

以 VBA 可呼叫 AddDivideHello 的最小示範為例。

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不靠自動產生的 class interface
  • 為了讓 VBA 好用,介面用 InterfaceIsDual
  • 加上 DispId,之後改方法順序時比較不會出事
  • COM 會呼叫 New,所以要有 public 且無參數的建構子

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

這一步請用 系統管理員權限的 CMD/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"

這一步做了兩件事:

  • regsvr32*.comhost.dll 註冊為 COM 伺服器
  • tlbregister*.tlb 註冊為 type library

8. 在 VBA 做參考設定並帶型別使用

  1. 打開 Excel 或 Access
  2. 打開 VBA 編輯器
  3. 工具 -> 參考設定
  4. 若清單裡看到函式庫,勾選
  5. 若看不到,透過 瀏覽... 選取 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 可用
  • 方法名 typo 可在執行前被發現
  • Object Browser 能檢視公開 API
  • 比硬寫 Object 更可讀

8.1 例外在 VBA 端會是 COM 錯誤

若 .NET 端丟例外(例如 Divide(10, 0)),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)

另外,用戶端要 對應版本的 .NET 8 執行時期。COM host 不走 self-contained,基本上是 framework-dependent 的運維。

10. 常見坑

10.1 不要維持 AnyCPU

VBA/Office 與 COM host 的 bitness 對不上,失敗方式會相當奇怪。

  • 64bit Office 用 x64 / win-x64
  • 32bit Office 用 x86 / win-x86

10.2 不要用 ClassInterfaceType.AutoDual

看起來很方便,但公開後動到成員順序或結構就容易弄壞。

要讓 VBA 穩定使用帶型別的 API,標準做法仍是:明確定義介面,類別用 ClassInterfaceType.None

10.3 不要隨便重新產 GUID

在 COM 裡,GUID 就是契約:

  • IID
  • CLSID

公開之後隨便換,既有 VBA 參考與註冊就會壞。

10.4 不要破壞公開介面

COM 世界裡「只是加一個方法」有時候也會出事:

  • 保留 ICalculator
  • 變動大時新增 ICalculator2
  • 類別同時實作兩者也可以

10.5 型別選偏「樸素」一點

VBA 看得到的邊界,型別不要太花。

比較好相處的是:

  • int
  • double
  • bool
  • string
  • DateTime
  • decimal
  • enum

10.6 Office 開著別做更新

Excel 或 Access 會把 DLL 抓住,造成編譯或重新註冊困難。

  • 關掉 Office
  • 需要時先解除註冊
  • 重建
  • 再註冊一次

11. 總結

把「.NET 8 DLL 在 VBA 裡帶型別使用」這件事收斂成 COM 發布 + dscom 產 TLB,要做的事其實有條理:

  • .NET 8 端用 EnableComHosting=true
  • 為 COM 設計 明確介面
  • 類別用 ClassInterfaceType.None
  • 給 VBA 的介面用 InterfaceIsDual
  • dscom tlbexport 產生 TLB
  • regsvr32 註冊 *.comhost.dll
  • dscom tlbregister 註冊 *.tlb
  • VBA 做參考設定後以 early binding 使用

簡單來說,.NET 8 時代的 VBA 整合,要把 COM host 與 TLB 分開想

  • 啟動入口是 *.comhost.dll
  • 型別資訊是 *.tlb
  • 實作本體是 *.dll

12. 參考資料

相關文章

共用相同標籤的最新文章。能以相近的主題延伸理解。

相關主題

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

與本主題相關的服務

本文連結到以下服務頁面,歡迎從最接近的入口查看。

回到部落格一覽