平台叫用服務
平台叫用服務(英語:Platform Invocation Services),或稱P/Invoke,是微軟的公共語言基礎設施實現的一個特性,類似於微軟的公共語言運行時提供的跨平台調用方式,允許受控代碼調用原生代碼。
託管代碼(例如C#或VB.NET或C++/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)的元數據,用來定義如何調用原生代碼、如何訪問數據(通常需要屬性源說明符來幫助編譯器生成整集(marshal)代碼)
- 這種定義就是「顯式」部分。
隱式
- 使用C++/CLI編程語言的程序可以同時使用受管堆(以追蹤指針)和任何原生內存區域,不必顯式聲明。(即「隱式」)
- 隱式方法最主要好處是如果原生數據結構改變了,只要保持命名兼任,就能保持向後兼容。
- 即,只要結構成員名稱沒有改變,就可以透明地支持在原生代碼頭文件中添加/刪除/重新排序結構。
細節
使用P/Invoke 時,CLR處理DLL加載以及將非託管以前的類型轉換為CTS] 類型(也稱為「參數編組」)。[1]要執行此操作,CLR:
P/Invoke對於使用標準(非託管)C 或 C++ 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帶來了新的挑戰:
如果遇到這些問題,這些參考資料會為每個問題指定解決方案。一個主要的好處是消除了結構聲明,字段聲明的順序和對齊問題在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接口類。此類的構造可以來自string或char []數組。兩者的實際本機內存結構是相同的,但每種類型的相應接口類構造函數將以不同的方式填充內存。因此,決定需要傳遞到函數中的.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中使用。
參見
參考文獻
- ^ 「參數編組」(Parameter marshaling)不應與通用術語「編組」(marshalling)混淆,意思是序列化。 封送參數在轉換為 CTS類型後將複製到CLR堆棧中,但不會序列化。
- ^ {引用web|url=https://docs.microsoft.com/en-us/cpp/dotnet/double-thunking-cpp%7Ctitle=Double[失效連結] Thunking(C++)}}
- ^ {{引用web|url=https://docs.microsoft.com/en-us/cpp/dotnet/initialization-of-mixed-assemblies%7Ctitle=初始化混合程序集}}[失效連結]
- ^ The PInvoke problem. learn.microsoft.com. February 6, 2004 [2023-06-28]. (原始內容存檔於2022-10-09).