平台叫用服務

平台调用服务

平台叫用服務(英語:Platform Invocation Services),或稱P/Invoke,是微軟的公共語言基礎設施實現的一個特性,類似於微軟公共語言執行時提供的跨平台呼叫方式,允許受控代碼呼叫原生代碼

受控代碼(例如C#VB.NETC++/CLI)提供對.NET Framework的組成庫中定義的類、方法和類型的本機訪問。雖然.NET Framework提供了一組廣泛的功能,但它可能無法訪問通常以非受控代碼編寫的許多較低階別的作業系統庫或也以非受控代碼編寫的第三方庫。P/Invoke是程式設計師可以用來訪問這些庫中的函數的技術。通過在受控代碼中聲明非寄存函數的簽章來呼叫這些庫中的函數,該簽章充當可以像任何其他寄存方法一樣呼叫的實際函數。該聲明參照庫的檔案路徑,並定義寄存類型中的函數參數和返回值,這些寄存類型最有可能由公共語言執行時(CLR)與非寄存類型隱式封送。當非寄存資料類型對於從寄存類型到寄存類型的簡單隱式轉換來說變得過於複雜時,框架允許用戶在函數、返回和/或參數上定義屬性,以顯式細化數據的編組方式,以免試圖隱式地這樣做會導致異常。

與使用非寄存語言進行編程相比,受控代碼程式設計師可以使用許多低階編程概念的抽象。因此,僅具有受控代碼經驗的程式設計師需要溫習編程概念,例如指標、結構和參照傳遞,以克服使用P/Invoke時的一些障礙。

平台叫用服務這一特性與微軟的公共語言執行時提供的較為類似,因此一般提到P/Invoke多數指微軟的.NET實現方案。這一方案能夠實現通過受控代碼訪問原生代碼。使用P/Invoke可以通過CLR來控制DLL的載入,以及將非受控代碼的資料類型轉換為寄存資料類型

Windows

編輯

在Microsoft Windows作業系統中,Native API有時也是以COM介面方式來推出,像是ADSI,FSRM(File Server Resource Manager)等,通常是新的服務或是介面才會廣泛使用COM原生介面方式。因為.NET Framework的推行,Windows的應用程式介面被分為兩種,一種是遵循原本Windows API方式的,稱為Native API,另一種則是以.NET Framework為基礎開發的,稱為Managed API,例如Managed DirectX或是IIS Admin APIs等。

在Microsoft Windows作業系統中,若是透過VB或是.NET Framework存取直接開放C函數的Native API時,則必須要利用平台叫用服務方式存取;若是存取以COM方式開放的Native API時,若該API支援COM Automation規格時,即可利用COM Interop Services來存取。

體系架構

編輯

概述

編輯

有兩類P/Invoke:

顯式

編輯
  • 原生代碼通過動態連結庫 (DLL)匯入
  • 嵌入到呼叫者的程式集(assembly)的元數據英語Metadata (CLI),用來定義如何呼叫原生代碼、如何訪問數據(通常需要屬性源說明符來幫助編譯器生成整集(marshal)代碼)
    • 這種定義就是「顯式」部分。

隱式

編輯
  • 使用C++/CLI程式語言的程式可以同時使用受管堆(以追蹤指標)和任何原生主記憶體區域,不必顯式聲明。(即「隱式」)
  • 隱式方法最主要好處是如果原生數據結構改變了,只要保持命名兼任,就能保持向下相容
    • 即,只要結構成員名稱沒有改變,就可以透明地支援在原生代碼標頭檔中添加/刪除/重新排序結構。

細節

編輯

使用P/Invoke 時,CLR處理DLL載入以及將非寄存以前的類型轉換為CTS] 類型(也稱為「參數編組」)。[1]要執行此操作,CLR

  • 定位包含該函數的DLL
  • DLL載入到主記憶體中。
  • 定位主記憶體中函數的地址,並將其參數壓入堆疊,根據需要封送(marshaling)數據。

P/Invoke對於使用標準(非寄存)CC++ DLL非常有用。當程式設計師需要訪問廣泛的Windows API時,可以使用它,因為Windows系統函式庫提供的許多功能缺乏可用的包裝器。當Win32 API未由.NET Framework公開時,必須手動編寫此API的包裝器。

缺點

編輯

編寫P/Invoke包裝器可能很困難並且容易出錯。使用本機DLL意味着程式設計師無法再像.NET環境中通常提供的那樣受益於類型安全垃圾回收。 當它們使用不當時,可能會導致諸如記憶體區段錯誤記憶體流失等問題。取得在.NET環境中使用的原生函數的準確簽章可能很困難。

其他陷阱包括:

  • 寄存語言中用戶定義的類型數據對齊不正確:根據C語言中的編譯器或編譯器指令,數據對齊的方式有不同,必須小心地顯式告訴CLR如何對齊非blittable類型的數據。 一個常見的例子是嘗試在.NET中定義資料類型以表示C語言中的union,即多個不同的變數在主記憶體中重疊。而在.NET中的類型中定義這兩個變數會導致它們位於主記憶體中的不同位置,因此必須使用特殊屬性來糾正此問題。
  • 寄存語言的垃圾收集器對數據位置的干擾:如果參照是.NET中方法的本地參照並傳遞給原生函數,則當寄存方法返回時,垃圾收集器可能會回收該參照。 需要注意的是,對象參照是pinned,防止它被垃圾收集器收集或移動,從而導致本機模組的無效訪問。

當使用C++/CLI時,發出的CIL可以自由地與寄存堆上的對象互動,同時與任何可定址的本機主記憶體位置互動。可以使用簡單的「object->field」表示法來分配值或指定方法呼叫,從而呼叫、修改或構造寄存堆駐留對象。消除了任何不必要的上下文交換,降低了主記憶體需求(更短的堆疊),從而顯著提高了效能。

C++/CLI帶來了新的挑戰:

  • 代碼容易出現雙重雷電(Double Thunking)[2]如果沒有特別說明
  • 「載入程式鎖定問題」(Loader Lock issue)[3]

如果遇到這些問題,這些參考資料會為每個問題指定解決方案。一個主要的好處是消除了結構聲明,欄位聲明的順序和對齊問題在C++互操作的上下文中不存在。

例子

編輯

基本例子

編輯

第一個簡單範例顯示了如何取得特定DLL的版本:

Windows API中的「DllGetVersion」函數簽章:

HRESULT DllGetVersion
(
    DLLVERSIONINFO* pdvi
)

P/Invoke C#代碼以呼叫「DellGetVersion」函數:

[StructLayout(LayoutKind.Sequential)]
private struct DLLVERSIONINFO {
    public int cbSize;
    public int dwMajorVersion;
    public int dwMinorVersion;
    public int dwBuildNumber;
    public int dwPlatformID;
}
[DllImport("shell32.dll")]
static extern int DllGetVersion(ref DLLVERSIONINFO pdvi);

第二個範例顯示了如何提取檔案中的圖示:

Windows API中的「ExtractIcon」函數簽章:

HICON ExtractIcon
(      
    HINSTANCE hInst,
    LPCTSTR lpszExeFileName,
    UINT nIconIndex
);

P/Invoke C#代碼以呼叫「ExtractIcon」函數:

[DllImport("shell32.dll")]
static extern IntPtr ExtractIcon(
    IntPtr hInst, 
    [MarshalAs(UnmanagedType.LPStr)] string lpszExeFileName, 
    uint nIconIndex);

下一個複雜的範例顯示了如何在Windows平台中的兩個行程之間共用事件

「CreateEvent」函數簽章:

 HANDLE CreateEvent(
     LPSECURITY_ATTRIBUTES lpEventAttributes,
     BOOL bManualReset,
     BOOL bInitialState,
     LPCTSTR lpName
 );

P/Invoke C#代碼以呼叫「CreateEvent」函數:

[DllImport("kernel32.dll", SetLastError=true)]
static extern IntPtr CreateEvent(
    IntPtr lpEventAttributes, 
    bool bManualReset,
    bool bInitialState, 
    [MarshalAs(UnmanagedType.LPStr)] string lpName);

一個更複雜的例子

編輯
// native declaration
typedef struct _PAIR 
{ 
	DWORD Val1; 
	DWORD Val2; 
} PAIR, *PPAIR;
// Compiled with /clr; use of #pragma managed/unmanaged can lead to double thunking;
// avoid by using a stand-alone .cpp with .h includes.
// This would be located in a .h file.

template<>
inline CLR_PAIR^ marshal_as<CLR_PAIR^, PAIR> (const PAIR&Src) {    // Note use of de/referencing. It must match your use.
	CLR_PAIR^ Dest = gcnew CLR_PAIR;
	Dest->Val1 = Src.Val1;
	Dest->Val2 = Src.Val2;
	return Dest;
};
CLR_PAIR^ mgd_pair1;
CLR_PAIR^ mgd_pair2;
PAIR native0,*native1=&native0;

native0 = NativeCallGetRefToMemory();

// Using marshal_as. It makes sense for large or frequently used types.
mgd_pair1 = marshal_as<CLR_PAIR^>(*native1);

// Direct field use
mgd_pair2->Val1 = native0.Val1;
mgd_pair2->val2 = native0.val2;

return(mgd_pair1); // Return to C#

工具

編輯

有許多工具旨在幫助生成P/Invoke簽章。

編寫一個實用程式來匯入C++標頭檔和本機DLL檔案並自動生成介面程式集是相當困難的。為P/Invoke簽章生成這樣一個匯入器/匯出器的主要問題是某些C++函數呼叫參數類型的模糊性。

Brad Abrams在這個問題上這樣說:[4]


問題在於C++函數,如下所示:

__declspec(dllexport) void MyFunction(char *params);


P/Invoke簽章中的參數params應該使用什麼類型?這可以是以C++null結尾的字串,也可以是char陣列或輸出char參數。那麼我們應該使用string,StringBuilder, char []還是ref char呢?

不管這個問題如何,有一些工具可以使P/Invoke簽章的生成更加簡單。

下面列出的工具之一xInterop C++.NET Bridge通過在.NET世界中實現同一C++方法的多個重寫解決了這個問題,開發人員可以選擇正確的方法進行呼叫。

PInvoke.net

編輯

PInvoke.net是一個包含大量標準Windows API的P/Invoke簽章的wiki。簽章由wiki用戶手動生成。在Microsoft Visual Studio的免費外掛程式可直接搜尋。

PInvoker

編輯

PInvoker是一個匯入本機DLL和C++標頭檔並匯出完全格式和編譯的P/Invoke互操作DLL的應用程式。它通過將本機指標函數參數封裝在PInvoker特定的.NET介面類中,克服了歧義問題。它沒有在P/Invoke方法定義中使用標準的.NET參數類型 (char[], string, 等),而是在P/Invokefunction呼叫中使用這些介面類。

例如,如果我們考慮上面的範例代碼,PInvoker將生成一個.NET P/Invoke函數,該函數接受一個包裝本機char *指標的.NET介面類。此類的構造可以來自stringchar []陣列。兩者的實際本機主記憶體結構是相同的,但每種類型的相應介面類建構函式將以不同的方式填充主記憶體。因此,決定需要傳遞到函數中的.NET類型的責任將傳遞給開發人員。

Microsoft Interop Assistant

編輯

Microsoft Interop Assistant是一個免費工具,提供二進制檔案和原始碼,可在CodePlex上下載。它是根據微軟有限公司公共許可證(Ms LPL)許可證。

它包括兩部分:

  • 一個轉換器,它接受包含struct和方法定義的本地C++標頭檔代碼部分。然後,它生成C# P/Invoke代碼,供複製並貼上到應用程式中。
  • 轉換了Windows API常數、方法和結構定義的可搜尋資料庫。

因為該工具生成C#原始碼而不是編譯的dll,所以用戶可以在使用前對代碼進行任何必要的更改。因此,通過應用程式選擇一個特定的.NET類型來在P/Invoke方法簽章中使用來解決模糊性問題。如果需要,用戶可以將其更改為所需的類型。

P/Invoke精靈

編輯

P/Invoke精靈使用與Microsoft Interop Assistant類似的方法,因為它接受本機C++標頭檔代碼,並生成C#(或VB.NET)代碼供貼上到.NET應用程式代碼中。

它還提供了要針對的框架的選項:用於桌面的.NET framework或用於Windows Mobile智能裝置(和Windows CE)的.NET Compact framework。

xInterop C++ .NET Bridge

編輯

xInterop C++ .NET Bridge是一個windows應用程式,用於為本機C++ DLL建立C#包裝器,並使用C++橋訪問.NET程式集,它附帶了一個C#/.NET庫,用於包裝標準C++類別,如字串、iostream等。可以從.NET訪問C++類別和對象。

該工具從現有的本機C++ DLL和相關的標頭檔生成C#包裝器DLL,這些標頭檔是該工具構建C#包裝器DLL所需的。P/Invoke簽章和數據封送處理由應用程式生成。生成的C#包裝器具有與C++對應程式類似的介面,參數類型轉換為.NET代碼。

此工具辨識未從C++DLL匯出的模板類,並實例化模板類並將其匯出到補充DLL中,相應的C++介面可以在.NET中使用。

參見

編輯

參考文獻

編輯
  1. ^ 「參數編組」(Parameter marshaling)不應與通用術語「編組」(marshalling)混淆,意思是序列化。 封送參數在轉換為 CTS類型後將複製到CLR堆疊中,但不會序列化。
  2. ^ {參照web|url=https://docs.microsoft.com/en-us/cpp/dotnet/double-thunking-cpp%7Ctitle=Double[失效連結] Thunking(C++)}}
  3. ^ {{參照web|url=https://docs.microsoft.com/en-us/cpp/dotnet/initialization-of-mixed-assemblies%7Ctitle=初始化混合程序集}}[失效連結]
  4. ^ The PInvoke problem. learn.microsoft.com. February 6, 2004 [2023-06-28]. (原始內容存檔於2022-10-09). 

外部連結

編輯