Osvědčené postupy nativní interoperability

.NET vám nabízí různé způsoby přizpůsobení nativního kódu interoperability. Tento článek obsahuje pokyny, které týmy microsoftu .NET sledují pro nativní interoperabilitu.

Obecné pokyny

Pokyny v této části platí pro všechny scénáře spolupráce.

  • ✔️ Použijte [LibraryImport], pokud je to možné, při cílení na .NET 7+.
    • Existují případy, kdy je použití [DllImport] vhodné. Analyzátor kódu s ID SYSLIB1054 vám řekne, kdy se jedná o tento případ.
  • ✔️ Používejte u svých metod a parametrů stejné názvy a kapitalizaci jako u původní metody, kterou chcete volat.
  • ✔️ ZVAŽTE použití stejného pojmenování a velká písmena pro konstantní hodnoty.
  • ✔️ DO definujte podpisy ukazatelů P/Invoke a funkce, které odpovídají argumentům funkce jazyka C.
  • ✔️ Použijte ty .NET typy, které nejlépe odpovídají nativnímu typu. Například v jazyce C# použijte uint , pokud je unsigned intnativní typ .
  • ✔️ Dáváte přednost vyjádření nativních typů vyšší úrovně pomocí struktur .NET místo tříd.
  • ✔️ Při předávání callbacků nespravovaným funkcím v jazyce C# upřednostňujte použití ukazatelů UnmanagedCallersOnlyAttribute na funkce, na rozdíl od Delegate typů. Další informace najdete na webu GetFunctionPointerForDelegate(Delegate).
  • ✔️ POUŽÍVEJTE atributy [In] a [Out] u parametrů pole.
  • ✔️ Používejte atributy [In] a [Out] pouze u jiných typů, pokud se chování, které chcete, liší od výchozího chování.
  • ✔️ ZVAŽTE použití System.Buffers.ArrayPool<T> k sdružování nativních vyrovnávacích pamětí pole.
  • ✔️ ZVAŽTE zabalení deklarací P/Invoke do třídy se stejným názvem a kapitalizací jako vaše nativní knihovna.
    • To umožňuje vašim atributům [LibraryImport] nebo [DllImport] využívat jazykový prvek C# nameof k předání názvu nativní knihovny a zajistit, že jste neudělali chybu při zadávání názvu nativní knihovny.
  • ✔️ Používejte SafeHandle handle k správě životnosti objektů, které zapouzdřují nespravované prostředky. Další informace naleznete v tématu Čištění nespravovaných prostředků.
  • ❌ Vyhněte se finalizačním metodám pro správu životnosti objektů, které zapouzdřují nespravované prostředky. Další informace naleznete v tématu Implementace metody Dispose.

Nastavení atributu LibraryImport

Analyzátor kódu s ID SYSLIB1054 vám pomůže s LibraryImportAttribute. Ve většině případů použití LibraryImportAttribute vyžaduje explicitní deklaraci, nikoli spoléhání na výchozí nastavení. Tento návrh je záměrný a pomáhá vyhnout se nezamýšlenému chování ve scénářích spolupráce.

Nastavení atributu DllImport

Nastavení Výchozí Doporučení Detaily
PreserveSig true Zachovat výchozí Pokud je tato hodnota explicitně nastavená na false, vrátí se neúspěšné návratové hodnoty HRESULT na výjimky (a vrácená hodnota v definici se v důsledku toho změní na hodnotu null).
SetLastError false Závisí na rozhraní API. Tuto hodnotu nastavte na true, pokud rozhraní API používá GetLastError a k získání hodnoty použijte Marshal.GetLastWin32Error. Pokud rozhraní API nastaví podmínku s informací, že obsahuje chybu, zobrazí se chyba před provedením dalších volání, aby se zabránilo neúmyslnému přepsání.
CharSet Definovaný kompilátorem (uvedený v dokumentaci znakové sady) Explicitně používejte CharSet.Unicode nebo CharSet.Ansi, když jsou v definici přítomny řetězce nebo znaky. To určuje chování zařazování řetězců a co ExactSpelling dělá, když false. Všimněte si, že CharSet.Ansi ve skutečnosti je UTF8 v Unixu. Most času Windows používá Unicode, zatímco Unix používá UTF8. Další informace najdete v dokumentaci o znakových sadách.
ExactSpelling false true Nastavte hodnotu true a získejte mírné výhody výkonu, protože modul runtime nebude hledat alternativní názvy funkcí s příponou "A" nebo "W" v závislosti na hodnotě CharSet nastavení ("A" pro CharSet.Ansi a "W" pro CharSet.Unicode).

Parametry řetězce

Připnutí string a přímé použití nativním kódem (místo zkopírování) při předání hodnotou (ne ref ani out) a jednou z následujících možností:

❌ Nepoužívejte [Out] string parametry. Řetězcové parametry předané hodnotou s atributem [Out] mohou destabilizovat běh programu, pokud je řetězec internovaný. Další informace o prokládání řetězců naleznete v dokumentaci pro String.Intern.

✔️ ZVAŽTE char[] nebo byte[] pole z ArrayPool, když se očekává, že nativní kód vyplní vyrovnávací paměť znaků. To vyžaduje předání argumentu jako [Out].

Pokyny specifické pro DllImport

✔️ ZVAŽTE nastavení vlastnosti CharSet v [DllImport], aby modul runtime znal očekávané kódování řetězců.

✔️ ZVAŽTE, jak se vyhnout parametrům StringBuilder . StringBuilder přiřazování vždy vytvoří nativní kopii vyrovnávací paměti. Proto může být velmi neefektivní. Typický scénář volání rozhraní API Windows, který přebírá řetězec:

  1. StringBuilder Vytvořte požadovanou kapacitu (přiděluje spravovanou kapacitu). {1}
  2. Vyvolat:
    1. Přidělí nativní vyrovnávací paměť {2}.
    2. Zkopíruje obsah, pokud [In](výchozí nastavení pro parametr StringBuilder).
    3. Zkopíruje nativní vyrovnávací paměť do nově přiděleného spravovaného pole, pokud [Out]{3}(také výchozí hodnota pro StringBuilder)).
  3. ToString() alokuje ještě další spravované pole {4}.

To jsou {4} alokace pro získání řetězce z nativního kódu. Nejlepší, co můžete udělat, pokud chcete toto omezení omezit, je znovu použít StringBuilder v jiném volání, ale tím se stále ukládá jenom jedno přidělení. Je mnohem lepší používat a ukládat vyrovnávací paměť znaků z ArrayPool do mezipaměti. Poté se můžete zaměřit pouze na přidělení pro ToString() při dalších voláních.

Druhým problémem StringBuilder je, že vždy kopíruje návratovou vyrovnávací paměť zpět na první hodnotu null. Pokud předávaný zpětný řetězec není ukončen nebo se jedná o řetězec s dvojitým ukončením null, v lepším případě je váš P/Invoke nesprávný.

Pokud použijeteStringBuilder, posledním překvapením je, že kapacita nezahrnuje skrytou hodnotu null, která je vždy zohledněna v interoperabilitě. Je běžné, že lidé udělají tuto chybu, protože většina rozhraní API požaduje velikost vyrovnávací paměti včetně hodnoty null. To může vést k plýtvání nebo zbytečným přidělením. Kromě toho tato past brání modulu runtime v optimalizaci přenosu dat StringBuilder, aby se minimalizovaly kopie.

Další informace o seskupování řetězců naleznete v tématu Výchozí seskupování řetězců a Přizpůsobení seskupování řetězců.

Windows Specific Pro řetězce [Out] bude CLR ve výchozím nastavení používat CoTaskMemFree k uvolnění řetězců nebo SysStringFree pro řetězce, které jsou označené jako UnmanagedType.BSTR. Pro většinu rozhraní API s vyrovnávací pamětí výstupního řetězce: Předaný počet znaků musí obsahovat hodnotu null. Pokud je vrácená hodnota menší než předaná hodnota počtu znaků, volání bylo úspěšné a hodnota je počet znaků bez koncové hodnoty null. V opačném případě je požadovaný počet vyrovnávací paměti včetně znaku null.

  • Předat 5, dostat 4: Řetězec má 4 znaky a končí nulovým znakem.
  • Zadat 5, získat 6: Řetězec je dlouhý 5 znaků, k uložení nulové hodnoty potřebujete 6-znakovou vyrovnávací paměť. Datové typy Windows pro řetězce

Logické parametry a pole

Logické hodnoty jsou snadné pokazit. Ve výchozím nastavení se .NET bool převede do Windows BOOL, kde se jedná o 4bajtovou hodnotu. Nicméně _Bool a bool typy jazyka C a C++ jsou jednobajtové. To může vést k tomu, že chyby budou obtížně sledovatelné, protože polovina návratové hodnoty bude zahozena, což může potenciálně změnit výsledek. Další informace o zařazování hodnot .NET bool do typů C nebo C++ bool najdete v dokumentaci k zařazování logických polí.

GUIDs

Identifikátory GUID jsou použitelné přímo v podpisech. Mnoho rozhraní API Windows bere aliasy typu GUID& jako REFIID. Pokud podpis metody obsahuje referenční parametr, umístěte klíčové ref slovo nebo [MarshalAs(UnmanagedType.LPStruct)] atribut na deklaraci parametru GUID.

GUID GUID podle reference
KNOWNFOLDERID REFKNOWNFOLDERID

❌ Nepoužívejte [MarshalAs(UnmanagedType.LPStruct)] pro nic jiného než ref parametry GUID.

Blittable typy

Typy Blittable jsou typy, které mají stejnou reprezentaci na úrovni bitu ve spravovaném a nativním kódu. Proto nemusí být převedeny do jiného formátu, aby byly zařazovány do a z nativního kódu, a vzhledem k tomu, že to zvyšuje výkon, měly by být upřednostňované. Některé typy nejsou blittable, ale je známo, že obsahují blittable obsah. Tyto typy mají podobné optimalizace jako blittable typy, pokud nejsou obsaženy v jiném typu, ale nejsou považovány za blittable, jestliže se nacházejí v polích struktur nebo pro účely UnmanagedCallersOnlyAttribute.

Typy Blittable při povolení maršálování za běhu programu

Blittovatelné typy:

  • byte, sbyte, short, ushort, int, uint, long, ulong, single, double, nint, nuint
  • nespravované ukazatele (například int*)
  • struktury s pevným rozložením, které mají pouze typy blittable hodnot pro pole instancí
    • Pevné rozložení vyžaduje [StructLayout(LayoutKind.Sequential)] nebo [StructLayout(LayoutKind.Explicit)]
    • Struktury jsou LayoutKind.Sequential ve výchozím nastavení

Typy s blittable obsahem:

  • nenořené jednorozměrné pole primitivních typů s proměnlivou velikostí (například int[])
  • třídy s pevným rozložením, které mají pouze blitelné typy hodnot pro pole instancí
    • Pevné rozložení vyžaduje [StructLayout(LayoutKind.Sequential)] nebo [StructLayout(LayoutKind.Explicit)]
    • třídy jsou LayoutKind.Auto ve výchozím nastavení

NENÍ kopírovatelný:

  • bool

NĚKDY blittable:

  • char

Typy, jejichž obsah je někdy blittable:

  • string

Pokud jsou blittable typy předány odkazem s in, ref nebo out, nebo pokud jsou typy s blittable obsahem předány hodnotou, jsou jednoduše připnuty marshallerem místo zkopírování do přechodné vyrovnávací paměti.

char je blittable v jednorozměrném poli nebo pokud je součástí typu, který je explicitně označen pomocí [StructLayout] s CharSet = CharSet.Unicode.

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

string obsahuje obsah blittable, pokud není obsažen v jiném typu a předává se hodnotou (ne ref nebo out) jako argument a některou z následujících možností:

Můžete zjistit, zda je typ blittable nebo obsahuje data, která lze přenášet jako blittable, pokusem o vytvoření připnuté GCHandle. Pokud typ není řetězec nebo považován za blittable, GCHandle.Alloc vyvolá chybu ArgumentException.

Blittovatelné typy, když je vypnuto maršálování za běhu

Když je zařazování za běhu zakázáno, pravidla určující, které typy lze přímo kopírovat mezi pamětmi, jsou výrazně jednodušší. Všechny typy, které jsou typy jazyka C# unmanaged a nemají žádná pole označená [StructLayout(LayoutKind.Auto)] , jsou blitelná. Všechny typy, které nejsou typu C# unmanaged, nejsou blittable. Koncept typů s blittable obsahem, jako jsou pole nebo řetězce, se nepoužije při zakázání zařazování za běhu. Jakýkoli typ, který není považován za blittable podle výše uvedeného pravidla, není podporován, pokud je za běhu zakázáno zařazování.

Tato pravidla se liší od integrovaného systému především v situacích, kdy se používají bool a char. Po vypnutí zařazování je bool předávána jako 1-bytová hodnota bez normalizace a char je vždy předávána jako 2-bytová hodnota. Pokud je zařazování za běhu povoleno, bool se může mapovat na hodnotu 1, 2 nebo 4 bajtů a je vždy normalizována, zatímco char se mapuje na hodnotu 1 nebo 2 bajtů v závislosti na CharSet.

✔️ Pokud je to možné, udělejte strukturu blitelnou.

Další informace naleznete v tématu:

Udržování spravovaných objektů naživu

GC.KeepAlive() zajistí, aby objekt zůstal v oboru, dokud se nestiskne metoda KeepAlive.

HandleRef umožňuje Marshalleru udržet objekt naživu po dobu trvání volání P/Invoke. Dá se použít místo IntPtr v podpisech metod. SafeHandle nahrazuje tuto třídu a měla by být použita.

GCHandle umožňuje připnout spravovaný objekt a získat na něj nativní ukazatel. Základní vzor je:

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

Připnutí není výchozí hodnota pro GCHandle. Dalším hlavním vzorem je předání odkazu spravovanému objektu prostřednictvím nativního kódu a zpět do spravovaného kódu, obvykle s zpětným voláním. Tady je vzor:

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

Nezapomeňte, že GCHandle je potřeba přímo uvolnit, aby nedošlo k únikům paměti.

Běžné datové typy Windows

Tady je seznam datových typů běžně používaných v rozhraních API systému Windows a typů C#, které jsou vhodné pro volání kódu Windows.

Následující typy mají stejnou velikost u 32bitových a 64bitových Windows bez ohledu na jejich názvy.

Šířka Windows jazyk C# Alternativa
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 Viz CLong a CULong.
32 LONG32 int
32 CLONG uint Viz CLong a CULong.
32 DWORD uint Viz CLong a CULong.
32 DWORD32 uint
32 UINT uint
32 UINT32 uint
32 ULONG uint Viz CLong a 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

Následující typy, které jsou ukazateli, se řídí šířkou platformy. Používá se IntPtr/UIntPtr pro tyto účely.

Typy podepsaných ukazatelů (použití IntPtr) Neznaménkové typy ukazatelů (použití UIntPtr)
HANDLE WPARAM
HWND UINT_PTR
HINSTANCE ULONG_PTR
LPARAM SIZE_T
LRESULT
LONG_PTR
INT_PTR

Windows PVOID, což je C void*, lze zařaďovat jako IntPtr nebo UIntPtr, ale pokud je to možné, raději void*.

datové typy Windows

Rozsahy datových typů

Dříve předdefinované podporované typy

Jsou vzácné případy, kdy je odstraněna integrovaná podpora pro nějaký typ.

Podpora integrovaného marshalu ve verzích UnmanagedType.HString a UnmanagedType.IInspectable byla odebrána ve verzi .NET 5. Binární soubory, které používají tento typ zařazování a které cílí na předchozí architekturu, je nutné překompilovat. Tento typ je stále možné zařažovat, ale musíte ho zařadovat ručně, jak ukazuje následující příklad kódu. Tento kód bude fungovat i v budoucnu a bude také kompatibilní s předchozími frameworky.

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

Důležité informace o datových typech napříč platformami

Existují typy v jazyce C/C++, které mají zeměpisnou šířku v tom, jak jsou definovány. Při psaní cross-platform inter operability mohou nastat případy, kdy se platformy liší a mohou způsobit problémy, pokud nejsou zohledněny.

C/C++ long

C/C++ long a C# long nemusí mít nutně stejnou velikost.

Typ long v jazyce C/C++ je definován tak, aby měl alespoň 32 bitů. To znamená, že existuje minimální počet požadovaných bitů, ale v případě potřeby se platformy můžou rozhodnout použít více bitů. Následující tabulka ukazuje rozdíly v zadaných bitech datového typu C/C++ long mezi platformami.

Platforma 32bitová 64bitová
Windows 32 32
macOS/*nix 32 64

Naproti tomu jazyk C# long je vždy 64 bitů. Z tohoto důvodu je nejlepší se vyhnout použití jazyka C# long ke spolupráci s C/C++ long.

(Tento problém s C/C++ long neexistuje pro C/C++ char, shorta intlong long protože jsou 8, 16, 32 a 64 bitů na všech těchto platformách.)

V .NET 6 a novějších verzích použijte typy CLong a CULong pro spolupráci s datovými typy C/C++ long a unsigned long. Následující příklad je pro CLong, ale můžete použít CULong k abstrakci unsigned long podobným způsobem.

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

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

Při cílení na .NET 5 a starší verze byste měli deklarovat samostatné podpisy pro Windows a non-Windows systémy pro řešení problému.

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

Spravované struktury se vytvoří v zásobníku a neodeberou se, dokud metoda nevrátí. Podle definice se pak "připnou" (nepřesune se GC). Adresu můžete také jednoduše získat v blocích nebezpečného kódu, pokud nativní kód nebude používat ukazatel po skončení aktuální metody.

Blittable struktury jsou mnohem výkonnější, protože je lze jednoduše použít přímo ve vrstvě marshalingu. Pokuste se, aby struktury byly blittable (například se vyhněte bool). Další informace najdete v části Blittable typy.

Pokud je struktura blittable, použijte sizeof() místo Marshal.SizeOf<MyStruct>() pro lepší výkon. Jak je uvedeno výše, můžete ověřit, že typ je blitovatelný pokusem o vytvoření připnutého GCHandle. Pokud typ není řetězec nebo považován za blittable, GCHandle.Alloc vyvolá chybu ArgumentException.

Ukazatele na struktury v definicích musí být předány buď pomocí ref, nebo pomocí unsafe a *.

✔️ Snažte se, aby se spravovaná struktura co nejvíce shodovala s tvarem a názvy, které se používají v oficiální dokumentaci platformy nebo v její hlavičce.

✔️ K lepšímu výkonu používejte jazyk C# sizeof() místo Marshal.SizeOf<MyStruct>() pro blitelné struktury.

❌ Nespoléhejte se na interní reprezentaci typů struktur vystavených knihovnami modulu runtime .NET, pokud nejsou explicitně zdokumentovány.

❌ Vyhněte se použití tříd k vyjádření složitých nativních typů prostřednictvím dědičnosti.

❌ Vyhněte se použití System.Delegate polí nebo System.MulticastDelegate polí k reprezentaci polí ukazatelů funkce ve strukturách.

Vzhledem k tomu, že System.Delegate a System.MulticastDelegate nemají požadovaný podpis, nezaručují, že předaný delegát bude odpovídat podpisu, který nativní kód očekává. Kromě toho může v rozhraní .NET Framework a .NET Core převod struktury obsahující System.Delegate nebo System.MulticastDelegate z její nativní reprezentace na spravovaný objekt destabilizovat modul runtime, pokud hodnota pole v nativní reprezentaci není ukazatel funkce, který obaluje spravovaný delegát. V .NET 5 a novějších verzích není podporováno zařazování pole System.Delegate nebo System.MulticastDelegate z nativní reprezentace do spravovaného objektu. Použijte konkrétní typ delegáta místo System.Delegate nebo System.MulticastDelegate.

Pevné vyrovnávací paměti

Pole jako INT_PTR Reserved1[2] musí být zpracováno do dvou položek IntPtr, Reserved1a a Reserved1b. Pokud je nativní pole primitivním typem, můžeme klíčové slovo použít fixed k jeho zápisu trochu čistěji. SYSTEM_PROCESS_INFORMATION Například vypadá takto v nativní hlavičce:

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

V jazyce C# ji můžeme napsat takto:

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

Jedná se však o určité problémy spojené s pevnými buffery. Pevné vyrovnávací paměti neschytitelných typů nebudou správně zařazovány, takže je potřeba místní pole rozšířit na více jednotlivých polí. Kromě toho v rozhraní .NET Framework a .NET Core před 3.0 platí, že pokud je struktura obsahující pevné pole vyrovnávací paměti vnořená do neslitovatelné struktury, nebude pole pevné vyrovnávací paměti správně zařazováno do nativního kódu.

Řešení potíží při selhání P/Invoke volání

Následující tabulka mapuje běžné příznaky jejich pravděpodobné příčiny a doporučené opravy.

Symptom Pravděpodobná příčina Oprava
DllNotFoundException Knihovna nebyla nalezena za běhu Zkontrolujte název knihovny, cestu a platformu. Použijte TryLoad k otestování načítání. V Linuxu ověřte LD_LIBRARY_PATH nebo rpath.
EntryPointNotFoundException Neshoda názvů exportu Kontrola nativních exportů (dumpbin /exports na Windows, nm -D v Linuxu). Kontrola manglování názvu C++ (chybí extern "C"). Explicitně nastavte EntryPoint .
AccessViolationException Nesoulad podpisů, použití po uvolnění paměti nebo chybějící fixace Porovnejte spravované a nativní podpisy. Zkontrolujte velikosti struktur pomocí Marshal.SizeOf<T>() oproti nativním sizeof. Ověřte životnost paměti. Použijte blitovatelný podpis k řešení problému s maršálováním
Tiché poškození dat Nesprávná velikost nebo kódování typu Přidejte protokolování hranic. Porovnat Marshal.SizeOf<T>() s nativním sizeof. Testujte se známými vstupně-výstupními páry.
Občasné pády GC přesunul nepřipnutý objekt nebo uvolnil delegáta. Kořenové zpětné volání delegátů po celou dobu jejich životnosti. Použijte GCHandle nebo fixed pro ukazatele uchovávané napříč voláními.
Poškození haldy zdarma Chybný alokátor Shoda s alokátorem: nikdy nemíchejte malloc/free s CoTaskMemAlloc/CoTaskMemFree nebo Marshal.FreeHGlobal. Použijte vlastní bezplatnou funkci knihovny.

Zabránění shromažďování delegátů pomocí GC.KeepAlive

Pokud použijete GetFunctionPointerForDelegate pro převod delegáta na ukazatel funkce, systém správy paměti nesleduje vztah mezi vráceným ukazatelem a zdrojovým delegátem. Pokud má delegát nárok na shromažďování před dokončením nativního kódu pomocí ukazatele, aplikace se chybově ukončí.

Slouží KeepAlive k zabránění shromažďování:

var callback = new MyDelegate((level, msgPtr) =>
{
    string msg = Marshal.PtrToStringUTF8(msgPtr) ?? string.Empty;
    Console.WriteLine($"[{level}] {msg}");
});

IntPtr fnPtr = Marshal.GetFunctionPointerForDelegate(callback);
NativeUsesCallback(fnPtr);
GC.KeepAlive(callback); // Prevent collection — fnPtr does not root the delegate

Pokud nativní kód ukládá ukazatel funkce po skončení volání (například jako trvalý callback), musí mít delegát během své životnosti přiřazený kořen – obvykle uložením static delegáta do pole.

Řešení konfliktů mezi dokumentací a nativními hlavičkami

Při psaní podpisů P/Invoke je možné zaznamenat nesrovnalosti mezi online dokumentací k rozhraní API a skutečnými nativními hlavičkovými soubory. Soubory hlaviček jsou autoritativním zdrojem podpisů funkcí, rozložení struktur, velikostí typů a konvencí volání. Pokud máte pochybnosti, ověřte P/Invoke podpisy s hlavičkou a nespoléhejte se pouze na dokumentaci.