自訂結構封送處理

有時候,結構的預設封送處理規則並不是您所需要的樣子。 .NET 執行階段提供一些延伸點,讓您自訂您結構的配置以及封送處理欄位的方式。 所有案例都支援自訂結構配置,但僅在啟用執行階段封送處理的情況下,才支援自訂欄位封送處理。 如果停用執行階段封送處理,則必須手動完成任何欄位封送處理。

注意

本文未涵蓋針對來源產生的 Interop 自訂封送處理。 如果您針對 P/Invokes 或 COM 使用來源產生的 Interop,請參閱自訂封送處理

自訂結構配置

.NET 提供 System.Runtime.InteropServices.StructLayoutAttribute 屬性與 System.Runtime.InteropServices.LayoutKind 列舉,讓您可以自訂欄位在記憶體中的放置方式。 下列指導方針將可協助您避免常見問題。

✔️ 考慮使用 LayoutKind.Sequential (若可行)。

✔️ 務必只在封送處理中使用 LayoutKind.Explicit (當您的原生結構也有明確的配置 (例如聯集) 時)。

❌ 避免使用類別透過繼承來表示複雜的原生類型。

❌ 避免使用 LayoutKind.Explicit (當您需要在 .NET Core 3.0 之前以執行階段為目標,而在非 Windows 平台上封送處理結構時)。 .NET Core 3.0 以前的執行階段不支援以傳值方式傳遞明確結構至 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 中的大部分布林值類型不同。 此外,不等於 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.StructLayoutAttributeStructLayoutAttribute.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。 只有具有內建 WinRT 支援的執行階段才支援 HSTRING 封送處理。 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。 根據預設,.NET decimal 類型會封送處理為原生 DECIMAL 結構。 不過,您可以使用 MarshalAsAttribute 搭配 UnmanagedType.Currency 值來指示封送處理器將 decimal 值轉換為原生 CY 值。

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

等位

聯集是一種資料類型,可以包含相同記憶體上不同類型的資料。 這是 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