如何:使用 P/Invoke 封送字符串

通过使用 .NET Framework 平台调用 (P/Invoke) 支持,可以使用 CLR 字符串类型 System::String 调用接受 C 样式字符串的本机函数。 建议尽可能使用 C++ 互操作功能,而不是 P/Invoke。 因为 P/Invoke 很少提供编译时错误报告,不属于类型安全,并且实现起来很繁琐。 如果未托管的 API 打包为 DLL,并且源代码不可用,则 P/Invoke 是唯一选项。 否则,请参阅 使用 C++ 互操作(隐式 P/Invoke)

托管和非托管字符串在内存中布局不同,因此,将字符串从托管函数传递到非托管函数需要 MarshalAsAttribute 属性来指示编译器插入需要的转换机制,以便正确安全地封送字符串数据。

与仅使用内部数据类型的函数一样,DllImportAttribute 用于将托管入口点声明到本机函数中。 传递字符串的函数可以使用 String 类型的句柄,而不是将这些入口点定义为采用 C 样式字符串。 使用此类型提示编译器插入执行所需转换的代码。 对于采用字符串的非托管函数中的每个函数参数,请使用 MarshalAsAttribute 属性指示应将 String 对象作为 C 样式字符串封送给本机函数。

封送处理器将对非托管函数的调用包装在隐藏的包装器例程中。 包装器例程将托管字符串固定并复制到非托管上下文中的本地分配字符串中。 然后将本地副本传递给非托管函数。 当非托管函数返回时,包装器删除资源。 或者,如果它位于堆栈上,则在包装器超出范围时将其回收。 非托管函数不负责此内存。 非托管代码仅在其自己的 CRT 设置的堆中创建和删除内存,因此封送器使用不同 CRT 版本永远不会有问题。

如果非托管函数返回一个字符串(作为返回值或 out 参数),封送器会将它复制到新的托管字符串中,然后释放内存。 有关详细信息,请参阅默认封送处理行为用平台调用封送数据

示例

以下代码由一个非托管模块和一个托管模块组成。 未托管的模块是 DLL,它定义一个名为 TakesAString 的函数。 TakesAString 接受 char* 形式的 C 样式窄字符串。

// TraditionalDll2.cpp
// compile with: /LD /EHsc
#include <windows.h>
#include <stdio.h>
#include <iostream>

using namespace std;

#define TRADITIONALDLL_EXPORTS
#ifdef TRADITIONALDLL_EXPORTS
#define TRADITIONALDLL_API __declspec(dllexport)
#else
#define TRADITIONALDLL_API __declspec(dllimport)
#endif

extern "C" {
   TRADITIONALDLL_API void TakesAString(char*);
}

void TakesAString(char* p) {
   printf_s("[unmanaged] %s\n", p);
}

托管模块是一个命令行应用程序,它导入 TakesAString 函数,但将其定义为采用托管 System.String 而不是 char*MarshalAsAttribute 属性用于指示调用 TakesAString 时应如何封送托管字符串。

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

value struct TraditionalDLL
{
   [DllImport("TraditionalDLL2.dll")]
      static public void
      TakesAString([MarshalAs(UnmanagedType::LPStr)]String^);
};

int main() {
   String^ s = gcnew String("sample string");
   Console::WriteLine("[managed] passing managed string to unmanaged function...");
   TraditionalDLL::TakesAString(s);
   Console::WriteLine("[managed] {0}", s);
}

此技术在非托管堆上构造字符串的副本,因此本机函数对字符串所做的更改不会反映在字符串的托管副本中。

传统 #include 指令不向托管代码公开 DLL 的任何部分。 事实上,DLL 仅在运行时被访问,因此使用 DllImport 导入的函数问题不会在编译时检测到。

另请参阅

在 C++ 中使用显式 P/Invoke(DllImport 特性)