自定义结构封送

有时,结构体的默认封送处理规则未必完全符合你的需求。 .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.U1UnmanagedType.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 = -1VARIANT_FALSE = 0. 中的大多数布尔类型不同。 此外,不等于 VARIANT_TRUE 的所有值都被视为 false。

自定义数组字段封送

.NET还包括一些自定义数组封送的方法。

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

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

如果要与 COM API 交互,可能需要将数组封送为 SAFEARRAY* 对象。 可以使用 System.Runtime.InteropServices.MarshalAsAttributeUnmanagedType.SafeArray 值来告诉运行时将数组封送为 SAFEARRAY*

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

如果需要自定义元素的类型 SAFEARRAY,则可以使用 MarshalAsAttribute.SafeArraySubTypeMarshalAsAttribute.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.LPStrUnmanagedType.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 值,可以将字符串封送为 HSTRINGHSTRING 封送处理仅支持在具有内置 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 上开发,可能会遇到一些使用原生 CYCURRENCY 结构的 API。 默认情况下,.NETdecimal类型封送到本机DECIMAL结构。 但是,可以将 MarshalAsAttributeUnmanagedType.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.IDispatchMarshalAsAttribute

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

如果要将其封送为 VARIANT,请添加一个值为 UnmanagedType.StructMarshalAsAttribute

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