Anpassen des Marshallings für Strukturen

Manchmal entsprechen die Standardregeln für das Marshallen nicht genau Ihren Anforderungen. Die .NET-Runtimes bieten einige Erweiterungspunkte, mit denen Sie das Layout der Struktur und das Marshallen von Feldern anpassen können. Das Anpassen des Strukturlayouts wird für alle Szenarien unterstützt. Das Anpassen des Marshallens von Feldern wird dagegen nur für Szenarien mit aktiviertem Runtime-Marshalling unterstützt. Ist das Runtime-Marshalling deaktiviert, muss das Marshallen von Feldern manuell durchgeführt werden.

Hinweis

Dieser Artikel befasst sich nicht mit der Anpassung des Marshallings für quellgenerierte Interoperabilität. Wenn Sie die von der Quelle generierte Interoperabilität für P/Invokes oder COM verwenden, lesen Sie Anpassen des Marshallings.

Strukturlayout anpassen

.NET bietet das System.Runtime.InteropServices.StructLayoutAttribute-Attribut und die System.Runtime.InteropServices.LayoutKind-Enumeration, damit Sie anpassen können, wie Felder im Arbeitsspeicher abgelegt werden. Die folgende Anleitung hilft Ihnen dabei, gängige Probleme zu vermeiden.

✔️ ZIEHEN Sie nach Möglichkeit die Verwendung von LayoutKind.Sequential in Betracht.

✔️ 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 von LayoutKind.Explicit beim Marshallen von Strukturen auf Windows-fremden Plattformen, wenn Sie Runtimes vor .NET Core 3.0 verwenden müssen. Vor Version 3.0 bietet die .NET Core-Runtime keine Unterstützung für die Übergabe expliziter Strukturen per Wert an native Funktionen auf Intel- oder AMD-64-Bit-Nicht-Windows-Systemen. Die Runtime unterstützt jedoch auf allen Plattformen die Übergabe expliziter Strukturen per Verweis.

Anpassen des Marshallens boolescher Felder

Nativer Code verfügt über viele verschiedene boolesche Darstellungen. Allein unter Windows gibt es drei Möglichkeiten zur Darstellung boolescher Werte. Der Runtime ist die native Definition Ihrer Struktur nicht bekannt, daher kann sie allenfalls raten, wie Ihre booleschen Werte gemarshallt werden sollen. Die .NET-Runtime bietet eine Möglichkeit anzugeben, wie Ihr boolesches Feld gemarshallt werden soll. Die folgenden Beispiele zeigen, wie .NET bool in andere 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 eine explizite Festlegung treffen möchten, können Sie mit dem Wert UnmanagedType.Bool dasselbe Verhalten wie oben erzielen:

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

Mit den unten gezeigten Werten UnmanagedType.U1 oder UnmanagedType.I1 können Sie die Runtime anweisen, das b-Feld als nativen 1-Byte-bool-Typ zu marshallen.

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

Unter Windows können Sie die Runtime mit dem Wert UnmanagedType.VariantBool anweisen, Ihren booleschen Wert in einen 2-Byte-VARIANT_BOOL-Wert 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 booleschen Typen insofern als dass VARIANT_TRUE = -1 und VARIANT_FALSE = 0 sind. Darüber hinaus werden alle Werte als FALSE betrachtet, die nicht gleich VARIANT_TRUE sind.

Anpassen des Marshallens von Arrayfeldern

.NET umfasst auch einige Möglichkeiten zum Anpassen des Marshallens von Arrays.

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 marshallen. Mit den Werten System.Runtime.InteropServices.MarshalAsAttribute und UnmanagedType.SafeArray können Sie die Runtime anweisen, ein Array als SAFEARRAY* zu marshallen:

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

Wenn Sie anpassen möchten, welche Art von Element in SAFEARRAY enthalten ist, können Sie mithilfe der Felder MarshalAsAttribute.SafeArraySubType und MarshalAsAttribute.SafeArrayUserDefinedSubType den genauen Elementtyp von SAFEARRAY anpassen.

Wenn Sie das Array direkt marshallen möchten, können Sie den Marshaller über den Wert UnmanagedType.ByValArray entsprechend anweisen. Wenn Sie diese Art des Marshallens nutzen, müssen Sie für das MarshalAsAttribute.SizeConst-Feld auch einen Wert für die Anzahl von Elementen im Array angeben, damit die Runtime die richtige Speichermenge für die Struktur zuweisen kann.

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

Hinweis

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

Anpassen des Marshallens von Zeichenfolgenfeldern

.NET bietet auch eine Vielzahl von Anpassungen für das Marshallen von Zeichenfolgenfeldern.

In der Standardeinstellung marshallt .NET eine Zeichenfolge als Zeiger auf eine mit NULL beendete Zeichenfolge. Die Codierung hängt von dem Wert des Felds StructLayoutAttribute.CharSet im System.Runtime.InteropServices.StructLayoutAttribute ab. Wenn kein Attribut angegeben ist, wird als Codierung standardmäßig die 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 Codierungen für verschiedene Felder verwenden oder Ihre Strukturdefinition einfach expliziter gestalten möchten, können Sie die Werte UnmanagedType.LPStr oder UnmanagedType.LPWStr für ein 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

Für die Verwendung von UnmanagedType.LPUTF8Str ist entweder .NET Framework 4.7 (oder höher) oder .NET Core 1.1 (oder höher) erforderlich. In .NET Standard 2.0 ist diese Option 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 marshallen.

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. Die WinRT-Unterstützung wurde in .NET 5 entfernt, sodass HSTRING-Marshallen ab .NET 5 nicht mehr unterstützt wird.

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

Wenn Ihre API verlangt, dass die Zeichenfolge direkt in der Struktur übergeben werden muss, 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 eine Zeichenfolgenlänge ü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 Marshallens von Dezimalfeldern

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

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

Unions

Eine Union ist ein Datentyp, der verschiedene Datentypen enthalten kann, die auf dem gleichen Arbeitsspeicher basieren. Es handelt sich um eine gängige Form von Daten in der Programmiersprache C. Eine Union kann in .NET mithilfe von LayoutKind.Explicit ausgedrückt werden. Beim Definieren einer Union in .NET empfiehlt sich die Verwendung von Strukturen. Die Verwendung von Klassen kann Layoutprobleme verursachen und zu unvorhersehbarem Verhalten 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;
    }
}

Marshallen von System.Object

Unter Windows können Sie Felder vom Typ object in nativen Code marshallen. Diese Felder können in einen der drei folgenden Typen gemarshallt werden:

Standardmäßig wird ein Feld vom Typ object in ein IUnknown*-Feld gemarshallt, das das Objekt umschließt.

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 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 Runtimetypen des Felds obj den verschiedenen Typen zugeordnet werden können, die in 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