從 Managed 程式碼呼叫原生函式

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

如需詳細資訊,請參閱

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

注意

封送處理連結庫提供替代方式,以優化的方式封送處理原生與受控環境之間的數據。 如需封送處理連結庫的詳細資訊,請參閱 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
把手 void* void* IntPtr、UIntPtr
位元組 unsigned char(無符號字元) unsigned char(無符號字元) 位元組
短 短 短 Int16
詞 無符號短整數 無符號短整數 UInt16
INT 整數 (int) 整數 (int) Int32
UINT 不帶正負號的整數 不帶正負號的整數 UInt32
長時間 長 長 Int32
BOOL 長 布爾 (bool) 布林值
下載 無符號長整數 無符號長整數 UInt32
ULONG 無符號長整數 無符號長整數 UInt32
字元 字元 字元 卡爾
LPSTR 字符指標 * String ^ [in], StringBuilder ^ [in, out] String ^ [in], StringBuilder ^ [in, out]
LPCSTR const char* String^ 繩子
LPWSTR wchar_t * String ^ [in], StringBuilder ^ [in, out] String ^ [in], StringBuilder ^ [in, out]
LPCWSTR const wchar_t* String^ 繩子
浮動 FLOAT FLOAT 單身
雙倍 雙倍 雙倍 雙重

如果封送處理器的位址傳遞至 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 互通性