自定义结构封送Customize structure marshaling

有时,结构的默认封送规则无法完全满足要求。Sometimes the default marshaling rules for structures aren't exactly what you need. .NET 运行时提供了几个扩展点以自定义结构的布局和字段的封送方式。The .NET runtimes provide a few extension points for you to customize your structure's layout and how fields are marshaled.

自定义结构布局Customizing structure layout

.NET 提供了 System.Runtime.InteropServices.StructLayoutAttribute 属性和 System.Runtime.InteropServices.LayoutKind 枚举,允许用户自定义字段在内存中的放置方式。.NET provides the System.Runtime.InteropServices.StructLayoutAttribute attribute and the System.Runtime.InteropServices.LayoutKind enumeration to allow you to customize how fields are placed in memory. 以下指南将帮助你避免常见问题。The following guidance will help you avoid common issues.

✔️ 请考虑尽量使用 LayoutKind.Sequential✔️ CONSIDER using LayoutKind.Sequential whenever possible.

✔️ 当本机结构还具有显式布局(如联合)时,请务必仅将 LayoutKind.Explicit 用于封送。✔️ DO only use LayoutKind.Explicit in marshaling when your native struct also has an explicit layout, such as a union.

❌ 如果需要在 .NET Core 3.0 之前以运行时为目标,请避免在非 Windows 平台上封送结构时使用 LayoutKind.Explicit❌ AVOID using LayoutKind.Explicit when marshaling structures on non-Windows platforms if you need to target runtimes before .NET Core 3.0. 3.0 之前的 .NET Core 运行时不支持在 Intel 或 AMD 64 位的非 Windows 系统上,按值将显式结构传递到本机函数。The .NET Core runtime before 3.0 doesn't support passing explicit structures by value to native functions on Intel or AMD 64-bit non-Windows systems. 但是,运行时支持在所有平台上按引用传递显式结构。However, the runtime supports passing explicit structures by reference on all platforms.

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

本机代码具有许多不同的布尔表示形式。Native code has many different boolean representations. 仅在 Windows 上,有三种方式可用于表示布尔值。On Windows alone, there are three ways to represent boolean values. 运行时不知道结构的本机定义,因此,它最多只能对如何封送布尔值做出猜测。The runtime doesn't know the native definition of your structure, so the best it can do is make a guess on how to marshal your boolean values. .NET 运行时提供指示如何封送布尔字段的方式。The .NET runtime provides a way to indicate how to marshal your boolean field. 下面的示例介绍如何将 .NET bool 封送到不同的本机布尔类型。The following examples show how to marshal .NET bool to different native boolean types.

布尔值默认作为本机 4 字节 Win32 BOOL 值进行封送,如下面的示例所示:Boolean values default to marshaling as a native 4-byte Win32 BOOL value as shown in the following example:

public struct WinBool
{
    public bool b;
}
struct WinBool
{
    public BOOL b;
};

如果想要明确指出,则可以使用 UnmanagedType.Bool 值获取如上所述的行为:If you want to be explicit, you can use the UnmanagedType.Bool value to get the same behavior as above:

public struct WinBool
{
    [MarshalAs(UnmanagedType.Bool)]
    public bool b;
}
struct WinBool
{
    public BOOL b;
};

使用下面的 UnmanagedType.U1UnmanagedType.I1 值,可以告知运行时将 b 字段作为 1 字节本机 bool 类型进行封送。Using the UnmanagedType.U1 or UnmanagedType.I1 values below, you can tell the runtime to marshal the b field as a 1-byte native bool type.

public struct CBool
{
    [MarshalAs(UnmanagedType.U1)]
    public bool b;
}
struct CBool
{
    public bool b;
};

在 Windows 上,可以使用 UnmanagedType.VariantBool 值告知运行时将布尔值封送到 2 字节的 VARIANT_BOOL 值:On Windows, you can use the UnmanagedType.VariantBool value to tell the runtime to marshal your boolean value to a 2-byte VARIANT_BOOL value:

public struct VariantBool
{
    [MarshalAs(UnmanagedType.VariantBool)]
    public bool b;
}
struct VariantBool
{
    public VARIANT_BOOL b;
};

备注

VARIANT_BOOLVARIANT_TRUE = -1VARIANT_FALSE = 0 中的大多数 bool 类型不同。VARIANT_BOOL is different than most bool types in that VARIANT_TRUE = -1 and VARIANT_FALSE = 0. 此外,不等于 VARIANT_TRUE 的所有值都将被视为 false。Additionally, all values that aren't equal to VARIANT_TRUE are considered false.

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

.NET 还包括自定义数组封送的多种方式。.NET also includes a few ways to customize array marshaling.

默认情况下,.NET 将数组作为指向元素的连续列表的指针进行封送:By default, .NET marshals arrays as a pointer to a contiguous list of the elements:

public struct DefaultArray
{
    public int[] values;
}
struct DefaultArray
{
    int* values;
};

如果要与 COM API 交互,则可能必须将数组作为 SAFEARRAY* 对象进行封送。If you're interfacing with COM APIs, you may have to marshal arrays as SAFEARRAY* objects. 可以使用 System.Runtime.InteropServices.MarshalAsAttributeUnmanagedType.SafeArray 值告知运行时将数组作为 SAFEARRAY* 进行封送:You can use the System.Runtime.InteropServices.MarshalAsAttribute and the UnmanagedType.SafeArray value to tell the runtime to marshal an array as a SAFEARRAY*:

public struct SafeArrayExample
{
    [MarshalAs(UnmanagedType.SafeArray)]
    public int[] values;
}
struct SafeArrayExample
{
    SAFEARRAY* values;
};

如果需要自定义 SAFEARRAY 中的元素类型,则可以使用 MarshalAsAttribute.SafeArraySubTypeMarshalAsAttribute.SafeArrayUserDefinedSubType 字段自定义 SAFEARRAY 的确切元素类型。If you need to customize what type of element is in the SAFEARRAY, then you can use the MarshalAsAttribute.SafeArraySubType and MarshalAsAttribute.SafeArrayUserDefinedSubType fields to customize the exact element type of the SAFEARRAY.

如果需要就地封送数组,则可以使用 UnmanagedType.ByValArray 值告知封送处理程序就地封送数组。If you need to marshal the array in-place, you can use the UnmanagedType.ByValArray value to tell the marshaler to marshal the array in-place. 使用此封送时,还必须为数组中的元素数对应的 MarshalAsAttribute.SizeConst 字段提供一个值,以便运行时可以正确地为结构分配空间。When you're using this marshaling, you also must supply a value to the MarshalAsAttribute.SizeConst field for the number of elements in the array so the runtime can correctly allocate space for the structure.

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

备注

.NET 不支持将变长数组字段作为 C99 可变数组成员进行封送。.NET doesn't support marshaling a variable length array field as a C99 Flexible Array Member.

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

.NET 还提供用于封送字符串字段的各种自定义。.NET also provides a wide variety of customizations for marshaling string fields.

默认情况下,.NET 将字符串作为指向以 null 结尾的字符串的指针进行封送。By default, .NET marshals a string as a pointer to a null-terminated string. 编码取决于 System.Runtime.InteropServices.StructLayoutAttribute 中的 StructLayoutAttribute.CharSet 字段的值。The encoding depends on the value of the StructLayoutAttribute.CharSet field in the System.Runtime.InteropServices.StructLayoutAttribute. 如果未指定任何属性,则编码将默认为 ANSI 编码。If no attribute is specified, the encoding defaults to an ANSI encoding.

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

如果需要对不同字段使用不同编码或只是希望在结构定义中更加明确,则可以在 System.Runtime.InteropServices.MarshalAsAttribute 属性上使用 UnmanagedType.LPStrUnmanagedType.LPWStr 值。If you need to use different encodings for different fields or just prefer to be more explicit in your struct definition, you can use the UnmanagedType.LPStr or UnmanagedType.LPWStr values on a System.Runtime.InteropServices.MarshalAsAttribute attribute.

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

如果想要使用 UTF-8 编码封送字符串,则可以在 MarshalAsAttribute 中使用 UnmanagedType.LPUTF8Str 值。If you want to marshal your strings using the UTF-8 encoding, you can use the UnmanagedType.LPUTF8Str value in your MarshalAsAttribute.

public struct UTF8String
{
    [MarshalAs(UnmanagedType.LPUTF8Str)]
    public string str;
}
struct UTF8String
{
    char* str;
};

备注

使用 UnmanagedType.LPUTF8Str 需要 .NET Framework 4.7(或更高版本)或 .NET Core 1.1(或更高版本)。Using UnmanagedType.LPUTF8Str requires either .NET Framework 4.7 (or later versions) or .NET Core 1.1 (or later versions). 不能在 .NET Standard 2.0 中使用。It isn't available in .NET Standard 2.0.

如果使用 COM API,则可能需要将字符串作为 BSTR 进行封送。If you're working with COM APIs, you may need to marshal a string as a BSTR. 使用 UnmanagedType.BStr 值可以将字符串作为 BSTR 进行封送。Using the UnmanagedType.BStr value, you can marshal a string as a BSTR.

public struct BString
{
    [MarshalAs(UnmanagedType.BStr)]
    public string str;
}
struct BString
{
    BSTR str;
};

使用基于 WinRT 的 API 时,可能需要将字符串作为 HSTRING 进行封送。When using a WinRT-based API, you may need to marshal a string as an HSTRING. 使用 UnmanagedType.HString 值可以将字符串作为 HSTRING 进行封送。Using the UnmanagedType.HString value, you can marshal a string as a HSTRING.

public struct HString
{
    [MarshalAs(UnmanagedType.HString)]
    public string str;
}
struct BString
{
    HSTRING str;
};

如果 API 要求你将字符串就地传入结构,则可以使用 UnmanagedType.ByValTStr 值。If your API requires you to pass the string in-place in the structure, you can use the UnmanagedType.ByValTStr value. 务必注意,通过 ByValTStr 封送的字符串的编码由 CharSet 属性确定。Do note that the encoding for a string marshaled by ByValTStr is determined from the CharSet attribute. 此外,还需要通过 MarshalAsAttribute.SizeConst 字段传递字符串长度。Additionally, it requires that a string length is passed by the MarshalAsAttribute.SizeConst field.

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

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

如果在 Windows 上操作,则可能会遇到一些使用本机 CYCURRENCY 结构的 API。If you're working on Windows, you might encounter some APIs that use the native CY or CURRENCY structure. 默认情况下,.NET decimal 类型会封送到本机 DECIMAL 结构。By default, the .NET decimal type marshals to the native DECIMAL structure. 但是,可以使用包含 UnmanagedType.Currency 值的 MarshalAsAttribute 来指示封送处理程序将 decimal 值转换为本机 CY 值。However, you can use a MarshalAsAttribute with the UnmanagedType.Currency value to instruct the marshaler to convert a decimal value to a native CY value.

public struct Currency
{
    [MarshalAs(UnmanagedType.Currency)]
    public decimal dec;
}
struct Currency
{
    CY dec;
};

封送 System.ObjectMarshal System.Object

在 Windows 上,可以将类型为 object 的字段封送到本机代码。On Windows, you can marshal object-typed fields to native code. 可以将这些字段封送到以下三个类型之一:You can marshal these fields to one of three types:

默认情况下,将类型为 object 的字段封送到用来包装对象的 IUnknown*By default, an object-typed field will be marshaled to an IUnknown* that wraps the object.

public struct ObjectDefault
{
    public object obj;
}
struct ObjectDefault
{
    IUnknown* obj;
};

如果要将对象字段封送到 IDispatch*,请添加包含 UnmanagedType.IDispatch 值的 MarshalAsAttributeIf you want to marshal an object field to an IDispatch*, add a MarshalAsAttribute with the UnmanagedType.IDispatch value.

public struct ObjectDispatch
{
    [MarshalAs(UnmanagedType.IDispatch)]
    public object obj;
}
struct ObjectDispatch
{
    IDispatch* obj;
};

如果要将其作为 VARIANT 进行封送,请添加包含 UnmanagedType.Struct 值的 MarshalAsAttributeIf you want to marshal it as a VARIANT, add a MarshalAsAttribute with the UnmanagedType.Struct value.

public struct ObjectVariant
{
    [MarshalAs(UnmanagedType.Struct)]
    public object obj;
}
struct ObjectVariant
{
    VARIANT obj;
};

下表介绍了如何将 obj 字段的不同运行时类型映射到存储在 VARIANT 中的各种类型:The following table describes how different runtime types of the obj field map to the various types stored in a VARIANT:

.NET 类型.NET Type VARIANT 类型VARIANT Type .NET 类型.NET Type VARIANT 类型VARIANT Type
byte VT_UI1 System.Runtime.InteropServices.BStrWrapper VT_BSTR
sbyte VT_I1 object VT_DISPATCH
short VT_I2 System.Runtime.InteropServices.UnknownWrapper VT_UNKNOWN
ushort VT_UI2 System.Runtime.InteropServices.DispatchWrapper VT_DISPATCH
int VT_I4 System.Reflection.Missing VT_ERROR
uint VT_UI4 (object)null VT_EMPTY
long VT_I8 bool VT_BOOL
ulong VT_UI8 System.DateTime VT_DATE
float VT_R4 decimal VT_DECIMAL
double VT_R8 System.Runtime.InteropServices.CurrencyWrapper VT_CURRENCY
char VT_UI2 System.DBNull VT_NULL
string VT_BSTR