类型封送 Type marshalling

本文内容

封送是当类型需要在托管代码和本机代码之间切换时转换类型的过程。

需要封送的原因是托管代码与非托管代码中的类型并不相同。例如,在托管代码中,可指定 String。但在非托管环境中,字符串类型可以是 Unicode(“宽型”)、非 Unicode、null 结尾、ASCII 等。默认情况下,P/Invoke 子系统会尝试基于默认行为执行正确的操作,如本文中所述。但是,如果需要额外的控制,可以使用 MarshalAs 属性指定要在非托管端上使用的预期类型。例如,如果想要将字符串作为以 null 结尾的 ANSI 字符串发送,则可以执行如下操作:

  1. [DllImport("somenativelibrary.dll")]
  2. static extern int MethodA([MarshalAs(UnmanagedType.LPStr)] string parameter);

封送通用类型的默认规则Default rules for marshalling common types

通常,运行时会尝试在封送时执行“正确的操作”,从而最大限地减少用户的工作。下表介绍了每种类型在用于参数或字段时的默认封送方式。C99/C++11 固定宽度的整数和字符类型用于确保下表适用于所有平台。可以使用与这些类型具有相同的对齐和大小要求的任何本机类型。

第一个表介绍了其封送与 P/Invoke 和字段封送相同的各种类型的映射。

.NET 类型本机类型
byteuint8_t
sbyteint8_t
shortint16_t
ushortuint16_t
intint32_t
uintuint32_t
longint64_t
ulonguint64_t
charcharchar16_t 依赖于 P/Invoke 或结构的 CharSet请参阅字符集文档
stringcharchar16_t 依赖于 P/Invoke 或结构的 CharSet请参阅字符集文档
System.IntPtrintptr_t
System.UIntPtruintptr_t
.NET 指针类型(例如,void)void
System.Runtime.InteropServices.SafeHandle 派生的类型void
System.Runtime.InteropServices.CriticalHandle 派生的类型void
boolWin32 BOOL 类型
decimalCOM DECIMAL 结构
.NET 委托本机函数指针
System.DateTimeWin32 DATE 类型
System.GuidWin32 GUID 类型

如果作为参数或结构进行封送,则几个封送类别具有不同的默认设置。

.NET 类型本机类型(参数)本机类型(字段)
.NET 数组指向数组元素的本机表示形式的数组开头的指针。不允许不带 [MarshalAs] 属性
LayoutKindSequentialExplicit 的类指向类的本机表示形式的指针类的本机表示形式

下表包含仅适用于 Windows 的默认封送规则。在非 Windows 平台上,无法封送这些类型。

.NET 类型本机类型(参数)本机类型(字段)
objectVARIANTIUnknown
System.ArrayCOM 接口不允许不带 [MarshalAs] 属性
System.ArgIteratorva_list不允许
System.Collections.IEnumeratorIEnumVARIANT不允许
System.Collections.IEnumerableIDispatch*不允许
System.DateTimeOffsetint64_t 表示自 1601 年 1 月 1 日午夜以来的时钟周期数int64_t 表示自 1601 年 1 月 1 日午夜以来的时钟周期数

某些类型只能作为参数封送,而不能作为字段封送。下表列出了这些类型:

.NET 类型本机类型(仅参数)
System.Text.StringBuildercharchar16_t 依赖于 P/Invoke 的 CharSet请参阅字符集文档
System.ArgIteratorva_list(仅在 Windows x86/x64/arm64 上)
System.Runtime.InteropServices.ArrayWithOffsetvoid
System.Runtime.InteropServices.HandleRefvoid

如果这些默认设置不执行所需的操作,则可以自定义参数的封送方式。参数封送一文介绍如何自定义不同参数类型的封送方式的具体步骤。

封送类和结构Marshalling classes and structs

有关类型封送的另一个问题是如何将结构传入非托管方法。例如,某些非托管方法需要使用结构作为参数。在这种情况下,需要在环境的托管部分中创建相应的结构或类,以便将它用作参数。不过,仅仅是定义类并不足够,还需要告知封送处理程序如何将类中的字段映射到非托管结构。在此处,StructLayout 属性将会很有用。

  1. [DllImport("kernel32.dll")]
  2. static extern void GetSystemTime(SystemTime systemTime);
  3. [StructLayout(LayoutKind.Sequential)]
  4. class SystemTime {
  5. public ushort Year;
  6. public ushort Month;
  7. public ushort DayOfWeek;
  8. public ushort Day;
  9. public ushort Hour;
  10. public ushort Minute;
  11. public ushort Second;
  12. public ushort Milsecond;
  13. }
  14. public static void Main(string[] args) {
  15. SystemTime st = new SystemTime();
  16. GetSystemTime(st);
  17. Console.WriteLine(st.Year);
  18. }

前面的代码演示了如何调用 GetSystemTime() 函数的简单示例。值得注意的部分是第 4 行。该属性指定应按顺序将类的字段映射到另一端(非托管端)上的结构。这意味着,字段的命名并不重要,唯一重要的是字段顺序,因为这种顺序需对应于非托管结构,如下面的示例所示:

  1. typedef struct _SYSTEMTIME {
  2. WORD wYear;
  3. WORD wMonth;
  4. WORD wDayOfWeek;
  5. WORD wDay;
  6. WORD wHour;
  7. WORD wMinute;
  8. WORD wSecond;
  9. WORD wMilliseconds;
  10. } SYSTEMTIME, *PSYSTEMTIME*;

有时,默认结构封送不执行所需的操作。自定义结构封送一文介绍如何自定义结构的封送方式。