Najlepsze rozwiązania dotyczące współdziałania natywnego

Platforma .NET udostępnia różne sposoby dostosowywania natywnego kodu współdziałania. Ten artykuł zawiera wskazówki, które są przestrzegane przez zespoły firmy Microsoft dla platformy .NET w celu zapewnienia współdziałania natywnego.

Wskazówki ogólne

Wskazówki zawarte w tej sekcji dotyczą wszystkich scenariuszy międzyoperacyjności.

  • ✔️ Użyj polecenia [LibraryImport], jeśli to możliwe, w przypadku określania wartości docelowej dla platformy .NET 7 lub nowszej.
    • Istnieją przypadki, w których użycie [DllImport] jest odpowiednie. Analizator kodu z identyfikatorem SYSLIB1054 informuje o tym, kiedy tak jest.
  • ✔️ Należy użyć tego samego nazewnictwa i wielkich liter dla metod i parametrów, co metoda natywna, którą chcesz wywołać.
  • ✔️ ROZWAŻ użycie tego samego nazewnictwa i wielkości liter dla wartości stałych.
  • ✔️ Do użyj typów platformy .NET, które mapują najbliżej typu natywnego. Na przykład w języku C#użyj wartości uint , gdy typem natywnym jest unsigned int.
  • ✔️ Preferuj wyrażanie typów natywnych wyższego poziomu przy użyciu struktur platformy .NET, a nie klas.
  • ✔️ Wolisz używać wskaźników funkcji, w przeciwieństwie do Delegate typów, podczas przekazywania wywołań zwrotnych do funkcji niezarządzanych w języku C#.
  • ✔️ Należy używać [In] atrybutów i [Out] w parametrach tablicy.
  • ✔️ Należy używać [In] atrybutów i [Out] tylko w innych typach, gdy zachowanie, którego chcesz użyć, różni się od domyślnego zachowania.
  • ✔️ ROZWAŻ użycie metody System.Buffers.ArrayPool<T> do pulowania buforów macierzy natywnej.
  • ✔️ ROZWAŻ zawijanie deklaracji P/Invoke w klasie o tej samej nazwie i wielkich literach co biblioteka natywna.
    • Dzięki temu atrybuty [LibraryImport] lub [DllImport] mogą używać funkcji języka C# nameof do przekazania nazwy biblioteki natywnej i upewnić się, że nazwa biblioteki natywnej nie została pominięta.

Ustawienia atrybutu LibraryImport

Analizator kodu z identyfikatorem SYSLIB1054 ułatwia zapoznanie się z usługą LibraryImportAttribute. W większości przypadków użycie metody wymaga jawnej LibraryImportAttribute deklaracji, a nie polegania na ustawieniach domyślnych. Ten projekt jest zamierzony i pomaga uniknąć niezamierzonego zachowania w scenariuszach międzyoperacyjności.

Ustawienia atrybutu DllImport

Ustawienie Wartość domyślna Zalecenie Szczegóły
PreserveSig true Zachowaj wartość domyślną Jeśli ta wartość jest jawnie ustawiona na wartość false, zwracane wartości HRESULT zostaną przekształcone w wyjątki (a wartość zwracana w definicji stanie się równa null w wyniku).
SetLastError false Zależy od interfejsu API Ustaw wartość true, jeśli interfejs API używa metody GetLastError i użyj polecenia Marshal.GetLastWin32Error, aby uzyskać wartość. Jeśli interfejs API ustawia warunek informujący, że ma błąd, przed wykonaniem innych wywołań, aby uniknąć niezamierzonego zastąpienia.
CharSet Zdefiniowane przez kompilator (określone w dokumentacji zestawu znaków) Jawne użycie CharSet.Unicode ciągów CharSet.Ansi lub znaków w definicji Określa zachowanie marshalling ciągów i co ExactSpelling robi, gdy false. Należy pamiętać, że CharSet.Ansi jest to rzeczywiście UTF8 w systemie Unix. W większości przypadków system Windows używa formatu Unicode, podczas gdy system Unix używa formatu UTF8. Zobacz więcej informacji na temat dokumentacji dotyczącej zestawów znaków.
ExactSpelling false true Ustaw wartość true i uzyskaj niewielką korzyść wydajności, ponieważ środowisko uruchomieniowe nie będzie szukać alternatywnych nazw funkcji z sufiksem "A" lub "W" w zależności od wartości CharSet ustawienia ("A" dla CharSet.Ansi i "W" dla CharSet.Unicode).

Parametry ciągu

Element A string jest przypięty i używany bezpośrednio przez kod macierzysty (zamiast kopiowany) po przekazaniu przez wartość (nie ref lub out) i jedną z następujących wartości:

❌ NIE używaj [Out] string parametrów. Parametry ciągu przekazywane przez wartość z atrybutem [Out] mogą zdestabilizować środowisko uruchomieniowe, jeśli ciąg jest ciągiem internowany. Zobacz więcej informacji na temat interningu ciągów w dokumentacji programu String.Intern.

✔️ ROZWAŻ char[] lub byte[] tablice z kodu natywnego ArrayPool , aby wypełnić bufor znaków. Wymaga to przekazania argumentu jako [Out].

Wskazówki specyficzne dla biblioteki DllImport

✔️ ROZWAŻ ustawienie CharSet właściwości w programie , [DllImport] aby środowisko uruchomieniowe znało oczekiwane kodowanie ciągów.

✔️ ROZWAŻ unikanie StringBuilder parametrów. StringBuilder Marshalling zawsze tworzy natywną kopię buforu. W związku z tym może to być niezwykle nieefektywne. Zapoznaj się z typowym scenariuszem wywoływania interfejsu API systemu Windows, który przyjmuje ciąg:

  1. StringBuilder Utwórz żądaną pojemność (przydziela pojemność zarządzaną). {1}
  2. Wywołać:
    1. Przydziela bufor {2}macierzysty .
    2. Kopiuje zawartość, jeśli [In](wartość domyślna parametru StringBuilder ).
    3. Kopiuje bufor macierzysty do nowo przydzielonej tablicy zarządzanej, jeśli [Out]{3}(również wartość domyślna dla StringBuilder).
  3. ToString() przydziela kolejną tablicę {4}zarządzaną .

{4} To alokacje, aby pobrać ciąg z kodu natywnego. Najlepszym rozwiązaniem, aby ograniczyć ten problem, jest ponowne użycie StringBuilder metody w innym wywołaniu, ale nadal zapisuje tylko jedną alokację. Znacznie lepiej używać i buforować bufor znaków z ArrayPoolklasy . Następnie możesz przejść do alokacji dla ToString() kolejnych wywołań.

Innym problemem jest StringBuilder to, że zawsze kopiuje bufor powrotny do pierwszej wartości null. Jeśli przekazany ciąg powrotny nie zostanie zakończony lub jest ciągiem z podwójnymi wartościami null, twój ciąg P/Invoke jest niepoprawny.

Jeśli używasz metody StringBuilder, jedna ostatnia gotcha polega na tym, że pojemność nie zawiera ukrytej wartości null, która jest zawsze uwzględniana w międzyoperacie. Często zdarza się, że ludzie mają to złe, ponieważ większość interfejsów API chce mieć rozmiar buforu , w tym wartość null. Może to spowodować zmarnowanie/niepotrzebne alokacje. Ponadto ta funkcja gotcha uniemożliwia środowisku uruchomieniowemu optymalizowanie StringBuilder marshallingu w celu zminimalizowania kopii.

Aby uzyskać więcej informacji na temat marshalingu ciągów, zobacz Domyślne marshalling dla ciągów i Dostosowywanie marshalling ciągów.

Windows Specific Dla [Out] ciągów CLR będzie domyślnie używany CoTaskMemFree do zwalniania ciągów lub SysStringFree ciągów oznaczonych jako UnmanagedType.BSTR. W przypadku większości interfejsów API z buforem ciągu wyjściowego: przekazana liczba znaków musi zawierać wartość null. Jeśli zwrócona wartość jest mniejsza niż przekazana liczba znaków, wywołanie powiodło się, a wartość jest liczbą znaków bez końcowej wartości null. W przeciwnym razie liczba jest wymaganym rozmiarem buforu , w tym znakiem null.

  • Przekaż wartość 5, pobierz 4: ciąg ma długość 4 znaków z końcową wartością null.
  • Przekaż wartość 5, pobierz 6: ciąg ma długość 5 znaków, wymaga 6-znakowego buforu do przechowywania wartości null. Typy danych systemu Windows dla ciągów

Parametry logiczne i pola

Booleans są łatwe do bałaganu. Domyślnie platforma .NETjesta bool jest do systemu Windows BOOL, gdzie jest to wartość 4-bajtowa. _BoolJednak typy i bool w językach C i C++ są pojedynczym bajtem. Może to prowadzić do trudności ze śledzeniem usterek, ponieważ połowa zwracanej wartości zostanie odrzucona, co potencjalnie spowoduje zmianę wyniku. Aby uzyskać więcej informacji na temat marshalingu wartości platformy .NET bool do typów C lub C++ bool , zobacz dokumentację dotyczącą dostosowywania marshalingu pól logicznych.

Identyfikatory guid

Identyfikatory GUID można używać bezpośrednio w podpisach. Wiele interfejsów API systemu Windows używa GUID& aliasów typu, takich jak REFIID. Gdy podpis metody zawiera parametr referencyjny, umieść ref słowo kluczowe lub [MarshalAs(UnmanagedType.LPStruct)] atrybut w deklaracji parametru GUID.

Identyfikator GUID By-ref GUID
KNOWNFOLDERID REFKNOWNFOLDERID

❌ Nie używaj [MarshalAs(UnmanagedType.LPStruct)] żadnych elementów innych niż ref parametry identyfikatora GUID.

Typy tabeli blittable

Typy blittable to typy, które mają tę samą reprezentację na poziomie bitów w zarządzanym i natywnym kodzie. W związku z tym nie muszą być konwertowane na inny format, który ma być sformatowany do i z kodu natywnego, a ponieważ poprawia to wydajność, powinny być preferowane. Niektóre typy nie są blittable, ale są znane z zawartości blittable. Te typy mają podobne optymalizacje jak typy blittable, gdy nie są zawarte w innym typie, ale nie są traktowane jako blittable, gdy w polach struktur lub w celach UnmanagedCallersOnlyAttribute.

Typy blittable po włączeniu marshalingu środowiska uruchomieniowego

Typy blittable:

  • byte, sbyte, , , intsingleushortuintlongulongshortdouble
  • struktury ze stałym układem, które mają tylko typy wartości w formie blittable dla pól wystąpień
    • układ stały wymaga [StructLayout(LayoutKind.Sequential)] lub [StructLayout(LayoutKind.Explicit)]
    • Struktury są LayoutKind.Sequential domyślnie

Typy z zawartością tabeli blittable:

  • nienagnieżdżone, jednowymiarowe tablice typów pierwotnych w formie blittable (na przykład int[])
  • klasy ze stałym układem, które mają tylko typy wartości w formie blittable dla pól wystąpień
    • układ stały wymaga [StructLayout(LayoutKind.Sequential)] lub [StructLayout(LayoutKind.Explicit)]
    • klasy są LayoutKind.Auto domyślnie

NOT blittable:

  • bool

CZASAMI blittable:

  • char

Typy z czasami zawartością tabeli blittable:

  • string

Gdy typy blittable są przekazywane przez odwołanie do in, reflub out, lub, gdy typy z zawartością blittable są przekazywane przez wartość, są po prostu przypięte przez marshaller zamiast być kopiowane do buforu pośredniego.

char jest blittable w tablicy jednowymiarowej lub jeśli jest częścią typu, który zawiera go jawnie oznaczony za pomocą [StructLayout] polecenia CharSet = CharSet.Unicode.

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

string zawiera zawartość, jeśli nie jest zawarta w innym typie i jest przekazywana przez wartość (nie ref lub out) jako argument i jedną z następujących wartości:

  • StringMarshalling parametr jest zdefiniowany jako Utf16.
  • Argument jest jawnie oznaczony jako [MarshalAs(UnmanagedType.LPWSTR)].
  • CharSet to Unicode.

Możesz sprawdzić, czy typ jest blittable lub zawiera zawartość tabeli blittable, próbując utworzyć przypięty GCHandleelement . Jeśli typ nie jest ciągiem lub uważanym za blittable, GCHandle.Alloc zgłosi błąd ArgumentException.

Typy tabeli Blittable, gdy marshalling środowiska uruchomieniowego jest wyłączony

Gdy marshalling środowiska uruchomieniowego jest wyłączony, reguły, dla których typów są blittable, są znacznie prostsze. Wszystkie typy, które są typami języka C# unmanaged i nie mają żadnych pól oznaczonych jako [StructLayout(LayoutKind.Auto)] blittable. Wszystkie typy, które nie są typami języka C# unmanaged , nie są blittable. Koncepcja typów z zawartością tabeli blittable, takich jak tablice lub ciągi, nie ma zastosowania, gdy marshalling środowiska uruchomieniowego jest wyłączony. Każdy typ, który nie jest traktowany jako blittable przez wyżej wymienioną regułę, jest nieobsługiwany, gdy marshalling środowiska uruchomieniowego jest wyłączony.

Te reguły różnią się od wbudowanego systemu przede wszystkim w sytuacjach, w których bool i char są używane. Gdy marshalling jest wyłączony, bool jest przekazywany jako wartość 1-bajtowa, a nie znormalizowany i char zawsze jest przekazywany jako wartość 2-bajtowa. Po włączeniu bool marshalingu środowiska uruchomieniowego można mapować na wartość 1, 2 lub 4 bajtów i zawsze jest znormalizowana, a char następnie mapuje wartość na 1 lub 2 bajty w zależności od CharSetwartości .

✔️ CZY sprawić, aby struktury można było rozbić, gdy jest to możliwe.

Aby uzyskać więcej informacji, zobacz:

Utrzymywanie aktywności obiektów zarządzanych

GC.KeepAlive() zapewni, że obiekt pozostanie w zakresie do momentu trafienia metody KeepAlive.

HandleRef umożliwia marshallerowi zachowanie obiektu żywego przez czas trwania wywołania P/Invoke. Można go użyć zamiast IntPtr w podpisach metod. SafeHandle skutecznie zastępuje tę klasę i zamiast tego należy jej używać.

GCHandle umożliwia przypinanie zarządzanego obiektu i pobieranie do niego natywnego wskaźnika. Podstawowy wzorzec to:

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

Przypinanie nie jest wartością domyślną dla elementu GCHandle. Innym głównym wzorcem jest przekazywanie odwołania do obiektu zarządzanego za pomocą kodu natywnego i z powrotem do kodu zarządzanego, zwykle z wywołaniem zwrotnym. Oto wzorzec:

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();

Nie zapomnij, że należy jawnie zwolnić, GCHandle aby uniknąć przecieków pamięci.

Typowe typy danych systemu Windows

Poniżej znajduje się lista typów danych, które są często używane w interfejsach API systemu Windows i których typów języka C# należy używać podczas wywoływania kodu systemu Windows.

Następujące typy są takie same jak w 32-bitowych i 64-bitowych systemach Windows, pomimo ich nazw.

Width Windows C# Alternatywne rozwiązanie
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 Zobacz CLong i CULong.
32 LONG32 int
32 CLONG uint Zobacz CLong i CULong.
32 DWORD uint Zobacz CLong i CULong.
32 DWORD32 uint
32 UINT uint
32 UINT32 uint
32 ULONG uint Zobacz CLong i 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

Następujące typy, będące wskaźnikami, są zgodne z szerokością platformy. W tym celu należy użyć polecenia IntPtr/UIntPtr .

Typy podpisanych wskaźników (użyj IntPtr) Typy niepodpisanych wskaźników (użyj UIntPtrpolecenia )
HANDLE WPARAM
HWND UINT_PTR
HINSTANCE ULONG_PTR
LPARAM SIZE_T
LRESULT
LONG_PTR
INT_PTR

System Windows PVOID, który jest C void*, może być marshalled jako IntPtr lub UIntPtr, ale preferuj void* , jeśli to możliwe.

Typy danych systemu Windows

Zakresy typu danych

Wcześniej wbudowane typy obsługiwane

W przypadku usunięcia wbudowanej obsługi typu występują rzadkie wystąpienia.

UnmanagedType.IInspectable Wbudowana obsługa UnmanagedType.HString marshalingu została usunięta w wersji .NET 5. Należy ponownie skompilować pliki binarne, które używają tego typu marshalingu i które są przeznaczone dla poprzedniej platformy. Nadal jest możliwe przeprowadzanie marshalingu tego typu, ale należy przeprowadzić marshaling ręcznie, jak pokazano w poniższym przykładzie kodu. Ten kod będzie działać w przyszłości i jest również zgodny z poprzednimi strukturami.

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

Zagadnienia dotyczące typów danych międzyplatformowych

Istnieją typy w języku C/C++, które mają szerokość geograficzną w sposobie ich definiowania. Podczas pisania międzyplatformowych międzyplatformowych przypadków mogą wystąpić różnice między platformami i mogą powodować problemy, jeśli nie są brane pod uwagę.

C/C++ long

C/C++ long i C# long nie muszą mieć tego samego rozmiaru.

Typ long w języku C/C++ jest zdefiniowany tak, aby miał "co najmniej 32" bity. Oznacza to, że istnieje minimalna liczba wymaganych bitów, ale platformy mogą w razie potrzeby używać większej liczby bitów. W poniższej tabeli przedstawiono różnice w podanych bitach dla typu danych C/C++ long między platformami.

Platforma 32-bitowa 64-bitowa
Windows 32 32
macOS/*nix 32 64

Natomiast język C# long jest zawsze 64-bitowy. Z tego powodu najlepiej unikać używania języka C# long do współdziałania z językiem C/C++ long.

(Ten problem z językiem C/C++ long nie istnieje dla języków C/C++ char, short, inti long long ponieważ są to odpowiednio 8, 16, 32 i 64 bity na wszystkich tych platformach).

W wersjach .NET 6 i nowszych użyj CLong typów i CULong do współdziałania z językami C/C++ long i unsigned long typami danych. Poniższy przykład dotyczy CLongelementu , ale możesz użyć CULong metody do abstrakcji unsigned long w podobny sposób.

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

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

W przypadku określania wartości docelowej dla platformy .NET 5 i starszych wersji należy zadeklarować oddzielne podpisy systemu Windows i innych niż Windows, aby rozwiązać ten problem.

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

Struktury

Zarządzane struktury są tworzone na stosie i nie są usuwane, dopóki metoda nie zostanie zwrócona. Następnie są one "przypięte" (nie zostaną przeniesione przez GC). Możesz również po prostu użyć adresu w niebezpiecznych blokach kodu, jeśli kod natywny nie będzie używać wskaźnika obok końca bieżącej metody.

Struktury blittable są o wiele bardziej wydajne, ponieważ mogą być po prostu używane bezpośrednio przez warstwę marshallingową. Spróbuj utworzyć struktury w formie usterek (na przykład unikaj bool). Aby uzyskać więcej informacji, zobacz sekcję Typy usterek.

Jeśli struktura jest blittable, użyj sizeof() zamiast Marshal.SizeOf<MyStruct>() w celu uzyskania lepszej wydajności. Jak wspomniano powyżej, można sprawdzić, czy typ jest nie do usterki, próbując utworzyć przypięty GCHandleelement . Jeśli typ nie jest ciągiem lub uważanym za blittable, GCHandle.Alloc zgłosi wartość ArgumentException.

Wskaźniki do struktur w definicjach muszą być przekazywane przez ref lub używane unsafe i *.

✔️ Dopasuj zarządzaną strukturę tak blisko, jak to możliwe do kształtu i nazw używanych w oficjalnej dokumentacji lub nagłówku platformy.

✔️ Aby zwiększyć wydajność, należy użyć języka C# sizeof() zamiast Marshal.SizeOf<MyStruct>() dla struktur blittable.

❌ UNIKAJ używania klas do wyrażania złożonych typów natywnych za pośrednictwem dziedziczenia.

❌ UNIKAJ używania System.Delegate pól lub System.MulticastDelegate do reprezentowania pól wskaźnika funkcji w strukturach.

Ponieważ System.Delegate i System.MulticastDelegate nie mają wymaganego podpisu, nie gwarantują, że delegat przekazany w będzie zgodny z podpisem oczekiwanym przez kod macierzysty. Ponadto w programach .NET Framework i .NET Core marshalling System.Delegate struktury zawierającej element lub System.MulticastDelegate z natywnej reprezentacji obiektu zarządzanego może zdestabilizować środowisko uruchomieniowe, jeśli wartość pola w reprezentacji natywnej nie jest wskaźnikiem funkcji, który opakowuje zarządzanego delegata. W wersjach .NET 5 i nowszych nie jest obsługiwane marshaling a System.Delegate lub System.MulticastDelegate pola z reprezentacji natywnej do obiektu zarządzanego. Użyj określonego typu delegata System.Delegate zamiast lub System.MulticastDelegate.

Stałe bufory

Tablica podobna INT_PTR Reserved1[2] musi być przesłana do dwóch IntPtr pól i Reserved1aReserved1b. Gdy macierz natywna jest typem pierwotnym, możemy użyć słowa kluczowego fixed , aby napisać go nieco bardziej czysto. Na przykład SYSTEM_PROCESS_INFORMATION wygląda następująco w nagłówku natywnym:

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

W języku C#możemy napisać go w następujący sposób:

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

Jednak istnieją pewne gotchas ze stałymi buforami. Stałe bufory typów niezwiązanych z blittable nie będą poprawnie rozsyłane, więc tablica w miejscu musi zostać rozszerzona na wiele pojedynczych pól. Ponadto w programach .NET Framework i .NET Core przed 3.0, jeśli struktura zawierająca stałe pole buforu jest zagnieżdżona w strukturze nielitowalnej, stałe pole buforu nie będzie poprawnie ustawiane na kod macierzysty.