如何:使用 PInvoke 封送结构

本文档介绍如何使用 P/Invoke 从托管函数调用接受 C 样式结构的本机函数。 尽管我们建议使用 C++ 互操作功能(而不是 P/Invoke,因为 P/Invoke 提供很少的编译时错误报告,不是类型安全的,并且实现起来可能比较繁琐),但如果非托管 API 打包为 DLL 且源代码不可用,P/Invoke 是唯一的选项。 否则,请参阅以下文档:

默认情况下,本机和托管结构在内存中布局不同,因此成功跨托管/非托管边界传递结构需要额外的步骤才能保持数据完整性。

本文档介绍定义本机结构的托管等效项所需的步骤以及如何将生成的结构传递给非托管函数。 本文档假定使用简单结构(即不包含字符串或指针的结构)。 有关非 blittable 互操作性的信息,请参阅使用 C++ 互操作(隐式 PInvoke)。 P/Invoke 不能将非 blittable 类型用作返回值。 Blittable 类型在托管和非托管代码中具有相同表示。 有关详细信息,请参阅 Blittable 和非 Blittable 类型

跨托管/非托管边界封送简单的 blittable 结构首先需要定义每个本机结构的托管版本。 这些结构可以具有任何法定名称;除了数据布局之外,两个结构的本机版本和托管版本之间没有关系。 因此,托管版本必须包含与本机版本的大小和顺序相同的字段。 (没有机制可以确保结构的托管版本和本机版本等效,因此在运行时之前,不兼容不会变得明显。程序员有责任确保两个结构具有相同的数据布局。)

由于托管结构的成员有时出于性能目的重新排列,因此必须使用 StructLayoutAttribute 属性来指示按顺序布局结构。 此外,最好显式设置结构打包设置,使其与本机结构所使用的设置相同。 (尽管默认情况下,Visual C++ 对托管代码使用 8 字节结构打包。)

  1. 接下来,使用 DllImportAttribute 声明与接受结构的任何非托管函数相对应的入口点,但在函数签名中使用结构的托管版本,如果对结构的两个版本使用相同的名称,则会成为争议。

  2. 现在,托管代码可以将结构的托管版本传递给非托管函数,就像它们实际上是托管函数一样。 可以按值或按引用传递这些结构,如以下示例所示。

非托管模块和托管模块

以下代码由一个非托管模块和一个托管模块组成。 非托管模块是 DLL,它定义一个名为 Location 的结构和一个名为 GetDistance 的函数,该函数接受 Location 结构的两个实例。 第二个模块是导入 GetDistance 函数的托管命令行应用程序,但根据 Location 结构 MLocation 的托管等效项定义它。 实际上,同一名称可能用于结构的两个版本;但是,此处使用不同的名称来演示 DllImport 原型是根据托管版本定义的。

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

示例:非托管 DLL 模块

// TraditionalDll3.cpp
// compile with: /LD /EHsc
#include <iostream>
#include <stdio.h>
#include <math.h>

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

#pragma pack(push, 8)
struct Location {
   int x;
   int y;
};
#pragma pack(pop)

extern "C" {
   TRADITIONALDLL_API double GetDistance(Location, Location);
   TRADITIONALDLL_API void InitLocation(Location*);
}

double GetDistance(Location loc1, Location loc2) {
   printf_s("[unmanaged] loc1(%d,%d)", loc1.x, loc1.y);
   printf_s(" loc2(%d,%d)\n", loc2.x, loc2.y);

   double h = loc1.x - loc2.x;
   double v = loc1.y = loc2.y;
   double dist = sqrt( pow(h,2) + pow(v,2) );

   return dist;
}

void InitLocation(Location* lp) {
   printf_s("[unmanaged] Initializing location...\n");
   lp->x = 50;
   lp->y = 50;
}

示例:托管命令行应用程序模块

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

[StructLayout(LayoutKind::Sequential, Pack=8)]
value struct MLocation {
   int x;
   int y;
};

value struct TraditionalDLL {
   [DllImport("TraditionalDLL3.dll")]
   static public double GetDistance(MLocation, MLocation);
   [DllImport("TraditionalDLL3.dll")]
   static public double InitLocation(MLocation*);
};

int main() {
   MLocation loc1;
   loc1.x = 0;
   loc1.y = 0;

   MLocation loc2;
   loc2.x = 100;
   loc2.y = 100;

   double dist = TraditionalDLL::GetDistance(loc1, loc2);
   Console::WriteLine("[managed] distance = {0}", dist);

   MLocation loc3;
   TraditionalDLL::InitLocation(&loc3);
   Console::WriteLine("[managed] x={0} y={1}", loc3.x, loc3.y);
}
[unmanaged] loc1(0,0) loc2(100,100)
[managed] distance = 141.42135623731
[unmanaged] Initializing location...
[managed] x=50 y=50

另请参阅

在 C++ 中使用显式 PInvoke(DllImport 特性)