更新紀錄(2 筆,最後更新 2026年09月04日)
本文的修改紀錄。已保存的更新前版本,可透過附有 DOI 的永久連結閱讀。
引用本文(DOI: 10.5281/zenodo.21616304)
本文保存於 Zenodo。以下同時提供一律指向最新版本的 DOI,以及固定於您正在閱讀版本的 DOI。
Go Komura(2026)。〈從 VBA 帶型別呼叫 .NET 8 DLL 的方法 - COM 公開與 dscom TLB〉。小村軟體有限公司。https://doi.org/10.5281/zenodo.21616304 https://comcomponent.com/zh-TW/blog/2026/03/16/007-dotnet8-dll-typed-vba-com-dscom-tlb/
- DOI(最新版本)
- 10.5281/zenodo.21616304
- DOI(此版本)
- 10.5281/zenodo.22297142
想從 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["typo 到執行階段才發現"]
圖 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)
需要的環境
| 項目 | 需要什麼 |
|---|---|
| OS | Windows。因為要做 COM 註冊,必須能以系統管理員權限執行 regsvr32 |
| .NET SDK | .NET 8 SDK。EnableComHosting 是 .NET 5 以後才有的功能 |
| Office | Excel 或 Access。先查看是 32 位元版還是 64 位元版(第 3 章) |
| TLB 產生工具 | dscom。64 位元用與 32 位元用的取得方式不同(第 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. 一開始就要決定的事 - 對齊 32 位元 / 64 位元
這裡弄錯,很高的機率會滾向 ActiveX 元件無法建立物件 那個方向。
請把 Office / VBA 與 COM 伺服器的位元數(bitness)對齊。
| 使用端 | .NET 端的判斷基準 | TLB 產生 | 註冊指令 |
|---|---|---|---|
| 64 位元 Office | x64 / win-x64 |
dscom |
C:\Windows\System32\regsvr32.exe |
| 32 位元 Office(在 64 位元 Windows 上) | x86 / win-x86 |
dscom32.exe |
C:\Windows\SysWOW64\regsvr32.exe |
.NET 5+ 之後的 COM host,如果放著 AnyCPU 不管,*.comhost.dll 容易偏到 64 位元那一側,有時候就跟 32 位元 Office 對不上。所以依照 Office 明確指定 x86 / x64 會比較安全。
flowchart TB
accTitle: 放著 AnyCPU 不管招來的不一致
accDescr: 說明放著 AnyCPU 不管時 comhost 容易偏到 64 位元那一側而與 32 位元 Office 對不上,因此依照 Office 明確指定 x86 或 x64 比較安全的圖。
b1["放著 AnyCPU 不管"] --> b2["comhost 容易偏到 64 位元那一側"]
b2 --> b3["與 32 位元 Office 對不上"]
b3 -.-> b4["依照 Office 明確指定位元數"]
圖 4: 位元數的落差,是通往「無法建立物件」的捷徑。
本文的程式碼以 64 位元 Office 為例。如果是 32 位元 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 64 位元的情況
只是要做 64 位元的 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 給 32 位元 Office 的情況 - dscom32.exe 要從哪裡取得
這裡是支援 32 位元 Office 時最容易卡住的地方。
用 dotnet tool install 裝到的 dscom,只能處理 AnyCPU 或 64 位元的組件,能產生的也只有 64 位元的 TLB。 要做 32 位元的 TLB,需要另一個執行檔 dscom32.exe,而且它不在 NuGet 上,要從 GitHub 的發行頁面下載。
- 取得處: https://github.com/dspace-group/dscom/releases
dscom.exe… 從 AnyCPU 或 64 位元組件做出 64 位元的 TLBdscom32.exe… 從 AnyCPU 或 32 位元組件做出 32 位元的 TLB
下載回來的 dscom32.exe,在本文的例子裡放在專案根目錄下的 tools 資料夾。放哪裡都可以,但請不要跟建置輸出一起散發。它是開發時用的工具,執行階段並不需要。
還有一個容易漏掉的前提。要跑 dscom32.exe,機器上必須裝有 x86 版的 .NET 執行階段。 這是因為 dscom 需要載入 hostfxr.dll,只裝了 x64 版的環境跑不起來。請在 dotnet --info 輸出的清單裡,查看有沒有 x86 的執行階段。
flowchart TB
accTitle: 產生 32 位元 TLB 的準備
accDescr: 說明用 dotnet tool 裝到的 dscom 只能做出 64 位元的 TLB,因此 32 位元的 TLB 要用從 GitHub 發行頁面取得的 dscom32.exe 產生,而且要跑它必須裝有 x86 版的 .NET 執行階段的圖。
p1["需要 32 位元的 TLB"] --> p2["從發行頁面取得 dscom32.exe"]
p2 --> p3["查看有沒有 x86 版執行階段"]
p3 --> p4["用 dscom32.exe 執行 tlbexport"]
p1 -.-> p5["dotnet tool 版只做得出 64 位元 TLB"]
圖 7: 32 位元的支援連工具的取得管道都不一樣,所以這裡特別容易卡住。
.\tools\dscom32.exe tlbexport .\bin\Release\net8.0-windows\VbaTypedComSample.dll --out .\bin\Release\net8.0-windows\VbaTypedComSample.tlb
另外,dscom 自己的文件也建議:放著 AnyCPU 不管的話 *.comhost.dll 會以 64 位元產生,所以要在 32 位元下使用,組件本身就要以 32 位元編譯。這和第 3 章的結論一樣。
如果嫌每次建置都要手動敲一次麻煩,裝上 dSPACE.Runtime.InteropServices.BuildTasks 套件,就能在編譯時自動產生 TLB。
7. 註冊 COM host 與 TLB
這一段請在系統管理員權限的命令提示字元 / PowerShell 下執行。
7.1 64 位元 Office / 64 位元 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 32 位元 Office(在 64 位元 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註冊成型別程式庫
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 章的位元數不一致,或第 7 章的 tlbregister 沒跑成功這兩者之一。從 32 位元 Office 看不到以 64 位元註冊的 TLB。
flowchart TB
accTitle: 引用項目清單裡沒出現時怎麼縮小範圍
accDescr: 說明設定引用項目的清單裡沒出現程式庫時,原因大多是位元數不一致或 tlbregister 沒跑成功,而且從 32 位元 Office 看不到以 64 位元註冊的 TLB 的圖。
q1["清單裡沒有出現程式庫"] --> a1["先懷疑第 3 章的位元數不一致"]
q1 --> a2["再懷疑第 7 章的 tlbregister 失敗"]
a1 -.-> nt1["從 32 位元 Office 看不到 64 位元的 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 註冊、執行階段啟動、引數與傳回值的封送處理(marshalling)整條路都通了。反過來說,如果值對不上就懷疑 .NET 端的實作,如果根本跑不起來就懷疑第 3 章與第 7 章。
flowchart TB
accTitle: 用執行結果縮小範圍
accDescr: 說明範例的執行結果如果符合預期,就代表從引用項目到封送處理整條路都通了,值對不上要懷疑 .NET 端的實作,根本跑不起來則要懷疑第 3 章的位元數與第 7 章的註冊的圖。
r1["執行 VBA 的範例"] --> q1{"結果如何"}
q1 -->|"符合預期"| ok1["整條路都通了"]
q1 -->|"值對不上"| ng1["懷疑 .NET 端的實作"]
q1 -->|"跑不起來"| ng2["懷疑位元數與註冊"]
圖 10: 光靠這 3 行輸出,就能決定該懷疑哪一層。
這樣一來,VBA 這邊有這些好處。
- IntelliSense 會生效
- 方法名稱的 typo 比較容易在執行前發現
- 可以用 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 會放入帶正負號的 Long 形式的 HRESULT,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)
而且,用戶端電腦上還需要對應的 .NET 8 執行階段。COM host 不是 self-contained 的散發方式,基本上是 framework-dependent 的維運。
要裝的東西具體來說如下。
- 下載頁面是 .NET 8 的下載頁
- 需要的不是 SDK,而是執行階段。本文的範例是沒有畫面的類別庫,所以
.NET Runtime就夠了。如果要用到 WPF 或 Windows Forms 的型別,就需要.NET Desktop Runtime - 位元數要跟 Office 對齊。 64 位元 Office 就是 x64、32 位元 Office 就是 x86 的執行階段。和第 3 章明確指定
x64/x86是同樣的理由,這裡對不上一樣起不來 - 要知道裝好了沒有,可以在用戶端電腦執行
dotnet --list-runtimes,看有沒有Microsoft.NETCore.App 8.x這一行
散發說明文件裡,一定要寫上這個「需要的執行階段種類、版本、位元數」。安裝步驟裡漏寫的話,到了現場看到 ActiveX 元件無法建立物件 才回頭懷疑位元數,時間就這樣燒掉了。
flowchart TB
accTitle: 散發時要湊齊的東西
accDescr: 說明散發時不是只放一個 DLL 而是放上輸出整套,用戶端電腦要裝與 Office 相同位元數的 .NET 8 執行階段,並且要在散發說明文件裡寫明需要的執行階段種類、版本與位元數的圖。
h1["放上輸出整套"] --> u1["在用戶端能跑起來"]
h2["相同位元數的 .NET 8 執行階段"] --> u1
h3["散發說明文件寫明執行階段資訊"] -.-> u1
圖 12: 只有 DLL 動不了,一整套加上執行階段湊齊才會動。
10. 常見陷阱
10.1 不要放著 AnyCPU 不管
VBA / Office 的位元數和 COM host 的位元數一錯開,失敗的方式會相當難受。
- 64 位元 Office 就用
x64/win-x64 - 32 位元 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
- 需要的話先取消註冊
- 重新建置
- 再註冊一次
取消註冊要用與註冊時相反的順序,並且用與註冊時相同位元數的指令。同樣需要系統管理員權限,這點和註冊時一樣。
# 64 位元 Office / 64 位元 COM 的情況
$out = Resolve-Path .\bin\Release\net8.0-windows
dscom tlbunregister "$out\VbaTypedComSample.tlb"
C:\Windows\System32\regsvr32.exe /u "$out\VbaTypedComSample.comhost.dll"
# 32 位元 Office(在 64 位元 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 就解不掉(以 64 位元註冊的東西,沒辦法用 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 本體。子命令清單與 32 位元支援的說明)
- 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 伺服器的位元數不一致。64 位元 Office 就以 x64/win-x64 建置並用 System32 的 regsvr32 註冊;32 位元 Office(在 64 位元 Windows 上)則以 x86/win-x86 建置並用 SysWOW64 的 regsvr32 註冊,產生 TLB 也要改用 dscom32.exe。.NET 5+ 的 COM host 如果放著 AnyCPU 不管,*.comhost.dll 容易偏到 64 位元那一側,有時候就跟 32 位元 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 的維運。還要注意,之後如果改變擺放位置,註冊也要重做。