從 Managed 程式碼呼叫原生函式

Common Language Runtime 提供 Platform Invocation Services 或 PInvoke,可讓 Managed 程式碼在原生動態連結程式庫 (DLL) 中呼叫 C 樣式函式。 相同的資料封送處理會用於 COM 與執行時間的互通性,以及用於「Just Works」或 IJW 機制。

如需詳細資訊,請參閱

本節中的範例只會說明如何使用 PInvokePInvoke 可以簡化自訂的資料封送處理,因為您在屬性中以宣告方式提供封送處理資訊,而不是撰寫程式封送處理常式代碼。

注意

封送處理程式庫提供替代方式,以優化的方式封送處理原生與受控環境之間的資料。 如需封送處理程式庫的詳細資訊,請參閱 C++ 封送處理概觀。 封送處理程式庫僅適用于資料,不適用於函式。

PInvoke 和 DllImport 屬性

下列範例示範在 Visual C++ 程式中的 用法 PInvoke 。 原生函式置入定義于 msvcrt.dll 中。 DllImport 屬性用於放置的宣告。

// platform_invocation_services.cpp
// compile with: /clr
using namespace System;
using namespace System::Runtime::InteropServices;

[DllImport("msvcrt", CharSet=CharSet::Ansi)]
extern "C" int puts(String ^);

int main() {
   String ^ pStr = "Hello World!";
   puts(pStr);
}

下列範例相當於先前的範例,但使用 IJW。

// platform_invocation_services_2.cpp
// compile with: /clr
using namespace System;
using namespace System::Runtime::InteropServices;

#include <stdio.h>

int main() {
   String ^ pStr = "Hello World!";
   char* pChars = (char*)Marshal::StringToHGlobalAnsi(pStr).ToPointer();
   puts(pChars);

   Marshal::FreeHGlobal((IntPtr)pChars);
}

IJW 的優點

  • 不需要為程式所使用的 Unmanaged API 撰寫 DLLImport 屬性宣告。 只要包含標頭檔並連結至匯入程式庫即可。

  • IJW 機制會稍微快一點(例如,IJW 存根不需要檢查是否需要釘選或複製資料項目,因為這是開發人員明確完成的)。

  • 這清楚地說明效能問題。 在此情況下,您會從 Unicode 字串轉譯為 ANSI 字串,而且您有語音應答記憶體配置和解除配置的事實。 在此情況下,使用 IJW 撰寫程式碼的開發人員會意識到呼叫 _putws 和使用 PtrToStringChars 會更適合效能。

  • 如果您使用相同的資料呼叫許多 Unmanaged API,則封送處理一次並傳遞封送處理複本比每次重新封送處理更有效率。

IJW 的缺點

  • 封送處理必須在程式碼中明確指定,而不是由屬性指定(通常具有適當的預設值)。

  • 封送處理常式代碼是內嵌的,其中在應用程式邏輯的流程中會更具侵入性。

  • 由於明確封送處理 API 會傳回 IntPtr 32 位到 64 位可攜性的型別,因此您必須使用額外的 ToPointer 呼叫。

C++ 所公開的特定方法是更有效率、更明確的方法,代價是一些額外的複雜度。

如果應用程式主要使用 Unmanaged 資料類型,或呼叫比 .NET Framework API 更多的 Unmanaged API,建議您使用 IJW 功能。 若要在大部分受控應用程式中呼叫偶爾的 Unmanaged API,選擇會比較微妙。

使用 Windows API 進行 PInvoke

PInvoke 在 Windows 中呼叫函式很方便。

在此範例中,Visual C++ 程式會與屬於 WIN32 API 一部分的 MessageBox 函式交互操作。

// platform_invocation_services_4.cpp
// compile with: /clr /c
using namespace System;
using namespace System::Runtime::InteropServices;
typedef void* HWND;
[DllImport("user32", CharSet=CharSet::Ansi)]
extern "C" int MessageBox(HWND hWnd, String ^ pText, String ^ pCaption, unsigned int uType);

int main() {
   String ^ pText = "Hello World! ";
   String ^ pCaption = "PInvoke Test";
   MessageBox(0, pText, pCaption, 0);
}

輸出是一個訊息方塊,其標題為 PInvoke Test,並包含 Hello World! 文字。

PInvoke 也會使用封送處理資訊來查閱 DLL 中的函式。 在 user32.dll 中,實際上沒有 MessageBox 函式,但 CharSet=CharSet::Ansi 可讓 PInvoke 使用 MessageBoxA,ANSI 版本,而不是 MessageBoxW,這是 Unicode 版本。 一般而言,我們建議您使用 Unmanaged API 的 Unicode 版本,因為這樣會消除從 .NET Framework 字串物件的原生 Unicode 格式到 ANSI 的轉譯額外負荷。

何時不使用 PInvoke

使用 PInvoke 不適用於 DLL 中的所有 C 樣式函式。 例如,假設 mylib.dll 中有一個宣告為 MakeSpecial 的函式,如下所示:

char * MakeSpecial(char * pszString);

如果我們在 Visual C++ 應用程式中使用 PInvoke,我們可能會撰寫類似下列內容:

[DllImport("mylib")]
extern "C" String * MakeSpecial([MarshalAs(UnmanagedType::LPStr)] String ^);

這裡的困難在於,我們無法刪除 MakeSpecial 所傳回之 Unmanaged 字串的記憶體。 透過 PInvoke 呼叫的其他函式會傳回使用者不需要解除配置的內部緩衝區指標。 在此情況下,使用 IJW 功能是顯而易見的選擇。

PInvoke 的限制

您無法從您做為參數的原生函式傳回相同的確切指標。 如果原生函式傳回 PInvoke 封送處理給它的指標,記憶體損毀和例外狀況可能會隨之而來。

__declspec(dllexport)
char* fstringA(char* param) {
   return param;
}

下列範例會示範此問題,即使程式可能提供正確的輸出,輸出還是來自已釋放的記憶體。

// platform_invocation_services_5.cpp
// compile with: /clr /c
using namespace System;
using namespace System::Runtime::InteropServices;
#include <limits.h>

ref struct MyPInvokeWrap {
public:
   [ DllImport("user32.dll", EntryPoint = "CharLower", CharSet = CharSet::Ansi) ]
   static String^ CharLower([In, Out] String ^);
};

int main() {
   String ^ strout = "AabCc";
   Console::WriteLine(strout);
   strout = MyPInvokeWrap::CharLower(strout);
   Console::WriteLine(strout);
}

封送處理引數

使用 PInvoke 時,Managed 和 C++ 原生基本類型之間不需要封送處理,且表單相同。 例如,Int32 與 int 之間或 Double 與 double 之間不需要封送處理。

不過,您必須封送處理沒有相同表單的類型。 這包括 char、string 和 struct 類型。 下表顯示封送處理器針對各種類型所使用的對應:

wtypes.h Visual C++ 使用 /clr 的 Visual C++ Common Language Runtime
HANDLE void* void* IntPtr、UIntPtr
BYTE unsigned char unsigned char Byte
SHORT short short Int16
WORD unsigned short unsigned short UInt16
INT int int Int32
UINT 不帶正負號的整數 不帶正負號的整數 UInt32
LONG long long Int32
BOOL long bool 布林值
下載 unsigned long unsigned long UInt32
ULONG unsigned long unsigned long UInt32
CHAR char char Char
LPSTR char * String ^ [in], StringBuilder ^ [in, out] String ^ [in], StringBuilder ^ [in, out]
LPCSTR const char* String^ String
LPWSTR wchar_t * String ^ [in], StringBuilder ^ [in, out] String ^ [in], StringBuilder ^ [in, out]
LPCWSTR const wchar_t* String^ String
FLOAT FLOAT float Single
DOUBLE double double 雙重

如果封送處理器的位址傳遞至 Unmanaged 函式,封送處理器會自動釘選執行時間堆積上配置的記憶體。 釘選可防止垃圾收集行程在壓縮期間移動配置的記憶體區塊。

在本主題稍早所示的範例中,DllImport 的 CharSet 參數會指定如何封送處理 Managed String;在此情況下,它們應該封送處理至原生端的 ANSI 字串。

您可以使用 MarshalAs 屬性,為原生函式的個別引數指定封送處理資訊。 有數個選項可用來封送處理 String * 引數:BStr、ANSIBStr、TBStr、LPStr、LPWStr 和 LPTStr。 預設值為 LPStr。

在此範例中,字串會封送處理為雙位元組 Unicode 字元字串 LPWStr。 輸出是 Hello World 的第一個字母! 因為封送處理字串的第二個位元組是 Null,因此會將這個 解譯為字串結束記號。

// platform_invocation_services_3.cpp
// compile with: /clr
using namespace System;
using namespace System::Runtime::InteropServices;

[DllImport("msvcrt", EntryPoint="puts")]
extern "C" int puts([MarshalAs(UnmanagedType::LPWStr)] String ^);

int main() {
   String ^ pStr = "Hello World!";
   puts(pStr);
}

MarshalAs 屬性位於 System::Runtime::InteropServices 命名空間中。 屬性可以與其他資料類型搭配使用,例如陣列。

如本主題稍早所述,封送處理程式庫提供原生與受控環境之間封送處理資料的新優化方法。 如需詳細資訊,請參閱 C++ 封送處理概觀。

效能考量

PInvoke 每個呼叫的負荷介於 10 到 30 x86 的指示之間。 除了此固定成本之外,封送處理也會建立額外的額外負荷。 Blittable 類型之間沒有在 Managed 和 Unmanaged 程式碼中具有相同標記法的封送處理成本。 例如,int 與 Int32 之間沒有轉譯的成本。

為了提升效能,PInvoke 呼叫會盡可能多封送處理資料,而不是將每個呼叫的資料封送處理較少的呼叫。

另請參閱

原生和 .NET 互通性