C# 透過 COM Interop 可將 Managed 組件暴露為傳統 COM 物件,讓 C++、VB6、VBA、Delphi 等非受控代碼呼叫。本文完整示範:強名稱簽章、介面定義 (DispId)、類別實作、RegAsm 註冊、類型程式庫 (.tlb) 產生、C++ 智慧指標呼叫等端到端流程。
[C# Class Library]
→ 介面 (Interface) + 類別 (Class) + 屬性 (ComVisible, Guid, ClassInterface, ComSourceInterfaces)
→ 編譯產生 .dll
→ RegAsm /tlb /codebase 註冊並產生 .tlb
→ [C++ Client] #import ".tlb" → 智慧指標呼叫
.snk)。AssemblyInfo.cs 確認 [assembly: ComVisible(true)] (或類別層級控制)。| 規則 | 說明 |
|---|---|
| 介面/類別/成員 | 必須為 public |
| 介面需 Guid | [Guid("...")] 由 guidgen.exe 或 VS 工具產生 |
| 成員需 DispId | [DispId(n)] 明確指定派發識別碼,順序即為 v-table 順序 |
| 類別介面類型 | [ClassInterface(ClassInterfaceType.None)] 禁止自動產生介面,強制實作明確介面 (推薦) |
| 事件介面 | [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)] + [ComSourceInterfaces(typeof(Events))] |
| 參數/回傳型別 | 限 COM 可轉換型別 (基本型別、陣列、字串、DateTime、Decimal、IPicture/IFont 等) |
using System;
using System.Runtime.InteropServices;
using System.Data.SqlClient;
namespace DatabaseComObject
{
// 1. 類別介面 (供 COM 呼叫)
[Guid("694C1820-04B6-4988-928F-FD858B95C880")]
[InterfaceType(ComInterfaceType.InterfaceIsDual)] // Dual: 支援早期/晚期繫結
public interface IDbComInterface
{
[DispId(1)] void Init(string userId, string password);
[DispId(2)] bool ExecuteSelectCommand(string sql);
[DispId(3)] bool NextRow();
[DispId(4)] void ExecuteNonSelectCommand(string sql);
[DispId(5)] string GetColumnData(int index);
}
// 2. 事件介面 (可選)
[Guid("47C976E0-C208-4740-AC42-41212D3C34F0")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IDbComEvents
{
// [DispId(1)] void OnError(string message); // 範例
}
// 3. 實作類別
[Guid("9E5E5FB2-219D-4EE7-AB27-E4DBED8E123E")]
[ClassInterface(ClassInterfaceType.None)] // 關鍵:不自動產生介面
[ComSourceInterfaces(typeof(IDbComEvents))] // 事件來源
[ComVisible(true)]
public class DbComClass : IDbComInterface
{
private SqlConnection conn;
private SqlDataReader reader;
public void Init(string userId, string password)
{
string cs = $"Data Source=.;Initial Catalog=Northwind;User ID={userId};Password={password};Connect Timeout=30";
conn = new SqlConnection(cs);
conn.Open();
// MessageBox 需參考 System.Windows.Forms,純 COM 元件建議改用事件/回呼
}
public bool ExecuteSelectCommand(string sql)
{
reader?.Close();
using (var cmd = new SqlCommand(sql, conn))
{
reader = cmd.ExecuteReader();
}
return true;
}
public bool NextRow() => reader?.Read() ?? false;
public string GetColumnData(int index)
{
object val = reader?.GetValue(index);
return val?.ToString() ?? "";
}
public void ExecuteNonSelectCommand(string sql)
{
using (var cmd = new SqlCommand(sql, conn))
cmd.ExecuteNonQuery();
}
}
}
建置專案即自動執行 RegAsm /codebase 註冊至 HKCR。
# 1. 複製 .dll 至目標資料夾 (如 C:\Program Files\MyCom\)
# 2. 以管理員身分執行開發人員命令提示字元
RegAsm.exe "C:\Program Files\MyCom\DatabaseComObject.dll" /tlb /codebase
# 產生 .tlb 並寫入登錄表
# 3. (選擇性) GAC 安裝強名稱組件
gacutil /i "C:\Program Files\MyCom\DatabaseComObject.dll"
// Stdafx.h 或對應 cpp
#import "C:\Program Files\MyCom\DatabaseComObject.tlb" no_namespace named_guids
// 使用
CoInitialize(NULL); // 單執行緒單元 (STA) 必須
DatabaseComObject::IDbComInterfacePtr dbCom(__uuidof(DatabaseComObject::DbComClass));
dbCom->Init("scott", "tiger");
char cmd[1024];
sprintf_s(cmd, "SELECT CompanyName, ContactName FROM Customers WHERE CustomerID = '%s'", "ALFKI");
dbCom->ExecuteSelectCommand(cmd);
if (dbCom->NextRow())
{
_bstr_t company = dbCom->GetColumnData(0);
_bstr_t contact = dbCom->GetColumnData(1);
// 轉 CString: CString str = (LPCTSTR)company;
}
CoUninitialize();
Dim db As DatabaseComObject.DbComClass
Set db = New DatabaseComObject.DbComClass
db.Init "scott", "tiger"
db.ExecuteSelectCommand "SELECT * FROM Categories"
Do While db.NextRow
Debug.Print db.GetColumnData(0) & " " & db.GetColumnData(1)
Loop
| 現象 | 原因 | 解決 |
|---|---|---|
| Class not registered (0x80040154) | 未註冊或位元數不符 (x86/x64) | 目標平台對應註冊:32-bit 用 RegAsm (Framework), 64-bit 用 RegAsm (Framework64) |
| Type mismatch / Invalid argument | 參數型別非 COM 可轉換 | 檢查陣列維度、DateTime 格式、避免泛型/介面參數 |
| 事件不觸發 | 事件介面未標記 IDispatch / Sink 未建立 | 確認 InterfaceIsIDispatch、ComSourceInterfaces |
| 部署後呼叫失敗 | 強名稱驗證失敗 / 相依組件缺失 | GAC 安裝或 /codebase、確保相依 DLL 同目錄 |