自定义结构封送

有时,结构的默认封送规则无法完全满足要求。 .NET 运行时提供了几个扩展点以自定义结构的布局和字段的封送方式。 所有方案都支持自定义结构布局,但仅支持在启用运行时封送的情况下自定义字段封送。 如果禁用运行时封送,则必须手动完成任何字段封送。

注意

本文不介绍如何为源生成的互操作自定义封送。 如果使用的是 P/Invokes 的源生成互操作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 系统上,按值将显式结构传递到本机函数。 但是,运行时支持在所有平台上按引用传递显式结构。

自定义布尔字段封送

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

布尔值默认作为本机 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_BOOLVARIANT_TRUE = -1VARIANT_FALSE = 0 中的大多数 bool 类型不同。 此外,不等于 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 将字符串作为指向以 null 结尾的字符串的指针进行封送。 编码取决于 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(或更高版本)。 不能在 .NET Standard 2.0 中使用。

如果使用 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 支持的运行时上支持封送。 .NET 5 中删除了 WinRT 支持,HSTRING因此 .NET 5 或更高版本不支持封送。

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。 默认情况下,.NET decimal 类型会封送到本机 DECIMAL 结构。 但是,可以使用包含 UnmanagedType.Currency 值的 MarshalAsAttribute 来指示封送处理程序将 decimal 值转换为本机 CY 值。

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

Unions

联合是一种数据类型,可以在同一内存中包含不同类型的数据。 它是 C 语言中常见的数据形式。 可以使用 LayoutKind.Explicit 在 .NET 中表示联合。 建议在 .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