首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# 開發 COM 組件完整指南:介面定義、類別實現、RegAsm 註冊、跨語言呼叫

     編輯:關於C語言

前言

COM 組件讓 C# 代碼可被 VB6、VBA、C++、Python、PowerShell 等非 .NET 環境呼叫。本文涵蓋介面定義屬性、類別實現、註冊部署、版本控管、跨語言呼叫範例。

核心屬性速查

屬性用途必要性
[ComVisible(true)]讓組件/類型對 COM 可見組件層級必須
[Guid("...")]指定穩定 GUID (介面/類別/事件)全部必須,勿自動產生
[InterfaceType(ComInterfaceType.InterfaceIsDual)]支援早期/晚期綁定介面建議
[ClassInterface(ClassInterfaceType.None)]不自動產生類別介面,強制走自定介面類別建議
[DispId(n)]指定成員 DISPID,確保版本穩定介面成員必須
[ProgId("MyLib.MyClass")]易讀程式識別碼,供 CreateObject 使用類別建議

1. 定義 COM 介面


using System;
using System.Runtime.InteropServices;

namespace MyCompany.ComInterop
{
    // 介面 GUID:固定不變,版本間相容關鍵
    [Guid("1EA4DBF0-3C3B-11CF-810C-00AA00389B71")]
    [InterfaceType(ComInterfaceType.InterfaceIsDual)] // 支援 IDispatch (晚期綁定) + vtable (早期綁定)
    [ComVisible(true)]
    public interface IMessageLogger
    {
        [DispId(1)]
        void LogSend(string senderId, string targetId, string fileName);

        [DispId(2)]
        void LogReceive(string receiverId, string senderId, string fileName);

        [DispId(3)]
        void LogSendSuccess(string senderId, string targetId, string fileName);

        [DispId(4)]
        void LogSendCancel(string cancelId, string targetId, string fileName);

        [DispId(5)]
        int GetLogCount(string userId); // 有回傳值

        [DispId(6)]
        string GetLastLog(string userId); // 字串回傳
    }
}

2. 實現 COM 類別


using System;
using System.IO;
using System.Runtime.InteropServices;

namespace MyCompany.ComInterop
{
    // 類別 GUID:固定
    [Guid("A1B2C3D4-E5F6-7890-ABCD-EF1234567890")]
    // 不自動產生 _MessageLogger 介面,強制使用 IMessageLogger
    [ClassInterface(ClassInterfaceType.None)]
    // ProgId:供 VBA/Python CreateObject("MyCompany.MessageLogger") 使用
    [ProgId("MyCompany.MessageLogger")]
    [ComVisible(true)]
    public class MessageLogger : IMessageLogger
    {
        private readonly string _logDirectory;
        private readonly object _lock = new();

        public MessageLogger()
        {
            // 預設記錄至 CommonApplicationData
            _logDirectory = Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
                "MyCompany", "Logs");
            Directory.CreateDirectory(_logDirectory);
        }

        // 允許自訂路徑 (給進階用戶)
        public MessageLogger(string customLogPath) : this()
        {
            if (Directory.Exists(customLogPath))
                _logDirectory = customLogPath;
        }

        private void WriteLog(string fileName, string content)
        {
            string path = Path.Combine(_logDirectory, fileName);
            lock (_lock)
            {
                File.AppendAllText(path, $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} {content}{Environment.NewLine}");
            }
        }

        public void LogSend(string senderId, string targetId, string fileName)
        {
            WriteLog("Send.log", $"{senderId} -> {targetId} : {fileName}");
        }

        public void LogReceive(string receiverId, string senderId, string fileName)
        {
            WriteLog("Receive.log", $"{receiverId} <- {senderId} : {fileName}");
        }

        public void LogSendSuccess(string senderId, string targetId, string fileName)
        {
            WriteLog("Success.log", $"{senderId} => {targetId} : {fileName} [OK]");
        }

        public void LogSendCancel(string cancelId, string targetId, string fileName)
        {
            WriteLog("Cancel.log", $"{cancelId} -X- {targetId} : {fileName} [CANCEL]");
        }

        public int GetLogCount(string userId)
        {
            string path = Path.Combine(_logDirectory, "Send.log");
            if (!File.Exists(path)) return 0;
            return File.ReadLines(path).Count(line => line.Contains(userId));
        }

        public string GetLastLog(string userId)
        {
            string path = Path.Combine(_logDirectory, "Send.log");
            if (!File.Exists(path)) return string.Empty;
            
            foreach (string line in File.ReadLines(path).Reverse())
            {
                if (line.Contains(userId)) return line;
            }
            return string.Empty;
        }
    }
}

3. 專案設定與建置

AssemblyInfo.cs 關鍵設定


using System.Runtime.InteropServices;

[assembly: ComVisible(true)] // 整個組件對 COM 可見
[assembly: Guid("ORGANIZATION-GUID-FOR-TYPELIB")] // TypeLib GUID

.csproj 關鍵屬性



  net48 
  true 
  true




  true

4. 註冊與部署

開發機:Visual Studio 自動註冊

專案屬性 → Build → 勾選 "Register for COM interop" (需管理員權限)

生產環境:RegAsm 手動註冊


# 註冊並產生 .tlb
RegAsm.exe MyCompany.ComInterop.dll /tlb:MyCompany.ComInterop.tlb /codebase

# 僅註冊 (不產生 tlb)
RegAsm.exe MyCompany.ComInterop.dll /codebase

# 解除註冊
RegAsm.exe MyCompany.ComInterop.dll /unregister

# 64 位系統註冊到 32 位 (給 32 位呼叫者如 VBA)
C:\Windows\Microsoft.NET\Framework\v4.0.30319\RegAsm.exe MyCompany.ComInterop.dll /tlb /codebase
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\RegAsm.exe MyCompany.ComInterop.dll /tlb /codebase

安裝程式整合 (WiX/InstallShield)




  
    
      
        
      
    
  

5. 跨語言呼叫範例

VBA (Excel/Access/Word)


Sub TestComLogger()
    Dim logger As Object
    Set logger = CreateObject("MyCompany.MessageLogger")
    
    logger.LogSend "USER001", "USER002", "Report.pdf"
    logger.LogReceive "USER002", "USER001", "Report.pdf"
    logger.LogSendSuccess "USER001", "USER002", "Report.pdf"
    
    Dim count As Long
    count = logger.GetLogCount("USER001")
    MsgBox "USER001 發送記錄數: " & count
    
    Dim lastLog As String
    lastLog = logger.GetLastLog("USER001")
    MsgBox "最後記錄: " & lastLog
End Sub

Python (win32com.client)


import win32com.client

logger = win32com.client.Dispatch("MyCompany.MessageLogger")
logger.LogSend("PYTHON01", "PYTHON02", "data.csv")
count = logger.GetLogCount("PYTHON01")
print(f"Log count: {count}")

C++ (早期綁定 #import)


#import "MyCompany.ComInterop.tlb" raw_interfaces_only, named_guids
using namespace MyCompany_ComInterop;

int main()
{
    CoInitialize(NULL);
    {
        IMessageLoggerPtr logger(__uuidof(MessageLogger));
        logger->LogSend(_bstr_t("CPP01"), _bstr_t("CPP02"), _bstr_t("file.txt"));
        long count = logger->GetLogCount(_bstr_t("CPP01"));
        printf("Count: %d\n", count);
    }
    CoUninitialize();
    return 0;
}

PowerShell


$logger = New-Object -ComObject MyCompany.MessageLogger
$logger.LogSend("PS01", "PS02", "script.ps1")
$count = $logger.GetLogCount("PS01")
Write-Host "Count: $count"

C# (晚期綁定 / 早期綁定)


// 晚期綁定 (無需參考組件)
dynamic logger = Activator.CreateInstance(Type.GetTypeFromProgID("MyCompany.MessageLogger"));
logger.LogSend("CS01", "CS02", "test.txt");

// 早期綁定 (專案參考組件)
using MyCompany.ComInterop;
IMessageLogger logger = new MessageLogger();
logger.LogSend("CS01", "CS02", "test.txt");

6. 版本控管與相容性

  • 介面不可變:已發布介面絕不修改,新功能新增介面 IMessageLogger2
  • DispId 固定:手動指定,防止重新編譯導致順序變更
  • 新增方法加新介面:類別實現多介面,舊呼叫者不受影響
  • TypeLib 版本:[assembly: TypeLibVersion(1, 0)],重大變更升主版本
  • 註冊免註冊 (Reg-Free COM):應用程式 manifest 內嵌 COM 資訊,免系統註冊,支援並存部署

7. 疑難排解

錯誤原因解決
Class not registered (0x80040154)未註冊、位數不符 (32/64位)雙位數註冊、檢查呼叫者位數
Type mismatch參數型別不符 (如傳 Integer 給 String)VBA 用 CStr/CLng 轉型、Python 確保 str/int
Access denied寫入受保護目錄 (Program Files)改寫 CommonApplicationData、或提權
Catastrophic failure (0x8000FFFF)COM 物件內部例外未處理所有 public 方法包 try-catch、回傳 HRESULT

總結

COM 互操作核心在於:穩定 GUID、明確 DispId、雙介面支援早晚綁定、正確註冊部署。現代 .NET 專案若需跨語言,優先考慮 gRPC、REST API、Named Pipes、MessagePack 等技術;維護舊系統整合時,本文模式可確保穩定相容。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved