Dela via


Handledning: Använd anpassade marshallers i källgenererade P/Invokes

I den här handledningen 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 marshalleringskod vid kompilering 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 marshalling-kod och eliminera det körningstidsgenereringskrav som är inneboende i DllImport.

För att ange de detaljer som behövs för att generera marshallingkoden både för körning och för att tillåta användare att anpassa för sina egna typer, behövs flera typer. Följande typer används i denna handledning.

  • 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 marshallern själv för att hitta olika indikationer som informerar om hur marshalling bör ske.

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

Källgeneratoranalys och fixering

Tillsammans med själva källgeneratorn tillhandahålls både en analysator och en korrigerare. 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 kraftigt optimerat på ett ohanterat språk som en .NET-utvecklare vill utnyttja. 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, char32_t* och error_data. char32_t* anger en sträng som är kodad i UTF-32, vilket inte är en strängkodning som .NET historiskt sett marshalar. 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 marshallingkod.

Anpassa marshalling för en inbyggd typ

Tänk på typen char32_t* först eftersom hantering 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 hantera en inbyggd UTF-32-kodad sträng till och från den string typen i hanterad kod. Det finns redan flera inbyggda marshallers för den string typ som marshalleras som UTF-8, UTF-16, ANSI och till och med som Windows-typen 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 typen string, den hanterade typen som ska marskalkas. Det andra argumentet är läget, vilket anger när marskalkaren ska användas. Den tredje typen är Utf32StringMarshaller, den typ som ska användas för marskalkning. 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 ett marshallerat format. 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 en situation där tillståndet är önskvärt. Observera ytterligare CustomMarshaller och notera det mer specifika läget, MarshalMode.ManagedToUnmanagedIn. Den här specialiserade marshallern implementeras som "tillståndsorienterad" och kan lagra tillstånd över ett interop-anrop. Mer specialisering och statligt tillstånd möjliggör optimeringar och anpassad marshalling för ett specifikt läge. Till exempel kan källgeneratorn instrueras att tillhandahålla en buffert som är allokerad på stacken, vilket kan undvika en explicit allokering under överföring. För att ange stöd för en stackallokerad buffert implementerar marshaller en BufferSize-egenskap och en FromManaged-metod som tar en Span av en unmanaged-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ängmarshallers. 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. Behöver inte klargöra om den tillståndslösa eller tillståndsfulla marshallern ska användas. Detta hanteras av implementeraren genom att definiera MarshalMode på marshaller-attributens CustomMarshaller. Källgeneratorn väljer den lämpligaste marshallern baserat på kontexten där MarshalUsing tillämpas, med MarshalMode.Default som reserv.

// 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 hantera en användardefinierad typ måste du definiera inte bara hanteringslogiken, utan även typen i C# att definiera för 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å Utf32StringMarshaller, kan du marshall char32_t* som en .NET string. Med hänsyn till .NET-stil blir resultatet följande definition i C#:

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

Namnge marshallern enligt namngivningsmönstret 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 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å utgångsscenariot. 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 . För ett insamlingsscenario, som motsvarar MarshalMode.ElementOut-läget, kommer marshallern att returnera 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 städa upp 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 betydelsefulla än andra och fel som identifieras som "katastrofala" indikerar vanligtvis ett oåterkalleligt eller katastrofalt 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 en signal för dig att kasta i stället för att bara returnera ErrorData.

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 implicerad marshaller när den används i en P/Invoke genom att lägga [NativeMarshalling(typeof(ErrorDataMarshaller))] till ErrorData definitionen. Nu får alla som använder din definition av den här typen i ett LibraryImport samtal dra nytta av dina datahanterare. De kan alltid åsidosätta dina marshallers med hjälp av MarshalUsing på användningsplatsen.

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

Se även