Osvědčené postupy nativní interoperability

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

Obecné pokyny

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

  • ✔️ Pro metody a parametry jako nativní metodu, kterou chcete volat, použijte stejné pojmenování a velká písmena.
  • ✔️ ZVAŽTE použití stejného pojmenování a velká písmena pro konstantní hodnoty.
  • ✔️ Použijte typy .NET, které se mapují nejblíže 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.
  • ✔️ Do use [In] and [Out] attributes on array parameters.
  • ✔️ Používejte [In] pouze atributy u [Out] 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í volání nespravovaného kódu do třídy se stejným názvem a velkými písmeny jako nativní knihovna.
    • To umožňuje, aby atributy [DllImport] používaly funkci jazyka C# nameof , aby předaly název nativní knihovny a zajistily, že jste nezadali chybně napsaný název nativní knihovny.

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átor (zadaný v dokumentaci ke znakové sadě) Explicitní použití CharSet.Unicode nebo CharSet.Ansi použití řetězců nebo znaků v definici 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. Ve většině případů systém Windows používá Unicode, zatímco Unix používá UTF8. Další informace najdete v dokumentaci ke znakové sadě.
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

Pokud je ZnakSet Unicode nebo je argument explicitně označený jako [MarshalAs(UnmanagedType.LPWSTR)]a řetězec se předá hodnotou (ne ref nebo out), řetězec se připne a použije přímo nativním kódem (místo zkopírování).

❌ Nepoužívejte [Out] string parametry. Řetězcové parametry předané hodnotou s [Out] atributem můžou modul runtime synchronizovat, pokud je řetězec internedilovaným řetězcem. Další informace o prokládání řetězců naleznete v dokumentaci pro String.Intern.

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

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

✔️ 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 systému 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í hodnota parametru)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 je {4} přidělení 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 do mezipaměti vyrovnávací paměť znaků z ArrayPool. Pak se můžete dostat jenom na přidělení pro ToString() následná volání.

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

Pokud použijeteStringBuilder, jedna poslední gotcha je, že kapacita neobsahuje skrytou hodnotu null, která je vždy zohledněny v interoperabilitě. Je běžné, že lidé tuto chybu získají, protože většina rozhraní API chce 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 chyba brání modulu runtime v optimalizaci StringBuilder zařazování, aby se minimalizovaly kopie.

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

Windows Specific Pro [Out] řetězce CLR bude ve výchozím nastavení používat CoTaskMemFree volné řetězce nebo SysStringFree řetězce, které jsou označeny 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, získat 4: Řetězec je dlouhý 4 znaky s koncovou hodnotou null.
  • Předat 5, získat 6: Řetězec je dlouhý 5 znaků, k uložení hodnoty null 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í je .NET bool zařazován do Windows BOOL, kde se jedná o 4 bajtovou hodnotu. Nicméně _Bool, a typy v C a bool C++ jsou jeden bajt. To může vést k obtížnému sledování chyb, protože polovina návratové hodnoty bude zahozena, což může změnit pouze výsledek. Další informace o zařazování hodnot .NET bool do typů C nebo C++ bool najdete v dokumentaci k přizpůsobení logického zařazování polí.

Guid

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

Identifikátor GUID Identifikátor GUID podle odkazu
KNOWNFOLDERID REFKNOWNFOLDERID

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

Blittable types

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 jsou známé, že obsahují obsah blittable. Tyto typy mají podobné optimalizace jako blittable typy, pokud nejsou obsaženy v jiném typu, ale nejsou považovány za blittable, pokud jsou v polích struktur nebo pro účely UnmanagedCallersOnlyAttribute.

Typy Blittable při povolení zařazování za běhu

Blittable types:

  • byte, sbyte, , short, intushort, uintlongulong, , singledouble
  • 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 obsahem s blittable:

  • 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í

NOT blittable:

  • bool

NĚKDY blittable:

  • char

Typy s obsahem NĚKDY blittable:

  • string

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

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

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

string obsahuje blittable obsah, pokud není obsažen v jiném typu a předává se jako argument, který je označený [MarshalAs(UnmanagedType.LPWStr)] nebo [DllImport] je CharSet = CharSet.Unicode nastaven.

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

Blittable types when runtime marshalling is disabled

Když je zařazování za běhu zakázané, pravidla, pro které typy jsou blittable, 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 typy jazyka C# unmanaged , nejsou blitelné. 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 při zařazování za běhu je zakázáno.

Tato pravidla se liší od integrovaného systému především v situacích, kdy bool se používají a char používají. Při zařazování se bool předává jako 1 bajtová hodnota a není normalizována a char vždy se předává jako 2 bajtová hodnota. Pokud je povoleno zařazování za běhu, bool může se mapovat na hodnotu 1, 2 nebo 4 bajtů a je vždy normalizována a char v závislosti na hodnotě 1 nebo 2 bajtů se mapuje na CharSethodnotu 1 nebo 2 bajtů .

✔️ 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í nespravovaného kódu. 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 explicitně uvolnit, aby nedošlo k nevracení paměti.

Běžné datové typy Windows

Tady je seznam datových typů, které se běžně používají v rozhraních API systému Windows a které typy jazyka C#, které se mají použít při volání do kódu Windows.

Následující typy mají stejnou velikost v 32bitovém a 64bitovém systému Windows bez ohledu na jejich názvy.

Šířka Windows 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) Typy bez znaménka ukazatele (použití UIntPtr)
HANDLE WPARAM
HWND UINT_PTR
HINSTANCE ULONG_PTR
LPARAM SIZE_T
LRESULT
LONG_PTR
INT_PTR

Systém Windows PVOID, který je C void*, může být zařazován jako buď IntPtr nebo UIntPtr, ale preferovat void* , pokud je to možné.

Datové typy Windows

Rozsahy datových typů

Dříve předdefinované podporované typy

Při odebrání integrované podpory typu existují vzácné instance.

Podpora UnmanagedType.HString integrovaného UnmanagedType.IInspectable zařazování 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 pokračovat a bude také kompatibilní s předchozími architekturami.

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í vzájemné spolupráce mezi platformami můžou nastat případy, kdy se platformy liší a můžou způsobovat problémy, pokud se nepovažují.

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 CLong a CULong typy pro spolupráci s C/C++ long a unsigned long datovými typy. 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 systému Windows a jiné než Windows, které problém řeší.

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 vzít v nebezpečných blocích kódu, pokud nativní kód nebude používat ukazatel za koncem aktuální metody.

Blittable struktury jsou mnohem výkonnější, protože je lze jednoduše použít přímo ve vrstvě zařazování. Pokuste se vytvořit strukturu blittable (například vyhnout bool). Další informace najdete v části Typy tabulky Blittable .

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 blitelný 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 ref buď pomocí, nebo použití unsafe a *.

✔️ DO se co nejblíže shoduje se spravovanou strukturou s tvarem a názvy, které se používají v oficiální dokumentaci nebo hlavičce platformy.

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

❌ 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 System.Delegate , že a System.MulticastDelegate nemají požadovaný podpis, nezaručují, že delegát předaný bude odpovídat podpisu, který nativní kód očekává. Kromě toho může v rozhraní .NET Framework a .NET Core zařazování struktury obsahující System.Delegate objekt nebo System.MulticastDelegate z jeho nativní reprezentace do spravovaného objektu spustit modul runtime, pokud hodnota pole v nativní reprezentaci není ukazatel funkce, který zabalí spravovaný delegát. V .NET 5 a novějších verzích není podporováno zařazování System.Delegate nebo System.MulticastDelegate pole z nativní reprezentace do spravovaného objektu. System.Delegate Místo nebo System.MulticastDelegate.

Pevné vyrovnávací paměti

Pole, jako INT_PTR Reserved1[2] je, musí být zařazováno na dvě IntPtr pole, 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;
    ...
}

Existují však některé chytáky s pevnými vyrovnávacími paměťmi. 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á vnořená do nelimitovatelné struktury, pevné pole vyrovnávací paměti nebude správně zařazováno do nativního kódu.