Kurz: Použití vlastních zařazovačů ve zdrojovém vygenerovaném volání nespravovaného kódu

V tomto kurzu se dozvíte, jak implementovat marshaller a použít ho pro vlastní zařazování ve zdrojově generovaných voláních P/Invokes.

Implementujete marshallery pro předdefinovaný typ, přizpůsobíte řazení pro konkrétní parametr a uživatelem definovaný typ a zadáte výchozí zařazování pro uživatelem definovaný typ.

Veškerý zdrojový kód použitý v tomto kurzu je k dispozici v úložišti dotnet/samples.

Přehled generátoru LibraryImport zdrojů

Typ System.Runtime.InteropServices.LibraryImportAttribute je vstupní bod uživatele pro generátor zdroje zavedený v .NET 7. Tento zdrojový generátor je navržený tak, aby místo za běhu vygeneroval veškerý zařazovaný kód v době kompilace. Vstupní body byly historicky zadány pomocí DllImport, ale tento přístup přichází s náklady, které nemusí být vždy přijatelné – další informace najdete v tématu Generování zdroje volání nespravovaného kódu. Generátor LibraryImport zdroje může vygenerovat veškerý zařazovací kód a odebrat požadavek na generování za běhu vnitřní do DllImport.

K vyjádření podrobností potřebných k vygenerování kódu pro modul runtime i pro uživatele, kteří si můžou přizpůsobit vlastní typy, je potřeba několik typů. V tomto kurzu se používají následující typy:

  • MarshalUsingAttribute – Atribut, který hledá generátor zdroje při použití lokality a používá se k určení typu marshaller pro zařazování atributu proměnné.

  • CustomMarshallerAttribute – Atribut použitý k označení marshalleru pro typ a režim, ve kterém se mají provádět operace seřazování (například podle odkazu ze spravovaného do nespravovaného).

  • NativeMarshallingAttribute – Atribut použitý k označení, který marshaller se má použít pro atributovaný typ. To je užitečné pro autory knihoven, kteří pro tyto typy poskytují typy a doprovodné marshallery.

Tyto atributy ale nejsou jedinými mechanismy, které jsou k dispozici vlastnímu autorovi marshalleru. Generátor zdrojů zkontroluje samotný marshaller a hledá různé další indikace, které informují o tom, jak by mělo dojít k zařazování.

Kompletní podrobnosti o návrhu najdete v úložišti dotnet/runtime .

Analyzátor generátoru zdrojového kódu a opravovač

Kromě samotného generátoru zdroje jsou k dispozici analyzátor i fixer. Analyzátor a fixer jsou ve výchozím nastavení povolené a dostupné od verze .NET 7 RC1. Analyzátor je navržený tak, aby vývojářům pomohl správně používat generátor zdrojů. Fixer poskytuje automatizované převody z mnoha DllImport vzorů do příslušného LibraryImport podpisu.

Představujeme nativní knihovnu

Použití generátoru LibraryImport zdrojů by znamenalo využívání nativní nebo nespravované knihovny. Nativní knihovna může být sdílená knihovna (tj .dll. , .so, nebo dylib), která přímo volá rozhraní API operačního systému, které není vystaveno prostřednictvím .NET. Knihovna může být také ta, která je silně optimalizovaná v nespravovaném jazyce, který chce vývojář .NET využívat. V tomto kurzu vytvoříte vlastní sdílenou knihovnu, která zpřístupňuje plochu rozhraní API ve stylu jazyka C. Následující kód představuje uživatelem definovaný typ a dvě rozhraní API, která budete využívat z jazyka C#. Tato dvě rozhraní API představují režim "in", ale v ukázce existují další režimy.

struct error_data
{
    int code;
    bool is_fatal_error;
    char32_t* message;    /* UTF-32 encoded string */
};

extern "C" DLL_EXPORT void STDMETHODCALLTYPE PrintString(char32_t* chars);

extern "C" DLL_EXPORT void STDMETHODCALLTYPE PrintErrorData(error_data data);

Předchozí kód obsahuje dva typy zájmu char32_t* a error_data. char32_t* představuje řetězec kódovaný v kódování UTF-32, což není řetězcové kódování, které .NET historicky zařazuje. error_data je uživatelem definovaný typ, který obsahuje 32bitové celočíselné pole, logické pole jazyka C++ a pole řetězce s kódováním UTF-32. Oba tyto typy vyžadují, abyste zdrojovému generátoru poskytli způsob generování zařazování kódu.

Přizpůsobení zařazování pro předdefinovaný typ

char32_t* Nejprve zvažte typ, protože seřazování tohoto typu vyžaduje typ definovaný uživatelem. char32_t* představuje nativní stranu, ale potřebujete také reprezentaci ve spravovaném kódu. V .NET existuje pouze jeden typ "řetězec", string. Proto zařazujete nativní řetězec kódování UTF-32 do a z typu ve spravovaném string kódu. Pro typ, který zařazuje jako UTF-8, UTF-16, ANSI, a dokonce i jako typ WindowsBSTR, již existuje několik předdefinovaných zařazovačůstring. Pro zařazování jako UTF-32 ale neexistuje. To je to, co potřebujete definovat.

Typ Utf32StringMarshaller je označen atributem CustomMarshaller , který popisuje, co dělá se zdrojovým generátorem. Prvním argumentem typu atributu string je typ, spravovaný typ pro zařazování, druhý je režim, který označuje, kdy použít marshaller, a třetí typ je Utf32StringMarshallertyp, který se má použít pro seřazování. Vícekrát můžete použít CustomMarshaller k dalšímu určení režimu a typu marshalleru, který se má pro daný režim použít.

Aktuální příklad ukazuje "bezstavový" marshaller, který přebírá určitý vstup a vrací data v zařazované podobě. Metoda Free existuje pro symetrii s nespravovaným zařazováním a uvolňování paměti je "bezplatná" operace pro spravovaný marshaller. Implementátor je volný k provádění operací, které jsou žádoucí k zařazení vstupu do výstupu, ale nezapomeňte, že generátor zdroje explicitně nezachová žádný stav.

namespace CustomMarshalling
{
    [CustomMarshaller(typeof(string), MarshalMode.Default, typeof(Utf32StringMarshaller))]
    internal static unsafe class Utf32StringMarshaller
    {
        public static uint* ConvertToUnmanaged(string? managed)
            => throw new NotImplementedException();

        public static string? ConvertToManaged(uint* unmanaged)
            => throw new NotImplementedException();

        public static void Free(uint* unmanaged)
            => throw new NotImplementedException();
    }
}

Specifika způsobu, jakým tento konkrétní marshaller provádí převod z string toho, na který char32_t* se dá v ukázce najít. Všimněte si, že všechna rozhraní .NET API je možné použít (například Encoding.UTF32).

Zvažte případ, kdy je žádoucí stav. Sledujte další CustomMarshaller a poznamenejte si konkrétnější režim . MarshalMode.ManagedToUnmanagedIn Tento specializovaný marshaller se implementuje jako stavový a může ukládat stav napříč voláním zprostředkovatele komunikace. Další specializace a stav umožňují optimalizace a přizpůsobené zařazování pro režim. Zdrojový generátor může být například instruován, aby poskytl vyrovnávací paměť přidělenou zásobníkem, která by se mohla vyhnout explicitnímu přidělení během zařazování. Chcete-li označit podporu vyrovnávací paměti přidělené zásobníku, marshaller implementuje BufferSize vlastnost a metodu FromManaged , která přebírá Span typ unmanaged . Tato BufferSize vlastnost označuje množství prostoru zásobníku – délku Span , do FromManagedkteré se má předat – marshaller by chtěl dostat během zařazování.

namespace CustomMarshalling
{
    [CustomMarshaller(typeof(string), MarshalMode.Default, typeof(Utf32StringMarshaller))]
    [CustomMarshaller(typeof(string), MarshalMode.ManagedToUnmanagedIn, typeof(ManagedToUnmanagedIn))]
    internal static unsafe class Utf32StringMarshaller
    {
        //
        // Stateless functions removed
        //

        public ref struct ManagedToUnmanagedIn
        {
            public static int BufferSize => 0x100;

            private uint* _unmanagedValue;
            private bool _allocated; // Used stack alloc or allocated other memory

            public void FromManaged(string? managed, Span<byte> buffer)
                => throw new NotImplementedException();

            public uint* ToUnmanaged()
                => throw new NotImplementedException();

            public void Free()
                => throw new NotImplementedException();
        }
    }
}

Teď můžete volat první ze dvou nativních funkcí pomocí zařazovačů řetězců UTF-32. Následující deklarace používá LibraryImport atribut, stejně jako DllImport, ale spoléhá na MarshalUsing atribut informovat generátor zdroje, který marshaller použít při volání nativní funkce. Není nutné objasnit, jestli by se měl použít bezstavový nebo stavový marshaller. To zpracovává implementátor definující MarshalMode atributy marshalleru CustomMarshaller . Generátor zdroje vybere nejvhodnější marshaller na základě kontextu, ve kterém MarshalUsing se použije, s MarshalMode.Default tím, že je náhradní.

// extern "C" DLL_EXPORT void STDMETHODCALLTYPE PrintString(char32_t* chars);
[LibraryImport(LibName)]
internal static partial void PrintString([MarshalUsing(typeof(Utf32StringMarshaller))] string s);

Přizpůsobení zařazování pro uživatelem definovaný typ

Přiřazování uživatelem definovaného typu vyžaduje definování nejen logiky řazení, ale také typu v jazyce C# pro zařazování do/z. Připomeňme si nativní typ, který se pokoušíme zařazuje.

struct error_data
{
    int code;
    bool is_fatal_error;
    char32_t* message;    /* UTF-32 encoded string */
};

Teď definujte, jak by vypadala v jazyce C#. Je int stejná velikost v moderním jazyce C++ i v .NET. A bool je kanonický příklad logické hodnoty v .NET. Stavět nad Utf32StringMarshaller, můžete zařašovat char32_t* jako .NET string. Při účtování stylu .NET je výsledkem následující definice v jazyce C#:

struct ErrorData
{
    public int Code;
    public bool IsFatalError;
    public string? Message;
}

Podle vzoru pojmenování pojmenujte marshaller ErrorDataMarshaller. Místo zadání marshalleru pro MarshalMode.Defaultněkteré režimy definujete pouze marshallery. Pokud se v takovém případě použije marshaller pro režim, který není k dispozici, generátor zdroje selže. Začněte definováním marshalleru pro směr "in". Jedná se o "bezstavový" marshaller, protože se marshaller sám skládá pouze z static funkcí.

namespace CustomMarshalling
{
    [CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedIn, typeof(ErrorDataMarshaller))]
    internal static unsafe class ErrorDataMarshaller
    {
        // Unmanaged representation of ErrorData.
        // Should mimic the unmanaged error_data type at a binary level.
        internal struct ErrorDataUnmanaged
        {
            public int Code;        // .NET doesn't support less than 32-bit, so int is 32-bit.
            public byte IsFatal;    // The C++ bool is defined as a single byte.
            public uint* Message;   // This could be as simple as a void*, but uint* is closer.
        }

        public static ErrorDataUnmanaged ConvertToUnmanaged(ErrorData managed)
            => throw new NotImplementedException();

        public static void Free(ErrorDataUnmanaged unmanaged)
            => throw new NotImplementedException();
    }
}

ErrorDataUnmanaged napodobuje tvar nespravovaného typu. Převod z na an ErrorDataErrorDataUnmanaged je nyní triviální s Utf32StringMarshaller.

Zařazování je zbytečné, protože jeho reprezentace je stejná v nespravovaném a spravovaném int kódu. Binární bool reprezentace hodnoty není definována v .NET, takže použijte její aktuální hodnotu k definování nuly a nenulové hodnoty v nespravovaném typu. Potom znovu použijte UTF-32 marshaller k převodu string pole na .uint*

public static ErrorDataUnmanaged ConvertToUnmanaged(ErrorData managed)
{
    return new ErrorDataUnmanaged
    {
        Code = managed.Code,
        IsFatal = (byte)(managed.IsFatalError ? 1 : 0),
        Message = Utf32StringMarshaller.ConvertToUnmanaged(managed.Message),
    };
}

Vzpomeňte si, že tento zařazovač definujete jako "in", takže je nutné vyčistit všechna přidělení provedená během zařazování. Pole intbool nepřidělila žádnou paměť, ale Message pole ano. Znovu použijte Utf32StringMarshaller k vyčištění zařazovaného řetězce.

public static void Free(ErrorDataUnmanaged unmanaged)
    => Utf32StringMarshaller.Free(unmanaged.Message);

Pojďme se krátce podívat na scénář "out". Vezměte v úvahu případ, kdy se vrátí jedna nebo více instancí error_data .

extern "C" DLL_EXPORT error_data STDMETHODCALLTYPE GetFatalErrorIfNegative(int code)

extern "C" DLL_EXPORT error_data* STDMETHODCALLTYPE GetErrors(int* codes, int len)
[LibraryImport(LibName)]
internal static partial ErrorData GetFatalErrorIfNegative(int code);

[LibraryImport(LibName)]
[return: MarshalUsing(CountElementName = "len")]
internal static partial ErrorData[] GetErrors(int[] codes, int len);

Volání nespravovaného kódu, které vrací jeden typ instance, který není kolekcí, je kategorizován jako .MarshalMode.ManagedToUnmanagedOut Kolekci obvykle používáte k vrácení více prvků a v tomto případě se Array používá. Marshaller pro scénář kolekce, který odpovídá MarshalMode.ElementOut režimu, vrátí více prvků a je popsán později.

namespace CustomMarshalling
{
    [CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedIn, typeof(ErrorDataMarshaller))]
    [CustomMarshaller(typeof(ErrorData), MarshalMode.ElementOut, typeof(Out))]
    internal static unsafe class ErrorDataMarshaller
    {
        //
        // Other marshallers removed
        //

        public static class Out
        {
            public static ErrorData ConvertToManaged(ErrorDataUnmanaged unmanaged)
                => throw new NotImplementedException();

            public static ErrorDataUnmanaged ConvertToUnmanaged(ErrorData managed)
                => throw new NotImplementedException();

            public static void Free(ErrorDataUnmanaged unmanaged)
                => throw new NotImplementedException();
        }
    }
}

Převod z ErrorDataUnmanaged na ErrorData je inverzní funkce k tomu, co jste udělali pro režim "in". Nezapomeňte, že potřebujete také vyčistit všechny přidělení, které nespravované prostředí očekávalo, že provedete. Je také důležité si uvědomit, že zde uvedené funkce jsou označené static , a proto jsou bezstavové, bezstavové je požadavek pro všechny režimy elementů. Všimněte si také, že existuje ConvertToUnmanaged metoda jako v režimu "v". Všechny režimy "element" vyžadují zpracování pro režimy "in" i "out".

Pro správu nespravovaného "out" marshallera, budete dělat něco zvláštního. Název datového typu, který zařazujete, se volá error_data a .NET obvykle vyjadřuje chyby jako výjimky. Některé chyby jsou více ovlivněné než jiné a chyby identifikované jako "závažná" obvykle značí katastrofickou nebo neopravitelnou chybu. error_data Všimněte si, že je pole pro kontrolu, jestli je chyba závažná. Zařazujete ho do spravovaného error_data kódu, a pokud je závažná, vyvoláte výjimku, a ne jenom ji převedete na ErrorData spravovaný kód a vrátíte ho.

namespace CustomMarshalling
{
    [CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedIn, typeof(ErrorDataMarshaller))]
    [CustomMarshaller(typeof(ErrorData), MarshalMode.ElementOut, typeof(Out))]
    [CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedOut, typeof(ThrowOnFatalErrorOut))]
    internal static unsafe class ErrorDataMarshaller
    {
        //
        // Other marshallers removed
        //

        public static class ThrowOnFatalErrorOut
        {
            public static ErrorData ConvertToManaged(ErrorDataUnmanaged unmanaged)
                => throw new NotImplementedException();

            public static void Free(ErrorDataUnmanaged unmanaged)
                => throw new NotImplementedException();
        }
    }
}

Parametr out převede z nespravovaného kontextu na spravovaný kontext, takže implementujete metodu ConvertToManaged . Když se nespravovaný volaný vrátí a poskytne ErrorDataUnmanaged objekt, můžete ho zkontrolovat pomocí ElementOut zařazovače režimu a zkontrolovat, jestli je označený jako závažná chyba. Pokud ano, znamená to, že místo toho, aby se vrátila ErrorData.

public static ErrorData ConvertToManaged(ErrorDataUnmanaged unmanaged)
{
    ErrorData data = Out.ConvertToManaged(unmanaged);
    if (data.IsFatalError)
        throw new ExternalException(data.Message, data.Code);

    return data;
}

Možná budete používat nejen nativní knihovnu, ale také chcete sdílet svou práci s komunitou a poskytnout knihovnu vzájemné spolupráce. Implicitní marshaller můžete zadat ErrorData pokaždé, když se použije v volání nespravovaného kódu přidáním [NativeMarshalling(typeof(ErrorDataMarshaller))] do ErrorData definice. Teď každý, kdo použije vaši definici tohoto typu v LibraryImport hovoru, získá výhodu vašich seřaďovačů. Vždy můžou přepsat zařazovače pomocí MarshalUsing webu use.

[NativeMarshalling(typeof(ErrorDataMarshaller))]
struct ErrorData { ... }

Viz také