跨語言互操作是企業級系統集成的常見需求。PHP 通過 COM 組件模型可調用 .NET Framework 構建的 DLL。本文提供從零開始的完整流程:C# 類庫創建、COM 可見性配置、強名稱簽名、regasm 註冊、gacutil 全局程序集緩存部署,以及 PHP COM 類調用與異常處理。
com_dotnet 擴展(php.ini: extension=com_dotnet)# 使用 .NET CLI
dotnet new classlib -n HelloWorld -f net48
cd HelloWorld
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<AssemblyName>HelloWorld</AssemblyName>
<RootNamespace>HelloWorld</RootNamespace>
<!-- 關鍵:啟用 COM 可見性 -->
<ComVisible>true</ComVisible>
<!-- 生成類型庫 -->
<RegisterForComInterop>true</RegisterForComInterop>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>HelloWorld.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>
</Project>
// Hello.cs
using System;
using System.Runtime.InteropServices;
namespace HelloWorld
{
// 介面定義(可選,但推薦:版本控制、類型安全)
[ComVisible(true)]
[Guid("D1A2B3C4-5E6F-7890-ABCD-EF1234567890")]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IHello
{
string Write();
string Greet(string name);
int Add(int a, int b);
DateTime GetServerTime();
}
// 類實現
[ComVisible(true)]
[Guid("E2B3C4D5-6F78-90AB-CDEF-123456789012")]
[ClassInterface(ClassInterfaceType.None)] // 只暴露介面
[ProgId("HelloWorld.Hello")] // COM ProgID,PHP 調用用此字符串
public class Hello : IHello
{
public string Write()
{
return "Hello World from C# DLL!";
}
public string Greet(string name)
{
return $"Hello, {name}! Current time: {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
}
public int Add(int a, int b)
{
return a + b;
}
public DateTime GetServerTime()
{
return DateTime.Now;
}
}
}
# 生成強名稱金鑰文件
sn -k HelloWorld.snk
# 或使用 Visual Studio:項目屬性 → 簽名 → 勾選「為程序集簽名」 → 選擇強名稱金鑰文件
dotnet build -c Release
# 輸出:bin/Release/net48/HelloWorld.dll
# 以管理員權限運行開發人員命令提示符
cd bin/Release/net48
# 註冊並生成類型庫
regasm HelloWorld.dll /tlb:HelloWorld.tlb /codebase
# 成功提示:
# Types registered successfully
# Assembly exported to '...\HelloWorld.tlb', and the type library was registered successfully
/tlb:生成 .tlb 類型庫文件,供早期綁定使用/codebase:在註冊表中記錄 DLL 絕對路徑,**非 GAC 部署時必須加**# 僅強名稱程序集可安裝 GAC
gacutil /i HelloWorld.dll
# 成功提示:
# Assembly successfully added to the cache
<?php
// com_client.php
try {
// 實例化 COM 對象(使用 ProgID)
$com = new COM("HelloWorld.Hello");
// 或者使用 CLSID(更穩定,不依賴註冊表 ProgID 映射)
// $com = new COM("{E2B3C4D5-6F78-90AB-CDEF-123456789012}");
// 調用方法
echo "Write(): " . $com->Write() . PHP_EOL;
echo "Greet('PHP'): " . $com->Greet('PHP') . PHP_EOL;
echo "Add(10, 20): " . $com->Add(10, 20) . PHP_EOL;
// 日期時間類型自動轉換為 PHP DateTime 或字符串
$time = $com->GetServerTime();
echo "GetServerTime(): " . $time->format('Y-m-d H:i:s') . PHP_EOL;
// 釋放 COM 對象(重要:避免內存洩漏)
$com = null;
unset($com);
} catch (com_exception $e) {
echo "COM Error: {$e->getMessage()}" . PHP_EOL;
echo "Code: {$e->getCode()}" . PHP_EOL;
} catch (Throwable $e) {
echo "General Error: {$e->getMessage()}" . PHP_EOL;
}
// C# 端
public string RiskyMethod()
{
throw new ArgumentException("參數無效", nameof(param));
}
// PHP 端自動捕獲為 com_exception
// $e->getMessage() 包含 "參數無效"
// $e->getCode() 為 HRESULT (0x80070057 = E_INVALIDARG)
DateTime 對象 |
| bool | bool | |
| Array/List | variant/SAFEARRAY | 需 foreach 遍歷 |
| 物件 | COM 物件 | 可鏈式調用 |
Class 'COM' not found | PHP 未啟用 com_dotnet | php.ini 啟用 extension=com_dotnet,重啟 PHP |
| Failed to create COM object | DLL 未註冊/註冊失敗 | 管理員運行 regasm /codebase,檢查註冊表 HKCR\HelloWorld.Hello |
| Access denied | DCOM 權限不足 | dcomcnfg → 組件服務 → 計算機 → 我的電腦 → 屬性 → COM 安全性 → 啟動和激活權限 → 編輯限制 → 添加 IIS_IUSRS/PHP 用戶 → 允許本地啟動/激活 |
| 參數類型不匹配 | PHP 弱類型傳遞 | 顯式轉換:(int)$val, (string)$val |
| 內存洩漏 | 未釋放 COM 對象 | 必須 $com = null; unset($com); 或使用 com_release() |
com.allow_dcom = true (php.ini)com_exception、記錄日誌、返回友好錯誤PHP 調用 C# DLL 是 Windows 平台特有的緊耦合集成方案,適合「既有 .NET 庫、無法重寫、性能要求中等、部署環境可控」的場景。關鍵成功因素:強名稱簽名、GAC 部署、DCOM 權限、COM 對象釋放。現代架構建議優先考慮 HTTP API 或 gRPC 微服務化,實現跨平台、松耦合、可獨立演進。