有时,结构体的默认封送处理规则未必完全符合你的需求。 .NET 运行时提供了几个扩展点,供你自定义结构体的布局以及字段的封送方式。 所有场景都支持自定义结构体布局,但自定义字段封送仅在启用运行时封送的场景中受支持。 如果 禁用运行时封送,则必须手动执行任何字段封送。
注释
本文不介绍如何为源生成的互操作自定义封送。 如果你对 P/Invoke 使用源生成的互操作 或 COM,请参阅 自定义封送处理。
自定义结构布局
.NET提供System.Runtime.InteropServices.StructLayoutAttribute属性和System.Runtime.InteropServices.LayoutKind枚举,用于自定义字段在内存中的放置方式。 以下指南将帮助你避免常见问题。
✔️ 请考虑尽可能使用 LayoutKind.Sequential 。
✔️ 仅当本机结构体也具有显式布局(例如联合体)时,才在封送处理中使用 LayoutKind.Explicit。
❌ 避免使用类通过继承来表达复杂的原生类型。
❌如果需要以 .NET Core 3.0 之前的运行时为目标,请避免在非 Windows 平台上封送结构时使用 LayoutKind.Explicit。 3.0 之前的 .NET Core 运行时不支持通过值将显式结构传递到 Intel 或 AMD 64 位非Windows系统上的本机函数。 但是,运行时支持在所有平台上通过引用传递显式结构。
固定大小的缓冲区和内联数组
如果需要在结构中定义固定大小的缓冲区,请不要使用该 StructLayoutAttribute.Size 属性分配额外的空间。 该 Size 属性控制类型的非托管布局(调用 Marshal.StructureToPtr时使用的布局),并且仅影响 blittable 类型的托管布局。 对于非 blittable 类型,运行时仅按实际字段分配空间,因此如果你试图使用这部分额外空间,可能会导致内存损坏。
❌ 避免使用 StructLayoutAttribute.Size 在托管类型中创建固定大小的缓冲区。
✔️ 在现代 .NET(C# 12 及更高版本)中,务必使用 System.Runtime.CompilerServices.InlineArrayAttribute 来定义固定大小缓冲区。
以下示例显示了可能导致内存损坏的错误方法:
// ❌ DON'T: This is dangerous and can corrupt memory
[StructLayout(LayoutKind.Explicit, Size = 100)]
class Bad100ByteBuffer
{
[FieldOffset(0)]
private byte b;
unsafe void UseBuffer()
{
fixed (byte* p = &b)
{
// Writing beyond the first byte corrupts memory
}
}
}
相反,应使用 InlineArrayAttribute 创建大小合适的缓冲区:
// ✔️ DO: Use InlineArrayAttribute for fixed-size buffers
class Good100ByteBuffer
{
[InlineArray(100)]
private struct Buffer100
{
private byte _element0;
}
private Buffer100 buffer;
unsafe void UseBuffer()
{
fixed (byte* p = &buffer[0])
{
// The buffer is guaranteed to be 100 bytes
}
}
}
有关内联数组的详细信息,请参阅 C# 语言参考。
自定义布尔字段封送
原生代码有多种不同的布尔表示方式。 单独Windows,有三种方法来表示布尔值。 运行时并不知道你的结构体在本机端的定义,因此它充其量只能猜测应如何封送你的布尔值。 .NET运行时提供了一种方法来指示如何封送布尔字段。 以下示例演示如何将.NETbool封送到不同的本机布尔类型。
布尔值默认作为本机 4 字节 Win32 BOOL 值进行封送,如以下示例所示:
public struct WinBool
{
public bool b;
}
struct WinBool
{
public BOOL b;
};
如果想明确指定,可以使用 UnmanagedType.Bool 值来获得与上述相同的行为:
public struct WinBool
{
[MarshalAs(UnmanagedType.Bool)]
public bool b;
}
struct WinBool
{
public BOOL b;
};
使用下面的 UnmanagedType.U1 或 UnmanagedType.I1 值,可以指定运行时将 b 字段封送为 1 字节的本机 bool 类型。
public struct CBool
{
[MarshalAs(UnmanagedType.U1)]
public bool b;
}
struct CBool
{
public bool b;
};
在 Windows 上,可以使用 UnmanagedType.VariantBool 值来告知运行时将布尔值封送处理为一个 2 字节的 VARIANT_BOOL 值:
public struct VariantBool
{
[MarshalAs(UnmanagedType.VariantBool)]
public bool b;
}
struct VariantBool
{
public VARIANT_BOOL b;
};
注释
VARIANT_BOOL 与该 VARIANT_TRUE = -1 和 VARIANT_FALSE = 0. 中的大多数布尔类型不同。 此外,不等于 VARIANT_TRUE 的所有值都被视为 false。
自定义数组字段封送
.NET还包括一些自定义数组封送的方法。
默认情况下,.NET封送数组作为指向元素连续列表的指针:
public struct DefaultArray
{
public int[] values;
}
struct DefaultArray
{
int32_t* values;
};
如果要与 COM API 交互,可能需要将数组封送为 SAFEARRAY* 对象。 可以使用 System.Runtime.InteropServices.MarshalAsAttribute 和 UnmanagedType.SafeArray 值来告诉运行时将数组封送为 SAFEARRAY*:
public struct SafeArrayExample
{
[MarshalAs(UnmanagedType.SafeArray)]
public int[] values;
}
struct SafeArrayExample
{
SAFEARRAY* values;
};
如果需要自定义元素的类型 SAFEARRAY,则可以使用 MarshalAsAttribute.SafeArraySubType 和 MarshalAsAttribute.SafeArrayUserDefinedSubType 字段自定义该 SAFEARRAY元素的确切元素类型。
如果需要就地封送数组,可以使用 UnmanagedType.ByValArray 值来告知封送器就地封送该数组。 使用此封送时,还必须为 MarshalAsAttribute.SizeConst 数组中元素数的字段提供一个值,以便运行时可以为结构正确分配空间。
public struct InPlaceArray
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
public int[] values;
}
struct InPlaceArray
{
int values[4];
};
注释
.NET不支持将可变长度数组字段封送为 C99 灵活数组成员。
自定义字符串字段封送
.NET 还提供了多种用于封送字符串字段的自定义选项。
默认情况下,.NET 将字符串封送为指向以空字符结尾的字符串的指针。 编码取决于 System.Runtime.InteropServices.StructLayoutAttribute 中的 StructLayoutAttribute.CharSet 字段的值。 如果未指定任何属性,则编码默认为 ANSI 编码。
[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.LPStr 或 UnmanagedType.LPWStr 值。
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 值。
public struct UTF8String
{
[MarshalAs(UnmanagedType.LPUTF8Str)]
public string str;
}
struct UTF8String
{
char* str;
};
注释
使用UnmanagedType.LPUTF8Str需要.NET Framework 4.7(或更高版本)或.NET Core 1.1(或更高版本)。 它不适用于标准 2.0 .NET。
如果使用 COM API,可能需要将字符串封送为一个 BSTR。 使用 UnmanagedType.BStr 值,可以将字符串封送为 BSTR。
public struct BString
{
[MarshalAs(UnmanagedType.BStr)]
public string str;
}
struct BString
{
BSTR str;
};
在使用基于 WinRT 的 API 时,可能需要将字符串封送为 HSTRING。 使用 UnmanagedType.HString 值,可以将字符串封送为 HSTRING。
HSTRING 封送处理仅支持在具有内置 WinRT 支持的运行时上进行。 WinRT 支持已在 .NET 5 中移除,因此 .NET 5 及更高版本中不支持HSTRING封送处理。
public struct HString
{
[MarshalAs(UnmanagedType.HString)]
public string str;
}
struct BString
{
HSTRING str;
};
如果 API 要求将字符串直接传入该结构中,则可以使用 UnmanagedType.ByValTStr 值。 请注意,由 ByValTStr 封送处理的字符串的编码由 CharSet 属性确定。 此外,它还要求通过 MarshalAsAttribute.SizeConst 字段传递字符串长度。
[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.
};
自定义十进制字段封送
如果你在 Windows 上开发,可能会遇到一些使用原生 CY 或 CURRENCY 结构的 API。 默认情况下,.NETdecimal类型封送到本机DECIMAL结构。 但是,可以将 MarshalAsAttribute 与 UnmanagedType.Currency 值一起使用,以指示封送器将 decimal 值转换为本机 CY 值。
public struct Currency
{
[MarshalAs(UnmanagedType.Currency)]
public decimal dec;
}
struct Currency
{
CY dec;
};
工会
联合体是一种数据类型,可以在同一块内存中存储不同类型的数据。 它是 C 语言中常见的数据形式。 在 .NET 中,可以使用 LayoutKind.Explicit 表示联合类型。 建议在.NET中定义联合时使用结构。 使用类可能会导致布局问题并产生不可预知的行为。
struct device1_config
{
void* a;
void* b;
void* c;
};
struct device2_config
{
int32_t a;
int32_t b;
};
struct config
{
int32_t type;
union
{
device1_config dev1;
device2_config dev2;
};
};
public unsafe struct Device1Config
{
void* a;
void* b;
void* c;
}
public struct Device2Config
{
int a;
int b;
}
public struct Config
{
public int Type;
public _Union Anonymous;
[StructLayout(LayoutKind.Explicit)]
public struct _Union
{
[FieldOffset(0)]
public Device1Config Dev1;
[FieldOffset(0)]
public Device2Config Dev2;
}
}
封送 System.Object
在 Windows 上,可以将 object 类型的字段封送到本机代码。 可以将这些字段封送为以下三种类型之一:
默认情况下,类型为 object 的字段将被封送为包装该对象的 IUnknown*。
public struct ObjectDefault
{
public object obj;
}
struct ObjectDefault
{
IUnknown* obj;
};
如果要将对象字段封送为 IDispatch*,请添加一个值为 UnmanagedType.IDispatch 的 MarshalAsAttribute。
public struct ObjectDispatch
{
[MarshalAs(UnmanagedType.IDispatch)]
public object obj;
}
struct ObjectDispatch
{
IDispatch* obj;
};
如果要将其封送为 VARIANT,请添加一个值为 UnmanagedType.Struct 的 MarshalAsAttribute。
public struct ObjectVariant
{
[MarshalAs(UnmanagedType.Struct)]
public object obj;
}
struct ObjectVariant
{
VARIANT obj;
};
下表说明了 obj 字段的不同运行时类型如何映射到 VARIANT 中存储的各种类型:
| .NET 类型 | VARIANT 类型 |
|---|---|
byte |
VT_UI1 |
sbyte |
VT_I1 |
short |
VT_I2 |
ushort |
VT_UI2 |
int |
VT_I4 |
uint |
VT_UI4 |
long |
VT_I8 |
ulong |
VT_UI8 |
float |
VT_R4 |
double |
VT_R8 |
char |
VT_UI2 |
string |
VT_BSTR |
System.Runtime.InteropServices.BStrWrapper |
VT_BSTR |
object |
VT_DISPATCH |
System.Runtime.InteropServices.UnknownWrapper |
VT_UNKNOWN |
System.Runtime.InteropServices.DispatchWrapper |
VT_DISPATCH |
System.Reflection.Missing |
VT_ERROR |
(object)null |
VT_EMPTY |
bool |
VT_BOOL |
System.DateTime |
VT_DATE |
decimal |
VT_DECIMAL |
System.Runtime.InteropServices.CurrencyWrapper |
VT_CURRENCY |
System.DBNull |
VT_NULL |