Freigeben über


Bewährte Methoden für native Interoperabilität

.NET bietet verschiedene Möglichkeiten zum Anpassen Ihres Codes für die native Interoperabilität. Dieser Artikel bietet einen Leitfaden, den die .NET-Teams von Microsoft in Bezug auf die native Interoperabilität befolgen.

Allgemeine Anleitung

Die Anleitungen in diesem Abschnitt gelten für alle Interoperabilitätsszenarien.

  • ✔️ Verwenden Sie [LibraryImport], falls möglich, bei Verwendung von .NET 7+ .
    • Es gibt Fälle, in denen [DllImport] verwendet wird. Eine Codeanalyse mit ID SYSLIB1054 teilt Ihnen mit, wann dies der Fall ist.
  • ✔️ VERWENDEN Sie die gleichen Benennungen und die gleiche Groß- und Kleinschreibung für Ihre Methoden und Parameter wie die native Methode, die Sie aufrufen möchten.
  • ✔️ ERWÄGEN Sie die Verwendung der gleichen Benennungen und der gleichen Groß- und Kleinschreibung für Werte von Konstanten.
  • ✔️ VERWENDEN Sie .NET-Typen, die dem nativen Typ am ähnlichsten sind. Wenn z.B. der native Typ in C# unsigned int ist, verwenden Sie uint.
  • ✔️ DRÜCKEN Sie native Typen auf höherer Ebene mit .NET-Strukturen statt Klassen aus.
  • ✔️ Verwenden Sie lieber Funktionszeiger als Delegate-Typen, wenn Sie Rückrufe an nicht verwaltete Funktionen in C# übergeben.
  • ✔️ VERWENDEN Sie [In]- und [Out]-Attribute für Arrayparameter.
  • ✔️ VERWENDEN Sie nur [In]- und [Out]-Attribute für andere Typen, wenn sich das gewünschte Verhalten vom Standardverhalten unterscheidet.
  • ✔️ ERWÄGEN Sie die Verwendung von System.Buffers.ArrayPool<T>, um Ihre nativen Arraypuffer in einem Pool zusammenzufassen.
  • ✔️ ERWÄGEN Sie eine Umschließung Ihrer P/Invoke-Deklarationen in einer Klasse mit dem gleichen Namen und der gleichen Groß- und Kleinschreibung wie in Ihrer nativen Bibliothek.
    • Damit können Ihre [LibraryImport]- oder [DllImport]-Attribute das C# nameof-Sprachfeature verwenden, um den Namen der nativen Bibliothek zu übergeben, und so sicherstellen, dass der Name der nativen Bibliothek nicht falsch geschrieben wurde.
  • ✔️ VERWENDEN Sie SafeHandle-Handles, um die Lebensdauer von Objekten zu verwalten, die nicht verwaltete Ressourcen kapseln. Weitere Informationen finden Sie unter Bereinigen von nicht verwalteten Ressourcen.
  • ❌ VERMEIDEN Sie Finalizer, um die Lebensdauer von Objekten zu verwalten, die nicht verwaltete Ressourcen kapseln. Weitere Informationen finden Sie unter Implementieren einer Dispose-Methode.

LibraryImport-Attributeinstellungen

Ein Codeanalysator mit ID SYSLIB1054 hilft Ihnen bei LibraryImportAttribute. In den meisten Fällen erfordert die Verwendung von LibraryImportAttribute eine explizite Deklaration, anstatt auf Standardeinstellungen zu vertrauen. Dieses Design ist beabsichtigt und trägt dazu bei, unbeabsichtigtes Verhalten in Interopszenarien zu vermeiden.

Attributeinstellungen für „DllImport“

Einstellung Standard Empfehlung Details
PreserveSig true Standard beibehalten Wenn diese Einstellung explizit auf „false“ festgelegt wird, werden fehlerhafte HRESULT-Rückgabewerte zu Ausnahmen umgewandelt (und der Rückgabewert in der Definition wird dadurch NULL).
SetLastError false Abhängig von der API Legen Sie diese Einstellung auf „true“ fest, wenn die API „GetLastError“ verwendet, und verwenden Sie „Marshal.GetLastWin32Error“, um den Wert abzurufen. Wenn die API eine Bedingung festlegt, die besagt, dass ein Fehler vorliegt, rufen Sie den Fehler ab, bevor Sie weitere Aufrufe senden, um ein versehentliches Überschreiben zu verhindern.
CharSet Vom Compiler definiert (angegeben in der Zeichensatzdokumentation) Explizit CharSet.Unicode oder CharSet.Ansi verwenden, wenn in der Definition Zeichenfolgen oder Zeichen vorhanden sind Damit wird das Marshallingverhalten von Zeichenfolgen angegeben und festgelegt, was ExactSpelling bei false ausführt. Beachten Sie, dass CharSet.Ansi tatsächlich UTF8 oder Unix ist. In den meisten Fällen verwendet Windows Unicode, Unix verwendet UTF8. Weitere Informationen finden Sie in der Dokumentation zu Zeichensätzen.
ExactSpelling false true Legen Sie diesen Wert auf „true“ fest, um einen leichten Leistungsvorteil zu erzielen, da die Runtime nicht nach alternativen Funktionsnamen mit dem Suffix „A“ oder „W“ sucht, je nach Wert der Einstellung von CharSet („A“ für CharSet.Ansi und „W“ für CharSet.Unicode).

Zeichenfolgenparameter

Ein string wird gepinnt und direkt von nativem Code verwendet (und nicht kopiert), wenn es als Wert (nicht ref oder out) übergeben wird und eine der folgenden Angaben enthält:

Parameter ❌NICHT verwenden[Out] string. Zeichenfolgenparameter, die per Wert mit dem [Out]-Attribut übergeben werden, können die Runtime destabilisieren, wenn die Zeichenfolge internalisiert ist. Weitere Informationen zum Internalisieren von Zeichenfolgen finden Sie in der Dokumentation zu String.Intern.

✔️ ERWÄGEN Sie die Verwendung von char[]- oder byte[]-Arrays aus einem ArrayPool, wenn nativer Code zum Füllen eines Zeichenpuffers erwartet wird. Dazu muss das Argument als [Out] übergeben werden.

DllImport-spezifische Anleitung

✔️ ERWÄGEN Sie, die Eigenschaft CharSet in [DllImport] festzulegen, damit die Runtime die erwartete Zeichenfolgencodierung kennt.

✔️ ERWÄGEN Sie die Vermeidung von StringBuilder-Parametern. StringBuilder -Marshalling erzeugt immer eine native Pufferkopie. Dies kann extrem ineffizient sein. Sehen Sie sich das folgende typische Szenario an, in dem eine Windows-API aufgerufen wird, die eine Zeichenfolge akzeptiert:

  1. Erstellen Sie einen StringBuilder mit der gewünschten Kapazität (ordnet die verwaltete Kapazität zu) {1}.
  2. Aufrufen:
    1. Ordnet einen nativen Puffer zu {2}.
    2. Kopiert den Inhalt, wenn [In] (Standard für einen StringBuilder-Parameter).
    3. Kopiert den nativen Puffer in ein neu zugeordnetes verwaltetes Array, wenn [Out] {3} (ebenfalls Standard für StringBuilder).
  3. ToString() ordnet ein weiteres verwaltetes Array zu {4}.

Das sind {4} Zuordnungen, um eine Zeichenfolge aus nativem Code abzurufen. Die beste Möglichkeit, dies zu beschränken, besteht darin, den StringBuilder in einem weiteren Aufruf wiederzuverwenden, damit wird aber dennoch nur eine Zuordnung eingespart. Es ist viel besser, einen Zeichenpuffer aus dem ArrayPool zu verwenden und zwischenzuspeichern. Damit benötigen Sie in nachfolgenden Aufrufen nur noch die Zuordnung für ToString().

Ein weiteres Problem bei StringBuilder ist, dass immer der Rückgabepuffer bis zum ersten NULL-Zeichen zurückkopiert wird. Wenn die zurückgegebene Zeichenfolge nicht beendet oder mit einem doppelten NULL-Zeichen beendet wird, ist „P/Invoke“ bestenfalls falsch.

Wenn Sie StringBuildertatsächlich verwenden, besteht eine weitere Besonderheit darin, dass die Kapazität kein verborgenes NULL-Zeichen umfasst, das bei der Interoperabilität immer berücksichtigt wird. Das wird häufig falsch gemacht, da die meisten APIs die Größe des Puffers einschließlich des NULL-Zeichens erwarten. Dies kann zu unnötigen bzw. verschwendeten Zuordnungen führen. Darüber hinaus verhindert diese Besonderheit, dass die Runtime das Marshallen von StringBuilder optimiert, um die Erstellung von Kopien zu minimieren.

Weitere Informationen zum Marshalling von Zeichenfolgen finden Sie unter Standardmäßiges Marshalling für Zeichenfolgen und Anpassen des Zeichenfolgenmarshallings.

Windows-spezifisch: Bei [Out]-Zeichenfolgen verwendet die CLR (Common Language Runtime) standardmäßig CoTaskMemFree, um Zeichenfolgen freizugeben, oder SysStringFree bei Zeichenfolgen, die als UnmanagedType.BSTR gekennzeichnet sind. Bei den meisten APIs mit Puffer für Ausgabezeichenfolgen gilt Folgendes: Die übergebene Zeichenanzahl muss das NULL-Zeichen enthalten. Wenn der zurückgegebene Wert kleiner ist als die Zeichenanzahl, ist der Aufruf erfolgreich und der Wert ist die Anzahl der Zeichen ohne das nachgestellte NULL-Zeichen. Andernfalls ist die Anzahl die erforderliche Größe des Puffers einschließlich des NULL-Zeichens.

  • 5 übergeben, 4 abrufen: Die Zeichenfolge ist 4 Zeichen lang und umfasst ein nachgestelltes NULL-Zeichen.
  • 5 übergeben, 6 abrufen: Die Zeichenfolge ist 5 Zeichen lang und erfordert einen Puffer mit 6 Zeichen für das NULL-Zeichen. Windows-Datentypen für Zeichenfolgen

Boolesche Parameter und Felder

Bei booleschen Werten passieren leicht Fehler. Standardmäßig erfolgt für einen bool-Wert von .NET ein Marshalling in einen BOOL-Wert in Windows. Dort handelt es sich um einen 4 Byte langen Wert. Die Typen _Bool und bool in C und C++ sind jedoch Einzelbytewerte. Dies kann zu Bugs führen, die sich nur sehr schwer auffinden lassen, da der halbe Rückgabewert verworfen wird, sich das Ergebnis aber nur möglicherweise ändert. Weitere Informationen zum Marshallen von bool-Werten aus .NET in bool-Typen in C oder C++ finden Sie in der Dokumentation zum Anpassen des Marshallings von booleschen Feldern.

GUIDs

GUIDs können direkt in Signaturen verwendet werden. Viele Windows-APIs akzeptieren GUID&-Typen wie REFIID. Wenn die Methodensignatur einen Verweisparameter enthält, platzieren Sie entweder ein ref Schlüsselwort (keyword) oder ein [MarshalAs(UnmanagedType.LPStruct)] Attribut in der GUID-Parameterdeklaration.

GUID By-ref-GUID
KNOWNFOLDERID REFKNOWNFOLDERID

❌ Verwenden Sie NICHT [MarshalAs(UnmanagedType.LPStruct)] für etwas anderes als für ref-GUID-Parameter.

Für Blitting geeignete Typen

Für Blitting geeignete Typen sind Typen, die in verwaltetem und nativem Code die gleiche Darstellung auf Bitebene aufweisen. Als solche müssen sie nicht in ein anderes Format konvertiert werden, um ein Marshalling in den und aus dem nativen Code zu ermöglichen. Da dies die Leistung verbessert, sind diese Typen zu bevorzugen. Einige Typen sind für Blitting nicht geeignet, enthalten aber bekanntermaßen für Blitting geeignete Inhalte. Diese Typen haben ähnliche Optimierungen wie für Blitting geeignete Typen, wenn sie nicht in einem anderen Typ enthalten sind. Sie werden jedoch nicht als für Blitting geeignet angesehen, wenn sie sich in Feldern von Strukturen befinden oder für UnmanagedCallersOnlyAttribute erforderlich sind.

Für Blitting geeignete Typen bei aktiviertem Laufzeitmarshalling

Für Blitting geeignete Typen:

  • byte, sbyte, short, ushort, int, uint, long, ulong, single, double
  • Strukturen mit festem Layout, die für Instanzfelder nur über für Blitting geeignete Typen verfügen
    • Ein festes Layout erfordert [StructLayout(LayoutKind.Sequential)] oder [StructLayout(LayoutKind.Explicit)]
    • Strukturen sind standardmäßig LayoutKind.Sequential

Typen mit für Blitting geeigneten Inhalten:

  • Nicht geschachtelte, eindimensionale Arrays aus für Blitting geeigneten primitiven Typen (z. B. int[])
  • Klassen mit festem Layout, die für Instanzfelder nur über für Blitting geeignete Typen verfügen
    • Ein festes Layout erfordert [StructLayout(LayoutKind.Sequential)] oder [StructLayout(LayoutKind.Explicit)]
    • Klassen sind standardmäßig LayoutKind.Auto

NICHT für Blitting geeignet:

  • bool

MANCHMAL für Blitting geeignet:

  • char

Typen mit MANCHMAL für Blitting geeigneten Inhalten:

  • string

Wenn für Blitting geeignete Typen per Verweis mit in, ref oder out übergeben werden oder wenn Typen mit für Blitting geeigneten Inhalten per Wert übergeben werden, werden sie vom Marshaller einfach angepinnt, anstatt in einen Zwischenpuffer kopiert zu werden.

char ist in einem eindimensionalen Array oder bei Verwendung in einem Typen für Blitting geeignet, der explizit mit [StructLayout] mit CharSet = CharSet.Unicode gekennzeichnet ist.

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct UnicodeCharStruct
{
    public char c;
}

string enthält für Blitting geeignete Inhalte, wenn sie nicht in einem anderen Typ enthalten sind und als Argument und eine der folgenden Werte (nicht ref oder out) übergeben werden:

Sie können ermitteln, ob ein Typ für Blitting geeignet ist oder für Blitting geeignete Inhalte enthält, indem Sie versuchen, ein angepinntes GCHandle zu erstellen. Wenn der Typ keine Zeichenfolge ist oder nicht als für Blitting geeignet betrachtet wird, löst GCHandle.Alloc eine ArgumentException aus.

Für Blitting geeignete Typen bei deaktiviertem Laufzeitmarshalling

Wenn das Laufzeitmarshalling deaktiviert ist, sind die Regeln dafür, welche Typen für Blitting geeignet sind, erheblich einfacher. Alle Typen, die C#-unmanaged-Typen sind und keine Felder enthalten, die mit [StructLayout(LayoutKind.Auto)] gekennzeichnet sind, sind für Blitting geeignet. Alle Typen, die keine unmanaged-Typen in C# sind, sind nicht für Blitting geeignet. Das Konzept von Typen mit für Blitting geeigneten Inhalten, z. B. Arrays oder Zeichenfolgen, findet keine Anwendung, wenn das Laufzeitmarshalling deaktiviert ist. Ein Typ, der von der oben genannten Regel als nicht für Blitting geeignet betrachtet wird, wird bei deaktiviertem Laufzeitmarshalling nicht unterstützt.

Diese Regeln unterscheiden sich vom integrierten System hauptsächlich in Situationen, in denen bool und char verwendet werden. Wenn das Marshalling deaktiviert ist, wird bool als 1-Byte-Wert übergeben und nicht normalisiert, und char wird immer als 2-Byte-Wert übergeben. Wenn das Laufzeitmarshalling aktiviert ist, kann bool einem 1-, 2- oder 4-Byte-Wert zugeordnet werden und wird immer normalisiert, und char wird abhängig vom CharSet entweder einem 1- oder 2-Byte-Wert zugeordnet.

✔️ LEGEN Sie Ihre Strukturen nach Möglichkeit als für Blitting geeignet fest.

Weitere Informationen finden Sie unter:

Beibehalten von verwalteten Objekten

GC.KeepAlive() stellt sicher, dass ein Objekt im Gültigkeitsbereich bleibt, bis die KeepAlive-Methode erreicht wird.

HandleRef ermöglicht es dem Marshaller, ein Objekt während der Dauer eines P/Invoke beizubehalten. Es kann statt IntPtr in Methodensignaturen verwendet werden. SafeHandle ersetzt diese Klasse und sollte stattdessen verwendet werden.

GCHandle ermöglicht das Anheften eines verwalteten Objekts und Abrufen des nativen Zeigers auf das Objekt. Das grundlegende Muster sieht folgendermaßen aus:

GCHandle handle = GCHandle.Alloc(obj, GCHandleType.Pinned);
IntPtr ptr = handle.AddrOfPinnedObject();
handle.Free();

Anheften ist kein Standardvorgang für GCHandle. Das andere wichtige Muster sieht so aus: Ein Verweis wird über nativen Code an ein verwaltetes Objekt übergeben und, in der Regel über einen Rückruf, wieder zurückgegeben. Hier sehen Sie das Muster:

GCHandle handle = GCHandle.Alloc(obj);
SomeNativeEnumerator(callbackDelegate, GCHandle.ToIntPtr(handle));

// In the callback
GCHandle handle = GCHandle.FromIntPtr(param);
object managedObject = handle.Target;

// After the last callback
handle.Free();

Denken Sie daran, dass GCHandle explizit freigegeben werden muss, um Arbeitsspeicherverluste zu vermeiden.

Allgemeine Windows-Datentypen

Die folgende Liste enthält Datentypen, die in Windows-APIs häufig verwendet werden, sowie C#-Typen, die beim Aufrufen des Windows-Codes verwendet werden sollen.

Die folgenden Typen weisen trotz ihrer Namen die gleiche Größe wie 32- und 64-Bit-Typen für Windows auf.

Breite Windows C# Alternative
32 BOOL int bool
8 BOOLEAN byte [MarshalAs(UnmanagedType.U1)] bool
8 BYTE byte
8 UCHAR byte
8 UINT8 byte
8 CCHAR byte
8 CHAR sbyte
8 CHAR sbyte
8 INT8 sbyte
16 CSHORT short
16 INT16 short
16 SHORT short
16 ATOM ushort
16 UINT16 ushort
16 USHORT ushort
16 WORD ushort
32 INT int
32 INT32 int
32 LONG int Weitere Informationen finden Sie unter CLong und CULong.
32 LONG32 int
32 CLONG uint Weitere Informationen finden Sie unter CLong und CULong.
32 DWORD uint Weitere Informationen finden Sie unter CLong und CULong.
32 DWORD32 uint
32 UINT uint
32 UINT32 uint
32 ULONG uint Weitere Informationen finden Sie unter CLong und CULong.
32 ULONG32 uint
64 INT64 long
64 LARGE_INTEGER long
64 LONG64 long
64 LONGLONG long
64 QWORD long
64 DWORD64 ulong
64 UINT64 ulong
64 ULONG64 ulong
64 ULONGLONG ulong
64 ULARGE_INTEGER ulong
32 HRESULT int
32 NTSTATUS int

Die folgenden Typen sind Zeiger und entsprechen der Breite der Plattform. Verwenden Sie IntPtr/UIntPtr für diese.

Signierte Zeigertypen (verwenden Sie IntPtr). Nicht signierte Zeigertypen (verwenden Sie UIntPtr).
HANDLE WPARAM
HWND UINT_PTR
HINSTANCE ULONG_PTR
LPARAM SIZE_T
LRESULT
LONG_PTR
INT_PTR

Ein PVOID-Typ in Windows, der void* in C entspricht, kann als IntPtr oder UIntPtr gemarshallt werden. void* sollte jedoch nach Möglichkeit bevorzugt werden.

Windows-Datentypen

Datentypbereiche

Früher integrierte unterstützte Typen

Es gibt seltene Fälle, in denen die integrierte Unterstützung für einen Typ entfernt wird.

Die integrierte Unterstützung für das Marshalling für UnmanagedType.HString und UnmanagedType.IInspectable wurde im .NET 5-Release entfernt. Sie müssen Binärdateien, die diesen Marshalltyp und ein vorheriges Framework als Ziel verwenden, neu kompilieren. Es ist weiterhin möglich, diesen Typ zu marshallen, dies muss aber manuell erfolgen, wie im folgenden Codebeispiel gezeigt. Dieser Code wird in Zukunft funktionieren und ist auch mit früheren Frameworks kompatibel.

public sealed class HStringMarshaler : ICustomMarshaler
{
    public static readonly HStringMarshaler Instance = new HStringMarshaler();

    public static ICustomMarshaler GetInstance(string _) => Instance;

    public void CleanUpManagedData(object ManagedObj) { }

    public void CleanUpNativeData(IntPtr pNativeData)
    {
        if (pNativeData != IntPtr.Zero)
        {
            Marshal.ThrowExceptionForHR(WindowsDeleteString(pNativeData));
        }
    }

    public int GetNativeDataSize() => -1;

    public IntPtr MarshalManagedToNative(object ManagedObj)
    {
        if (ManagedObj is null)
            return IntPtr.Zero;

        var str = (string)ManagedObj;
        Marshal.ThrowExceptionForHR(WindowsCreateString(str, str.Length, out var ptr));
        return ptr;
    }

    public object MarshalNativeToManaged(IntPtr pNativeData)
    {
        if (pNativeData == IntPtr.Zero)
            return null;

        var ptr = WindowsGetStringRawBuffer(pNativeData, out var length);
        if (ptr == IntPtr.Zero)
            return null;

        if (length == 0)
            return string.Empty;

        return Marshal.PtrToStringUni(ptr, length);
    }

    [DllImport("api-ms-win-core-winrt-string-l1-1-0.dll")]
    [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
    private static extern int WindowsCreateString([MarshalAs(UnmanagedType.LPWStr)] string sourceString, int length, out IntPtr hstring);

    [DllImport("api-ms-win-core-winrt-string-l1-1-0.dll")]
    [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
    private static extern int WindowsDeleteString(IntPtr hstring);

    [DllImport("api-ms-win-core-winrt-string-l1-1-0.dll")]
    [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
    private static extern IntPtr WindowsGetStringRawBuffer(IntPtr hstring, out int length);
}

// Example usage:
[DllImport("api-ms-win-core-winrt-l1-1-0.dll", PreserveSig = true)]
internal static extern int RoGetActivationFactory(
    /*[MarshalAs(UnmanagedType.HString)]*/[MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(HStringMarshaler))] string activatableClassId,
    [In] ref Guid iid,
    [Out, MarshalAs(UnmanagedType.IUnknown)] out object factory);

Überlegungen zu plattformübergreifenden Datentypen

Es gibt Typen in C/C++, die etwas Spielraum bei der Definition aufweisen. Beim Schreiben plattformübergreifender Interoperabilität können Fälle auftreten, in denen sich Plattformen unterscheiden und die Probleme verursachen können, wenn sie nicht berücksichtigt werden.

C/C++ long

C/C++-long und C#-long weisen nicht unbedingt die gleiche Größe auf.

Der long-Typ in C/C++ ist so definiert, dass er mindestens 32 Bits lang ist. Dies bedeutet, dass es eine Mindestanzahl erforderlicher Bits gibt, aber Plattformen können bei Bedarf mehr Bits verwenden. Die folgende Tabelle zeigt die Unterschiede in den bereitgestellten Bits für den C/C++-Datentyp long auf verschiedenen Plattformen.

Plattform 32-Bit 64-Bit
Windows 32 32
macOS/*nix 32 64

Im Gegensatz dazu entspricht C#-long immer 64 Bit. Aus diesem Grund empfiehlt es sich, die Verwendung von C#-long für die Interoperabilität mit C/C++-long zu vermeiden.

(Dieses Problem mit C/C++-long ist für C/C++-char, -short, -int und -long long nicht vorhanden, da sie auf all diesen Plattformen 8, 16, 32 und 64 Bit aufweisen.)

Verwenden Sie in .NET 6 und höheren Versionen die Typen CLong und CULong für die Interoperabilität mit den C/C++-Datentypen long und unsigned long. Das folgende Beispiel gilt für CLong, Sie können jedoch CULong verwenden, um unsigned long auf ähnliche Weise zu abstrahieren.

// Cross platform C function
// long Function(long a);
[DllImport("NativeLib")]
extern static CLong Function(CLong a);

// Usage
nint result = Function(new CLong(10)).Value;

Wenn Sie für .NET 5 und frühere Versionen programmieren, sollten Sie separate Windows- und Nicht-Windows-Signaturen deklarieren, um das Problem zu behandeln.

static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);

// Cross platform C function
// long Function(long a);

[DllImport("NativeLib", EntryPoint = "Function")]
extern static int FunctionWindows(int a);

[DllImport("NativeLib", EntryPoint = "Function")]
extern static nint FunctionUnix(nint a);

// Usage
nint result;
if (IsWindows)
{
    result = FunctionWindows(10);
}
else
{
    result = FunctionUnix(10);
}

Strukturen

Verwaltete Strukturen werden im Stapel erstellt und erst dann entfernt, wenn die Methode zurückgegeben wird. Per Definition werden sie „angeheftet“ (also von der Garbage Collection nicht verschoben). Sie können auch einfach die Adresse in unsicheren Codeblöcken verwenden, wenn der native Code den Zeiger nicht über das Ende der aktuellen Methode hinaus verwendet.

Für Blitting geeignete Strukturen sind wesentlich leistungsfähiger, da sie ganz einfach direkt von der Marshallingebene genutzt werden können. Versuchen Sie, Typen blitfähig zu machen (vermeiden Sie z.B. bool). Weitere Informationen finden Sie im Abschnitt Für Blitting geeignete Typen.

Wenn eine Struktur für Blitting geeignet ist, verwenden Sie sizeof() statt Marshal.SizeOf<MyStruct>(), um eine bessere Leistung zu erzielen. Wie oben erwähnt, können Sie überprüfen, ob der Typ für Blitting geeignet ist, indem Sie versuchen, ein angeheftetes GCHandle zu erstellen. Wenn der Typ keine Zeichenfolge ist oder nicht als für Blitting geeignet betrachtet wird, löst GCHandle.Alloc eine ArgumentException aus.

Zeiger auf Strukturen in Definitionen müssen entweder von ref übergeben werden oder unsafe und * verwenden.

✔️ PASSEN Sie die verwaltete Struktur so eng wie möglich an die Form und die Namen an, die in der offiziellen Dokumentation zur Plattform oder im Header verwendet werden.

✔️ VERWENDEN Sie sizeof() aus C# anstelle von Marshal.SizeOf<MyStruct>() für Strukturen, die für Blitting geeignet sind, um die Leistung zu verbessern.

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

❌ VERMEIDEN Sie die Verwendung von System.Delegate- oder System.MulticastDelegate-Feldern, um Felder für Funktionszeiger in Strukturen darzustellen.

Da System.Delegate und System.MulticastDelegate nicht über eine erforderliche Signatur verfügen, garantieren sie nicht, dass der übergebene Delegat der Signatur entspricht, die vom nativen Code erwartet wird. Außerdem kann in .NET Framework und .NET Core das Marshalling einer Struktur mit einem System.Delegate oder einen System.MulticastDelegate von ihrer nativen Darstellung in ein verwaltetes Objekt die Runtime destabilisieren, wenn der Wert des Felds in der nativen Darstellung kein Funktionszeiger ist, der einen verwalteten Delegaten umschließt. In .NET 5 und höheren Versionen wird das Marshalling eines System.Delegate- oder System.MulticastDelegate-Felds von einer nativen Darstellung in ein verwaltetes Objekt nicht unterstützt. Verwenden Sie anstelle von System.Delegate oder System.MulticastDelegate einen bestimmten Delegattyp.

Puffer fester Größe

Ein Array wie INT_PTR Reserved1[2] muss in zwei IntPtr-Felder gemarshallt werden: Reserved1a und Reserved1b. Wenn das native Array ein primitiver Typ ist, können wir das Schlüsselwort fixed verwenden, um es etwas übersichtlicher zu schreiben. SYSTEM_PROCESS_INFORMATION sieht im nativen Header beispielsweise so aus:

typedef struct _SYSTEM_PROCESS_INFORMATION {
    ULONG NextEntryOffset;
    ULONG NumberOfThreads;
    BYTE Reserved1[48];
    UNICODE_STRING ImageName;
...
} SYSTEM_PROCESS_INFORMATION

In C# können wir es folgendermaßen schreiben:

internal unsafe struct SYSTEM_PROCESS_INFORMATION
{
    internal uint NextEntryOffset;
    internal uint NumberOfThreads;
    private fixed byte Reserved1[48];
    internal Interop.UNICODE_STRING ImageName;
    ...
}

Es gibt jedoch einige Besonderheiten bei festen Puffern. Bei festen Puffer aus nicht für Blitting geeigneten Typen wird das Marshalling nicht ordnungsgemäß ausgeführt, daher muss das vorhandene Array auf mehrere einzelne Felder erweitert werden. Darüber hinaus gilt für .NET Framework und .NET Core vor Version 3.0: Wenn eine Struktur, die ein festes Pufferfeld enthält, in einer nicht für Blitting geeigneten Struktur geschachtelt wird, erfolgt kein ordnungsgemäßes Marshalling des festen Pufferfelds zum nativen Code.