Anpassen des Marshallings für Strukturen

Manchmal sind die Standardregeln für das Marshalling von Strukturen nicht genau das, was Sie benötigen. Die .NET-Runtimes bieten Ihnen einige Erweiterungsmöglichkeiten, mit denen Sie das Layout Ihrer Struktur und das Marshalling von Feldern anpassen können. Das Anpassen des Strukturlayouts wird für alle Szenarien unterstützt, das Anpassen des Feld-Marshallings jedoch nur für Szenarien, in denen das Laufzeitmarshalling aktiviert ist. Wenn Runtime-Marshalling deaktiviert ist, muss jegliches Feld-Marshalling manuell erfolgen.

Hinweis

Dieser Artikel behandelt nicht das Anpassen des Marshallings für quellgenerierte Interop. Wenn Sie quellgenerierte Interoperabilität für P/Invokes oder COM verwenden, finden Sie weitere Informationen unter Anpassen des Marshallings.

Layout der Struktur anpassen

.NET stellt das System.Runtime.InteropServices.StructLayoutAttribute Attribut und die System.Runtime.InteropServices.LayoutKind Enumeration bereit, damit Sie anpassen können, wie Felder im Arbeitsspeicher platziert werden. Die folgenden Anleitungen helfen Ihnen, häufige Probleme zu vermeiden.

✔️ Verwenden Sie nach Möglichkeit LayoutKind.Sequential.

✔️ VERWENDEN Sie beim Marshallen nur dann LayoutKind.Explicit, wenn Ihre native Struktur auch ein explizites Layout aufweist (beispielsweise bei einer Union).

❌ VERMEIDEN Sie die Verwendung von Klassen, um komplexe native Typen durch Vererbung auszudrücken.

❌VERMEIDEN Sie die Verwendung LayoutKind.Explicit beim Marshalling von Strukturen auf Nicht-Windows-Plattformen, wenn Sie Laufzeiten vor .NET Core 3.0 als Ziel festlegen müssen. Die .NET Core-Runtime vor 3.0 unterstützt nicht das Übergeben expliziter Strukturen nach Wert an systemeigene Funktionen auf Intel- oder AMD 64-Bit-Nicht-Windows-Systemen. Die Runtime unterstützt jedoch auf allen Plattformen die Übergabe expliziter Strukturen per Verweis.

Puffer mit fester Größe und Inlinearrays

Wenn Sie einen Puffer mit fester Größe in einer Struktur definieren müssen, verwenden Sie die StructLayoutAttribute.Size Eigenschaft nicht, um zusätzlichen Speicherplatz zuzuweisen. Die Size-Eigenschaft steuert das nicht verwaltete Layout des Typs (das layout, das beim Aufrufen Marshal.StructureToPtr verwendet wird) und wirkt sich nur auf das verwaltete Layout für blittbare Typen aus. Bei nicht-blittbaren Typen reserviert die Laufzeit Speicherplatz nur anhand der tatsächlich vorhandenen Felder, was zu Speicherkorruption führen kann, wenn Sie versuchen, den zusätzlichen Speicherplatz zu verwenden.

❌ VERMEIDEN Sie die Verwendung StructLayoutAttribute.Size zum Erstellen von Puffern mit fester Größe in verwalteten Typen.

✔️ Verwenden Sie System.Runtime.CompilerServices.InlineArrayAttribute, um Puffer mit fester Größe in modernem .NET (C# 12 und höher) zu definieren.

Das folgende Beispiel zeigt den falschen Ansatz, der zu Speicherbeschädigungen führen kann:

// ❌ 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
        }
    }
}

Verwenden Sie stattdessen InlineArrayAttribute, um einen Puffer in der richtigen Größe zu erstellen:

// ✔️ 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
        }
    }
}

Weitere Informationen zu Inlinearrays finden Sie in der C#-Sprachreferenz.

Anpassen des Marshallings eines booleschen Felds

Nativer Code hat viele verschiedene Boolesche Repräsentationen. Allein auf Windows gibt es drei Möglichkeiten, boolesche Werte darzustellen. Die Laufzeitumgebung kennt die native Definition Ihrer Struktur nicht, daher kann sie bestenfalls nur vermuten, wie Ihre Booleschen Werte zu marshallen sind. Die .NET-Laufzeit bietet eine Möglichkeit anzugeben, wie Ihr Boolean-Feld marshalled werden soll. Die folgenden Beispiele zeigen, wie .NET bool in verschiedene native boolesche Typen gemarshallt wird.

Boolesche Werte werden standardmäßig als nativer Win32-BOOL-Wert mit 4 Byte gemarshallt, wie im folgenden Beispiel gezeigt:

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

Wenn Sie explizit sein möchten, können Sie den UnmanagedType.Bool Wert verwenden, um das gleiche Verhalten wie oben zu erhalten:

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

Mithilfe der unten stehenden Werte UnmanagedType.U1 oder UnmanagedType.I1 können Sie die Laufzeit anweisen, das Feld b als nativen bool-Typ mit 1 Byte zu marshallen.

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

In Windows können Sie den UnmanagedType.VariantBool Wert verwenden, um die Laufzeit anweisen, ihren booleschen Wert in einen 2-Byte-Wert VARIANT_BOOL zu marshallen:

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

Hinweis

VARIANT_BOOL unterscheidet sich von den meisten bool-Typen dadurch, dass VARIANT_TRUE = -1 und VARIANT_FALSE = 0. Darüber hinaus werden alle Werte, die nicht gleich VARIANT_TRUE sind, als "false" betrachtet.

Anpassen des Marshallings von Arrayfeldern

.NET enthält auch einige Möglichkeiten zum Anpassen von Array-Marshalling.

In der Standardeinstellung marshallt .NET Arrays als Zeiger auf eine zusammenhängende Liste der Elemente:

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

Wenn Sie mit COM-APIs arbeiten, müssen Sie Arrays möglicherweise als SAFEARRAY*-Objekte übergeben. Sie können den Wert System.Runtime.InteropServices.MarshalAsAttribute und den Wert UnmanagedType.SafeArray verwenden, um die Laufzeit anzuweisen, ein Array als SAFEARRAY* zu marshallen:

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

Wenn Sie anpassen müssen, welcher Elementtyp im SAFEARRAY enthalten ist, können Sie die Felder MarshalAsAttribute.SafeArraySubType und MarshalAsAttribute.SafeArrayUserDefinedSubType verwenden, um den genauen Elementtyp des SAFEARRAY anzupassen.

Wenn Sie das Array direkt marshallen möchten, können Sie den Marshaller über den Wert UnmanagedType.ByValArray entsprechend anweisen. Wenn Sie dieses Marshalling verwenden, müssen Sie auch einen Wert für das Feld MarshalAsAttribute.SizeConst mit der Anzahl der Elemente im Array angeben, damit die Laufzeit den Speicherplatz für die Struktur korrekt reservieren kann.

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

Hinweis

.NET unterstützt das Marshallen eines Arrayfelds mit variabler Länge nicht als flexibles Arraymembelement C99.

Anpassen des Marshallings von Zeichenfolgenfeldern

.NET bietet auch eine Vielzahl von Anpassungsmöglichkeiten für das Marshalling von Zeichenfolgenfeldern.

In der Standardeinstellung marshallt .NET eine Zeichenfolge als Zeiger auf eine mit NULL beendete Zeichenfolge. Die Codierung hängt vom Wert des Felds StructLayoutAttribute.CharSet in der System.Runtime.InteropServices.StructLayoutAttribute ab. Wenn kein Attribut angegeben ist, wird standardmäßig eine ANSI-Codierung verwendet.

[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.
};

Wenn Sie unterschiedliche Kodierungen für verschiedene Felder verwenden müssen oder Ihre Strukturdefinition einfach expliziter gestalten möchten, können Sie die Werte UnmanagedType.LPStr oder UnmanagedType.LPWStr in einem System.Runtime.InteropServices.MarshalAsAttribute-Attribut verwenden.

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.
};

Wenn Sie Ihre Zeichenfolgen mit der UTF-8-Codierung marshallen möchten, können Sie den Wert UnmanagedType.LPUTF8Str in Ihrem MarshalAsAttribute verwenden.

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

Hinweis

Die Verwendung UnmanagedType.LPUTF8Str erfordert entweder .NET Framework 4.7 (oder höher) oder .NET Core 1.1 (oder höher). Sie ist in .NET Standard 2.0 nicht verfügbar.

Wenn Sie mit COM-APIs arbeiten, müssen Sie eine Zeichenfolge möglicherweise als BSTR marshallen. Mit dem Wert UnmanagedType.BStr können Sie eine Zeichenfolge als BSTR marshalen.

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

Wenn Sie eine WinRT-basierte API verwenden, müssen Sie eine Zeichenfolge möglicherweise als HSTRING marshallen. Mit dem Wert UnmanagedType.HString können Sie eine Zeichenfolge als HSTRING marshallen. HSTRING-Marshallen wird nur für Runtimes mit integrierter WinRT-Unterstützung unterstützt. WinRT-Unterstützung wurde in .NET 5 entfernt, sodass HSTRING Marshalling in .NET 5 oder höher nicht unterstützt wird.

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

Wenn Ihre API verlangt, dass Sie die Zeichenfolge direkt in der Struktur übergeben, können Sie den Wert UnmanagedType.ByValTStr verwenden. Beachten Sie, dass die Codierung für eine Zeichenfolge, die mit ByValTStr gemarshallt wird, durch das Attribut CharSet festgelegt wird. Darüber hinaus muss über das Feld MarshalAsAttribute.SizeConst die Länge einer Zeichenfolge übergeben werden.

[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.
};

Anpassen des Marshallings von Dezimalfeldern

Wenn Sie an Windows arbeiten, treten möglicherweise einige APIs auf, die die systemeigene CY Oder CURRENCY Struktur verwenden. Standardmäßig marshallt der .NET-decimal-Typ in die native DECIMAL-Struktur. Sie können jedoch einen MarshalAsAttribute Wert mit dem UnmanagedType.Currency Wert verwenden, um den Marshaller anzuweisen, einen decimal Wert in einen systemeigenen CY Wert zu konvertieren.

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

Gewerkschaften

Eine Union ist ein Datentyp, der unterschiedliche Datentypen enthalten kann, die über demselben Speicher verfügen. Es ist eine gängige Form von Daten in der Sprache C. Eine Union kann in .NET mit LayoutKind.Explicit dargestellt werden. Es wird empfohlen, beim Definieren einer Union in .NET Strukturen zu verwenden. Die Verwendung von Klassen kann zu Layoutproblemen führen und zu unvorhersehbaren Verhaltensweisen führen.

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;
    }
}

Marshal System.Object

Unter Windows können Sie Felder vom Typ object in nativen Code marshallen. Sie können diese Felder in einen von drei Typen umwandeln:

Standardmäßig wird ein Feld vom Typ object in ein IUnknown*-Objekt marshalled, das das Objekt kapselt.

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

Wenn Sie ein Objektfeld in IDispatch* marshallen möchten, fügen Sie ein MarshalAsAttribute mit dem Wert UnmanagedType.IDispatch hinzu.

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

Wenn Sie es als ein VARIANT marshallen möchten, fügen Sie ein MarshalAsAttribute mit dem Wert UnmanagedType.Struct hinzu.

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

In der folgenden Tabelle wird beschrieben, wie verschiedene Laufzeittypen des Felds obj den verschiedenen Typen zugeordnet werden, die in einer VARIANT gespeichert sind:

.NET-Typ VARIANT-Typ
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