Share via


Självstudie: Använda anpassade marshallers i källgenererade P/Invokes

I den här självstudien får du lära dig hur du implementerar en marshaller och använder den för anpassad marshalling i källgenererade P/Invokes.

Du implementerar marshallers för en inbyggd typ, anpassar marshalling för en specifik parameter och en användardefinierad typ och anger standard marshalling för en användardefinierad typ.

All källkod som används i den här självstudien är tillgänglig på lagringsplatsen dotnet/samples.

LibraryImport Översikt över källgeneratorn

Typen System.Runtime.InteropServices.LibraryImportAttribute är användarens startpunkt för en källgenerator som introducerades i .NET 7. Den här källgeneratorn är utformad för att generera all marshallingkod vid kompileringstillfället i stället för vid körning. Startpunkter har tidigare angetts med hjälp av DllImport, men den metoden medför kostnader som kanske inte alltid är acceptabla– mer information finns i P/Invoke source generation (P/Invoke source generation). Källgeneratorn LibraryImport kan generera all marshallingkod och ta bort körningsgenereringskravet som är inbyggt i DllImport.

För att uttrycka den information som behövs för att generera kod för marshalling både för körningen och för användare att anpassa för sina egna typer, behövs flera typer. Följande typer används i den här självstudien:

  • MarshalUsingAttribute – Attribut som söks av källgeneratorn på användningsplatser och används för att fastställa marshallertypen för att ordna den tilldelade variabeln.

  • CustomMarshallerAttribute – Attribut som används för att ange en marshaller för en typ och i vilket läge marshallingåtgärderna ska utföras (till exempel by-ref från hanterad till ohanterad).

  • NativeMarshallingAttribute – Attribut som används för att ange vilken marshaller som ska användas för den tilldelade typen. Detta är användbart för biblioteksförfattare som tillhandahåller typer och tillhörande marshallers för dessa typer.

Dessa attribut är dock inte de enda mekanismerna som är tillgängliga för en anpassad marshallerförfattare. Källgeneratorn inspekterar själva marshallern för olika andra indikationer som informerar om hur marshalling ska ske.

Fullständig information om designen finns i dotnet/runtime-lagringsplatsen .

Källgeneratoranalysator och korrigering

Tillsammans med själva källgeneratorn tillhandahålls både en analysator och en korrigering. Analysatorn och korrigeringsverktyget är aktiverade och tillgängliga som standard sedan .NET 7 RC1. Analysatorn är utformad för att hjälpa utvecklare att använda källgeneratorn korrekt. Korrigeringsverktyget tillhandahåller automatiserade konverteringar från många DllImport mönster till lämplig LibraryImport signatur.

Introduktion till det interna biblioteket

LibraryImport Att använda källgeneratorn skulle innebära att ett internt eller ohanterat bibliotek förbrukas. Ett internt bibliotek kan vara ett delat bibliotek (dvs. .dll, .so, eller dylib) som direkt anropar ett operativsystem-API som inte exponeras via .NET. Biblioteket kan också vara ett som är mycket optimerat på ett ohanterat språk som en .NET-utvecklare vill använda. I den här självstudien skapar du ett eget delat bibliotek som exponerar en API-yta i C-stil. Följande kod representerar en användardefinierad typ och två API:er som du ska använda från C#. Dessa två API:er representerar läget "in", men det finns ytterligare lägen att utforska i exemplet.

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

Föregående kod innehåller de två typerna av intresse och char32_t*error_data. char32_t* representerar en sträng som är kodad i UTF-32, vilket inte är en strängkodning som .NET historiskt sett konverterar. error_data är en användardefinierad typ som innehåller ett 32-bitars heltalsfält, ett C++-booleskt fält och ett UTF-32-kodat strängfält. Båda dessa typer kräver att du tillhandahåller ett sätt för källgeneratorn att generera kod för marshalling.

Anpassa marshalling för en inbyggd typ

Tänk på typen char32_t* först eftersom marshalling av den här typen krävs av den användardefinierade typen. char32_t* representerar den inbyggda sidan, men du behöver också representation i hanterad kod. I .NET finns det bara en strängtyp, string. Därför kommer du att samla en inbyggd UTF-32-kodad sträng till och från string typen i hanterad kod. Det finns redan flera inbyggda marshallers för den string typ som marskalk som UTF-8, UTF-16, ANSI och även som Windows-typ BSTR . Det finns dock ingen för marshalling som UTF-32. Det är det du behöver definiera.

Typen Utf32StringMarshaller är markerad med ett CustomMarshaller attribut som beskriver vad den gör med källgeneratorn. Det första typargumentet till attributet är string typen, den hanterade typen som ska marskalkas, det andra är läget, vilket anger när marshaller ska användas och den tredje typen är Utf32StringMarshaller, den typ som ska användas för marshalling. Du kan använda flera CustomMarshaller gånger för att ytterligare ange läget och vilken marshallertyp som ska användas för det läget.

Det aktuella exemplet visar en "tillståndslös" marshaller som tar vissa indata och returnerar data i marshallformat. Metoden Free finns för symmetri med ohanterad marshalling och skräpinsamlaren är den "kostnadsfria" åtgärden för den hanterade marshallern. Implementeraren kan utföra de åtgärder som önskas för att konvertera indata till utdata, men kom ihåg att inget tillstånd uttryckligen bevaras av källgeneratorn.

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

Detaljerna för hur den här marshallern utför konverteringen från string till char32_t* finns i exemplet. Observera att alla .NET-API:er kan användas (till exempel Encoding.UTF32).

Överväg ett fall där tillstånd är önskvärt. Observera ytterligare CustomMarshaller och notera det mer specifika läget, MarshalMode.ManagedToUnmanagedIn. Den här specialiserade marshallern implementeras som "tillståndskänslig" och kan lagra tillstånd över interop-anropet. Mer specialisering och tillstånd tillåter optimeringar och skräddarsydd marshalling för ett läge. Till exempel kan källgeneratorn instrueras att tillhandahålla en stackallokerad buffert som kan undvika en explicit allokering under marshalling. För att ange stöd för en stackallokerad buffert implementerar marshaller en BufferSize egenskap och en FromManaged metod som tar en av en unmanagedSpan typ. Egenskapen BufferSize anger mängden stackutrymme – längden på den Span som ska skickas till FromManaged– som marshallern vill få under marskalksanropet.

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

Nu kan du anropa den första av de två inbyggda funktionerna med hjälp av dina UTF-32-sträng marshallers. Följande deklaration använder LibraryImport attributet, precis som DllImport, men förlitar sig på MarshalUsing attributet för att tala om för källgeneratorn vilken marshaller som ska användas när den interna funktionen anropas. Det finns ingen anledning att klargöra om den tillståndslösa eller tillståndskänsliga marshallern ska användas. Detta hanteras av implementeraren som MarshalMode definierar på marshaller-attributen CustomMarshaller . Källgeneratorn väljer den lämpligaste marshallern baserat på den kontext där MarshalUsing används, med MarshalMode.Default återställningen.

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

Anpassa marshalling för en användardefinierad typ

För att kunna ordna en användardefinierad typ måste du definiera inte bara marshallinglogik, utan även typen i C# att konvertera till/från. Kom ihåg den inbyggda typen som vi försöker marskalka.

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

Definiera nu hur det skulle se ut i C#. En int har samma storlek i både modern C++ och i .NET. A bool är det kanoniska exemplet på ett booleskt värde i .NET. Om du bygger ovanpå Utf32StringMarshallerkan du marskalka char32_t* som .NET string. Resultatet är följande definition i C#:

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

Efter namngivningsmönstret namnger du marshallern ErrorDataMarshaller. I stället för att ange en marshaller för MarshalMode.Defaultdefinierar du bara marshallers för vissa lägen. I det här fallet, om marshaller används för ett läge som inte tillhandahålls, misslyckas källgeneratorn. Börja med att definiera en marshaller för "i"-riktningen. Detta är en "statslös" marshaller eftersom marshallern själv bara består av static funktioner.

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 efterliknar formen på den ohanterade typen. Konverteringen från en ErrorData till en ErrorDataUnmanaged är nu trivial med Utf32StringMarshaller.

Marshalling av en int är onödig eftersom dess representation är identisk i ohanterad och hanterad kod. Ett bool värdes binära representation definieras inte i .NET, så använd dess aktuella värde för att definiera ett noll- och icke-nollvärde i den ohanterade typen. Återanvänd sedan DIN UTF-32-marshaller för att konvertera fältet string till en uint*.

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

Kom ihåg att du definierar denna marshaller som en "in", så du måste rensa alla allokeringar som utförs under marshalling. Fälten int och bool allokerar inget minne, men det gjorde fältet Message . Återanvänd igen Utf32StringMarshaller för att rensa den marshallerade strängen.

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

Vi tar en kort titt på "out"-scenariot. Tänk på det fall där en eller flera instanser av error_data returneras.

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

En P/Invoke som returnerar en enskild instanstyp, icke-samling, kategoriseras som en MarshalMode.ManagedToUnmanagedOut. Vanligtvis använder du en samling för att returnera flera element, och i det här fallet används en Array . Marshaller för ett samlingsscenario, som motsvarar MarshalMode.ElementOut läget, returnerar flera element och beskrivs senare.

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

Konverteringen från ErrorDataUnmanaged till ErrorData är inverteringen av det du gjorde för läget "in". Kom ihåg att du också måste rensa alla allokeringar som den ohanterade miljön förväntade dig att utföra. Det är också viktigt att notera att funktionerna här är markerade static och därför är "tillståndslösa", att vara tillståndslös är ett krav för alla elementlägen. Du kommer också att märka att det finns en ConvertToUnmanaged metod som i läget "in". Alla elementlägen kräver hantering för både in- och out-lägen.

För den hanterade ohanterade "out" marshaller, kommer du att göra något speciellt. Namnet på den datatyp som du samlar in anropas error_data och .NET uttrycker vanligtvis fel som undantag. Vissa fel är mer effektfulla än andra och fel som identifieras som "allvarliga" indikerar vanligtvis ett oåterkalleligt eller oåterkalleligt fel. Observera att error_data det finns ett fält för att kontrollera om felet är allvarligt. Du konverterar en error_data till hanterad kod, och om den är dödlig utlöser du ett undantag i stället för att bara konvertera den till en ErrorData och returnera den.

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

En "out"-parameter konverterar från en ohanterad kontext till en hanterad kontext, så du implementerar ConvertToManaged metoden. När den ohanterade anroparen returnerar och tillhandahåller ett ErrorDataUnmanaged objekt kan du inspektera det med hjälp av din ElementOut läges-marshaller och kontrollera om det har markerats som ett allvarligt fel. I så fall är det din indikation att kasta i stället för att ErrorDatabara returnera .

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

    return data;
}

Kanske kommer du inte bara att använda det interna biblioteket, utan du vill också dela ditt arbete med communityn och tillhandahålla ett interop-bibliotek. Du kan ange ErrorData en underförstådd marshaller när den används i en P/Invoke genom att lägga [NativeMarshalling(typeof(ErrorDataMarshaller))] till i ErrorData definitionen. Nu får alla som använder din definition av den här typen i ett LibraryImport samtal förmånen av dina marshallers. De kan alltid åsidosätta dina marshallers med hjälp MarshalUsing av på användningsplatsen.

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

Se även