从托管代码调用本机函数

公共语言运行时提供平台调用服务或 PInvoke,它使托管代码能够调用本机动态链接库 (DLL) 中的 C 样式函数。 相同的数据编组用于 COM 与运行时的互操作性以及“It Just Works”(简称 IJW)机制。

有关详细信息,请参阅:

本节中的示例仅说明如何使用 PInvokePInvoke 可以简化自定义数据编组,因为你在属性中以声明方式提供编组信息,而不是编写过程编组代码。

注意

封送库提供了另一种以优化方式在本机和托管环境之间封送数据的方法。 有关封送处理库的详细信息,请参阅 Overview of Marshaling in C++。 封送库仅可用于数据,不能用于函数。

PInvoke 和 DllImport 特性

以下示例显示了如何在 Visual C++ 程序中使用 PInvoke。 本机函数 puts 是在 msvcrt.dll 中定义的。 DllImport 特性用于声明 puts。

// 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 的优势

  • 无需为程序使用的非托管 API 编写 DLLImport 属性声明。 只需包含头文件并与导入库链接即可。

  • IJW 机制的速度稍微快一些(例如,IJW 存根不需要检查是否需要固定或复制数据项,因为这是由开发人员明确完成的)。

  • 这种机制清楚地说明了性能问题。 在这种情况下,实际上你是从 Unicode 字符串转换为 ANSI 字符串,并且你有一个伴随的内存分配和解除分配。 在这种情况下,使用 IJW 编写代码的开发人员会意识到调用 _putws 和使用 PtrToStringChars 会更好地提高性能。

  • 如果你使用相同的数据调用许多非托管 API,则最好是将其封送一次并传递封送副本,这样比每次都重新封送的效率更高。

IJW 的缺点

  • 封送必须在代码中明确指定,而不是通过特性来指定(通常具有适当的默认值)。

  • 封送代码是内联的,这在应用程序逻辑流中更具侵入性。

  • 因为显式封送 API 返回 32 位到 64 位可移植性的 IntPtr 类型,所以你必须使用额外的 ToPointer 调用。

C++ 公开的特定方法是更有效的显式方法,但代价是更复杂性。

如果应用程序主要使用非托管数据类型,或者如果它调用的非托管 API 多于 .NET Framework API,我们建议你使用 IJW 功能。 要在大部分托管的应用程序中调用偶尔的非托管 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 可以使用 ANSI 版本的 MessageBoxA,而不是 Unicode 版本的 MessageBoxW。 通常,我们建议你使用非托管 API 的 Unicode 版本,因为这样可以消除从将 .NET Framework 字符串对象转换为 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 返回的非托管字符串占用的内存。 通过 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,在托管和 C++ 本机原始类型之间不需要封送,具有相同的形式。 例如,在 Int32 和 int 之间或 Double 和 double 之间不需要封送。

但对于没有相同窗体的类型,必须进行封送。 这包括 char、string 和 struct 类型。 下表显示了封送拆收器用于各种类型的映射:

wtypes.h Visual C++ 使用 /clr 的 Visual C++ 公共语言运行时
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 unsigned int unsigned int UInt32
LONG long long Int32
BOOL long bool 布尔
DWORD 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 * 字符串 ^ 字符串
LPWSTR wchar_t * String ^ [in], StringBuilder ^ [in, out] String ^ [in], StringBuilder ^ [in, out]
LPCWSTR const wchar_t* 字符串 ^ 字符串
FLOAT float FLOAT Single
DOUBLE Double Double 双精度

如果已将内存地址传递给非托管函数,则封送拆收器会自动固定在运行时堆上分配的内存。 固定可防止垃圾收集器在压缩期间移动分配的内存块。

在本主题前面显示的示例中,DllImport 的 CharSet 参数指定应如何封送托管字符串;在这种情况下,它们应该被封送为本地端的 ANSI 字符串。

你可以使用 MarshalAs 特性为本机函数的各个参数指定封送信息。 封送 String * 参数有多种选择:BStr、ANSIBStr、TBStr、LPStr、LPWStr 和 LPTStr。 默认值为 LPStr。

在此示例中,字符串被封送为双字节 Unicode 字符串 LPWStr。 输出是 Hello World! 的第一个字母 这是因为封送字符串的第二个字节为空,并且 puts 将其解释为字符串结束标记。

// 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 类型之间没有封送成本。 例如,在 int 和 Int32 之间进行转换是没有成本的。

为了获得更好的性能,请尽量减少 PInvoke 调用的数量,封送尽可能多的数据,而不是 PInvoke 封送的数量,每次调用封送少量数据。

另请参阅

本机和 .NET 的互操作性