自定义结构封送 Customizing structure marshalling

本文内容

有时,结构的默认封送规则无法完全满足要求。.NET 运行时提供了几个扩展点以自定义结构的布局和字段的封送方式。

自定义结构布局Customizing structure layout

.NET 提供了 StructLayoutAttribute.aspx) 属性和 System.Runtime.InteropServices.LayoutKind 枚举,允许用户自定义字段在内存中的放置方式。以下指南将帮助你避免常见问题。

✔️ 请考虑尽量使用 LayoutKind.Sequential

✔️ 当本机结构还具有显式布局(如联合)时,请务必仅将 LayoutKind.Explicit 用于封送。

❌ 在非 Windows 平台上封送结构时,请避免使用 LayoutKind.Explicit.NET Core 运行时不支持在 Intel 或 AMD 64 位的非 Windows 系统上按值将显式结构传递到本机函数。但是,运行时支持在所有平台上按引用传递显式结构。

自定义布尔字段封送Customizing boolean field marshalling

本机代码具有许多不同的布尔表示形式。仅在 Windows 上,有三种方式可用于表示布尔值。运行时不知道结构的本机定义,因此,它最多只能对如何封送布尔值做出猜测。.NET 运行时提供指示如何封送布尔字段的方式。下面的示例介绍如何将 .NET bool 封送到不同的本机布尔类型。

布尔值默认作为本机 4 字节 Win32 BOOL 值进行封送,如下面的示例所示:

  1. public struct WinBool
  2. {
  3. public bool b;
  4. }
  1. struct WinBool
  2. {
  3. public BOOL b;
  4. };

如果想要明确指出,则可以使用 UnmanagedType.Bool 值获取如上所述的行为:

  1. public struct WinBool
  2. {
  3. [MarshalAs(UnmanagedType.Bool)]
  4. public bool b;
  5. }
  1. struct WinBool
  2. {
  3. public BOOL b;
  4. };

使用下面的 UnmanagedType.U1UnmanagedType.I1 值,可以告知运行时将 b 字段作为 1 字节本机 bool 类型进行封送。

  1. public struct CBool
  2. {
  3. [MarshalAs(UnmanagedType.U1)]
  4. public bool b;
  5. }
  1. struct CBool
  2. {
  3. public bool b;
  4. };

在 Windows 上,可以使用 UnmanagedType.VariantBool 值告知运行时将布尔值封送到 2 字节的 VARIANT_BOOL 值:

  1. public struct VariantBool
  2. {
  3. [MarshalAs(UnmanagedType.VariantBool)]
  4. public bool b;
  5. }
  1. struct VariantBool
  2. {
  3. public VARIANT_BOOL b;
  4. };

备注

VARIANT_BOOLVARIANT_TRUE = -1VARIANT_FALSE = 0 中的大多数 bool 类型不同。此外,不等于 VARIANT_TRUE 的所有值都将被视为 false。

自定义数组字段封送Customizing array field marshalling

.NET 还包括自定义数组封送的多种方式。

默认情况下,.NET 将数组作为指向元素的连续列表的指针进行封送:

  1. public struct DefaultArray
  2. {
  3. public int[] values;
  4. }
  1. struct DefaultArray
  2. {
  3. int* values;
  4. };

如果要与 COM API 交互,则可能必须将数组作为 SAFEARRAY 对象进行封送。可以使用 System.Runtime.InteropServices.MarshalAsAttributeUnmanagedType.SafeArray 值告知运行时将数组作为 SAFEARRAY 进行封送:

  1. public struct SafeArrayExample
  2. {
  3. [MarshalAs(UnmanagedType.SafeArray)]
  4. public int[] values;
  5. }
  1. struct SafeArrayExample
  2. {
  3. SAFEARRAY* values;
  4. };

如果需要自定义 SAFEARRAY 中的元素类型,则可以使用 MarshalAsAttribute.SafeArraySubTypeMarshalAsAttribute.SafeArrayUserDefinedSubType 字段自定义 SAFEARRAY 的确切元素类型。

如果需要就地封送数组,则可以使用 UnmanagedType.ByValArray 值告知封送处理程序就地封送数组。使用此封送时,还必须为数组中的元素数对应的 MarshalAsAttribute.SizeConst 字段提供一个值,以便运行时可以正确地为结构分配空间。

  1. public struct InPlaceArray
  2. {
  3. [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
  4. public int[] values;
  5. }
  1. struct InPlaceArray
  2. {
  3. int values[4];
  4. };

备注

.NET 不支持将变长度数组字段作为 C99 可变数组成员进行封送。

自定义字符串字段封送Customizing string field marshalling

.NET 还提供用于封送字符串字段的各种自定义。

默认情况下,.NET 将字符串作为指向以 null 结尾的字符串的指针进行封送。编码取决于 StructLayoutAttribute.aspx) 中的 StructLayoutAttribute.CharSet 字段的值。如果未指定任何属性,则编码将默认为 ANSI 编码。

  1. [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
  2. public struct DefaultString
  3. {
  4. public string str;
  5. }
  1. struct DefaultString
  2. {
  3. char* str;
  4. };
  1. [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
  2. public struct DefaultString
  3. {
  4. public string str;
  5. }
  1. struct DefaultString
  2. {
  3. char16_t* str; // Could also be wchar_t* on Windows.
  4. };

如果需要对不同字段使用不同编码或只是希望在结构定义中更加明确,则可以在 System.Runtime.InteropServices.MarshalAsAttribute 属性上使用 UnmanagedType.LPStrUnmanagedType.LPWStr 值。

  1. public struct AnsiString
  2. {
  3. [MarshalAs(UnmanagedType.LPStr)]
  4. public string str;
  5. }
  1. struct AnsiString
  2. {
  3. char* str;
  4. };
  1. public struct UnicodeString
  2. {
  3. [MarshalAs(UnmanagedType.LPWStr)]
  4. public string str;
  5. }
  1. struct UnicodeString
  2. {
  3. char16_t* str; // Could also be wchar_t* on Windows.
  4. };

如果想要使用 UTF-8 编码封送字符串,则可以在 MarshalAsAttribute 中使用 UnmanagedType.LPUTF8Str 值。

  1. public struct UTF8String
  2. {
  3. [MarshalAs(UnmanagedType.LPUTF8Str)]
  4. public string str;
  5. }
  1. struct UTF8String
  2. {
  3. char* str;
  4. };

备注

使用 UnmanagedType.LPUTF8Str 需要 .NET Framework 4.7(或更高版本)或 .NET Core 1.1(或更高版本)。不能在 .NET Standard 2.0 中使用。

如果使用 COM API,则可能需要将字符串作为 BSTR 进行封送。使用 UnmanagedType.BStr 值可以将字符串作为 BSTR 进行封送。

  1. public struct BString
  2. {
  3. [MarshalAs(UnmanagedType.BStr)]
  4. public string str;
  5. }
  1. struct BString
  2. {
  3. BSTR str;
  4. };

使用基于 WinRT 的 API 时,可能需要将字符串作为 HSTRING 进行封送。使用 UnmanagedType.HString 值可以将字符串作为 HSTRING 进行封送。

  1. public struct HString
  2. {
  3. [MarshalAs(UnmanagedType.HString)]
  4. public string str;
  5. }
  1. struct BString
  2. {
  3. HSTRING str;
  4. };

如果 API 要求你将字符串就地传入结构,则可以使用 UnmanagedType.ByValTStr 值。务必注意,通过 ByValTStr 封送的字符串的编码由 CharSet 属性确定。此外,还需要通过 MarshalAsAttribute.SizeConst 字段传递字符串长度。

  1. [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
  2. public struct DefaultString
  3. {
  4. [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 4)]
  5. public string str;
  6. }
  1. struct DefaultString
  2. {
  3. char str[4];
  4. };
  1. [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
  2. public struct DefaultString
  3. {
  4. [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 4)]
  5. public string str;
  6. }
  1. struct DefaultString
  2. {
  3. char16_t str[4]; // Could also be wchar_t[4] on Windows.
  4. };

自定义十进制字段封送Customizing decimal field marshalling

如果在 Windows 上操作,则可能会遇到一些使用本机 CYCURRENCY 结构的 API。默认情况下,.NET decimal 类型会封送到本机 DECIMAL 结构。但是,可以使用包含 UnmanagedType.Currency 值的 MarshalAsAttribute 来指示封送处理程序将 decimal 值转换为本机 CY 值。

  1. public struct Currency
  2. {
  3. [MarshalAs(UnmanagedType.Currency)]
  4. public decimal dec;
  5. }
  1. struct Currency
  2. {
  3. CY dec;
  4. };

封送 System.ObjectMarshalling System.Objects

在 Windows 上,可以将类型为 object 的字段封送到本机代码。可以将这些字段封送到以下三个类型之一:

  1. public struct ObjectDefault
  2. {
  3. public object obj;
  4. }
  1. struct ObjectDefault
  2. {
  3. IUnknown* obj;
  4. };

如果要将对象字段封送到 IDispatch*,请添加包含 UnmanagedType.IDispatch 值的 MarshalAsAttribute

  1. public struct ObjectDispatch
  2. {
  3. [MarshalAs(UnmanagedType.IDispatch)]
  4. public object obj;
  5. }
  1. struct ObjectDispatch
  2. {
  3. IDispatch* obj;
  4. };

如果要将其作为 VARIANT 进行封送,请添加包含 UnmanagedType.Struct 值的 MarshalAsAttribute

  1. public struct ObjectVariant
  2. {
  3. [MarshalAs(UnmanagedType.Struct)]
  4. public object obj;
  5. }
  1. struct ObjectVariant
  2. {
  3. VARIANT obj;
  4. };

下表介绍了如何将 obj 字段的不同运行时类型映射到存储在 VARIANT 中的各种类型:

.NET 类型VARIANT 类型.NET 类型VARIANT 类型
byteVT_UI1System.Runtime.InteropServices.BStrWrapperVT_BSTR
sbyteVT_I1objectVT_DISPATCH
shortVT_I2System.Runtime.InteropServices.UnknownWrapperVT_UNKNOWN
ushortVT_UI2System.Runtime.InteropServices.DispatchWrapperVT_DISPATCH
intVT_I4System.Reflection.MissingVT_ERROR
uintVT_UI4(object)nullVT_EMPTY
longVT_I8boolVT_BOOL
ulongVT_UI8System.DateTimeVT_DATE
floatVT_R4decimalVT_DECIMAL
doubleVT_R8System.Runtime.InteropServices.CurrencyWrapperVT_CURRENCY
charVT_UI2System.DBNullVT_NULL
stringVT_BSTR