Delen via


Zelfstudie: Aangepaste marshallers gebruiken in door de bron gegenereerde P/Invokes

In deze zelfstudie leert u hoe u een marshaller implementeert en gebruikt voor aangepaste marshalling in door de bron gegenereerde P/Invokes.

U implementeert marshallers voor een ingebouwd type, past marshalling aan voor een specifieke parameter en een door de gebruiker gedefinieerd type en geeft standaard marshalling op voor een door de gebruiker gedefinieerd type.

Alle broncode die in deze zelfstudie wordt gebruikt, is beschikbaar in de opslagplaats dotnet/samples.

Overzicht van de LibraryImport brongenerator

Het System.Runtime.InteropServices.LibraryImportAttribute type is het invoerpunt van de gebruiker voor een brongenerator die is geïntroduceerd in .NET 7. Deze brongenerator is ontworpen voor het genereren van alle marshallcode tijdens het compileren in plaats van tijdens runtime. Toegangspunten zijn historisch opgegeven met behulp van DllImport, maar die benadering wordt geleverd met kosten die mogelijk niet altijd acceptabel zijn. Zie P/Invoke-brongeneratie voor meer informatie. De LibraryImport brongenerator kan alle marshalling-code genereren en de behoefte aan run-time generatie die specifiek is voor DllImport verwijderen.

Verschillende typen zijn nodig om de details uit te drukken die vereist zijn voor het genereren van marshallcode, zowel voor de runtime als voor gebruikers die deze willen aanpassen voor hun eigen typen. In deze zelfstudie worden de volgende typen gebruikt:

  • MarshalUsingAttribute – Kenmerk dat wordt gezocht door de brongenerator op gebruikssites en gebruikt om het marshallertype te bepalen voor het marshallen van de toegeschreven variabele.

  • CustomMarshallerAttribute – Attribuut dat wordt gebruikt om een marshaller aan te geven voor een type en de modus waarin de marshallingbewerkingen moeten worden uitgevoerd (bijvoorbeeld via referentie van beheerd naar onbeheerd).

  • NativeMarshallingAttribute – Kenmerk gebruikt om aan te geven welke marshaller moet worden gebruikt voor het toegewezen type. Dit is handig voor bibliotheekauteurs die typen en bijbehorende marshallers voor deze typen bieden.

Deze attributen zijn echter niet de enige mechanismen die beschikbaar zijn voor de auteur van een aangepaste marshaller. De brongenerator inspecteert de marshaller zelf voor verschillende andere indicaties die aangeven hoe marshalling moet plaatsvinden.

Volledige details over het ontwerp vindt u in de dotnet/runtime-opslagplaats .

Brongeneratoranalyser en reparatie

Samen met de brongenerator zelf worden zowel een analyzer als een fixer geleverd. De analyzer en fixer zijn standaard ingeschakeld en beschikbaar sinds .NET 7 RC1. De analyzer is ontworpen om ontwikkelaars te helpen de brongenerator correct te gebruiken. De fixer biedt geautomatiseerde conversies van veel DllImport patronen in de juiste LibraryImport handtekening.

Introductie van de native bibliotheek

Het gebruik van de LibraryImport brongenerator zou betekenen dat een systeemeigen of onbeheerde bibliotheek wordt gebruikt. Een natuurlijke bibliotheek kan een gedeelde bibliotheek zijn (dat wil zeggen, .dll, .so, of dylib) die rechtstreeks een API van een besturingssysteem aanroept die niet beschikbaar is via .NET. De bibliotheek kan ook een bibliotheek zijn die sterk is geoptimaliseerd in een onbeheerde taal die een .NET-ontwikkelaar wil gebruiken. Voor deze zelfstudie bouwt u uw eigen gedeelde bibliotheek die een API-oppervlak in C-stijl beschikbaar maakt. De volgende code vertegenwoordigt een door de gebruiker gedefinieerd type en twee API's die u uit C# gaat gebruiken. Deze twee API's vertegenwoordigen de modus 'in', maar er zijn extra modi om in het voorbeeld te verkennen.

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

De voorgaande code bevat de twee typen van belang, char32_t* en error_data. char32_t* vertegenwoordigt een tekenreeks die is gecodeerd in UTF-32, een codering die door .NET historisch niet wordt verwerkt. error_data is een door de gebruiker gedefinieerd type dat een 32-bits geheel getalveld, een Booleaans C++ veld en een UTF-32 gecodeerd tekenreeksveld bevat. Voor beide typen moet u een manier opgeven zodat de brongenerator marshalling-code kan genereren.

Marshalling aanpassen voor een ingebouwd type

Overweeg eerst het char32_t* type, omdat het marshallen van dit type is vereist door het door de gebruiker gedefinieerde type. char32_t* vertegenwoordigt de systeemeigen kant, maar u hebt ook een weergave in beheerde code nodig. In .NET is er slechts één tekenreekstype, string. Daarom gaat u een UTF-32-gecodeerde string omzetten naar en van het string type in beheerde code. Er zijn al verschillende ingebouwde marshallers voor het string-type die marshal in UTF-8, UTF-16, ANSI en zelfs als het Windows BSTR-type. Er is echter geen mogelijkheid voor marshalling als UTF-32. Dat moet u definiëren.

Het Utf32StringMarshaller type wordt gemarkeerd met een CustomMarshaller kenmerk, dat beschrijft wat het doet met de brongenerator. Het eerste typeargument voor het kenmerk is het string type, het beheerde type om te marshallen, de tweede is de modus, die aangeeft wanneer de marshaller moet worden gebruikt, en het derde type is Utf32StringMarshaller, het type dat moet worden gebruikt voor marshalling. U kunt de CustomMarshaller meerdere keren toepassen om de modus verder op te geven en welk marshallertype voor die modus moet worden gebruikt.

In het huidige voorbeeld ziet u een 'statenloze' marshaller die bepaalde invoer aanneemt en gegevens retourneert in gemarshallede vorm. De Free methode bestaat voor symmetrie met de onbeheerde marshalling en de garbagecollector is de 'vrije' bewerking voor de beheerde marshaller. De implementator is vrij om alle bewerkingen uit te voeren die nodig zijn om de invoer naar de uitvoer toe te wijzen, maar onthoud dat er geen status expliciet wordt bewaard door de brongenerator.

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

De details van hoe deze marshaller de conversie uitvoert van string naar char32_t* vindt u in het voorbeeld. Houd er rekening mee dat eventuele .NET-API's kunnen worden gebruikt (bijvoorbeeld Encoding.UTF32).

Overweeg een geval waarin de staat wenselijk is. Bekijk de aanvullende CustomMarshaller en noteer de specifiekere modus. MarshalMode.ManagedToUnmanagedIn Deze gespecialiseerde marshaller wordt geïmplementeerd als stateful en kan de status opslaan tijdens de interop-aanroep. Meer specialisatie en overheidsvergunningen optimalisaties en maatwerk marshalling voor een modus. De brongenerator kan bijvoorbeeld worden geïnstrueerd om een stack-gealloceerde buffer te bieden die expliciete toewijzing tijdens marshalling kan voorkomen. Om aan te geven dat er ondersteuning is voor een stack-toegewezen buffer, implementeert de marshaller een BufferSize-eigenschap en een FromManaged-methode die een Span van een unmanaged-type neemt. De BufferSize eigenschap geeft de hoeveelheid stapelruimte aan die de marshaller wenst te ontvangen tijdens de marshal-aanroep, waarbij de lengte van de Span moet worden doorgegeven aan FromManaged.

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

U kunt nu de eerste van de twee systeemeigen functies aanroepen met behulp van uw UTF-32-tekenreeks marshallers. De volgende declaratie maakt gebruik van het LibraryImport kenmerk, net zoals DllImport, maar is afhankelijk van het MarshalUsing kenmerk om de brongenerator te vertellen welke marshaller moet gebruiken bij het aanroepen van de systeemeigen functie. Het is niet nodig om te verduidelijken of de staatloze of staatvolle marshaller moet worden gebruikt. Dit wordt afgehandeld door de implementator die de MarshalMode-kenmerken van de marshaller in CustomMarshaller definieert. De brongenerator selecteert de meest geschikte marshaler op basis van de context waarin MarshalUsing wordt toegepast, met MarshalMode.Default als terugval.

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

Marshalling aanpassen voor een door de gebruiker gedefinieerd type

Het marshallen van een door de gebruiker gedefinieerd type vereist het definiëren van niet alleen de marshallinglogica, maar ook het type in C# om naartoe/van te marshallen. Herinner je het natieve type dat we proberen te serialiseren.

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

Definieer nu hoe het eruit zou zien in C#. Een int is dezelfde grootte in zowel moderne C++ als in .NET. A bool is het canonieke voorbeeld voor een Booleaanse waarde in .NET. Bovenop Utf32StringMarshaller kunt u een marshal char32_t* als een .NET string gebruiken. Rekening houdend met de .NET-stijl, is het resultaat als volgt: in C# de volgende definitie:

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

Noem de marshaller ErrorDataMarshaller volgens het naamgevingspatroon. In plaats van een marshaller voor MarshalMode.Defaultop te geven, definieert u alleen marshallers voor sommige modi. In dit geval mislukt de brongenerator als de marshaller wordt gebruikt voor een modus die niet is opgegeven. Begin met het definiëren van een marshaller voor de 'in'-richting. Dit is een "stateless" marshaller omdat de marshaller zelf alleen uit static functies bestaat.

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 Bootst de vorm van het niet-beheerde type na. De conversie van een ErrorData naar een ErrorDataUnmanaged is nu triviaal met Utf32StringMarshaller.

Marshalling van een int is onnodig omdat de representatie identiek is in onbeheerde en beheerde code. De binaire weergave van een bool waarde is niet gedefinieerd in .NET, dus gebruik de huidige waarde om een nul- en niet-nulwaarde te definiëren in het niet-beheerde type. Gebruik vervolgens uw UTF-32 marshaller opnieuw om het string veld om te zetten in een uint*.

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

U herinnert zich nog dat u deze marshaller definieert als 'in', dus u moet alle toewijzingen opschonen die tijdens de marshalling worden uitgevoerd. De int velden en bool velden hebben geen geheugen toegewezen, maar het Message veld wel. Gebruik Utf32StringMarshaller opnieuw om de gemarshalled string op te schonen.

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

Laten we kort eens kijken naar het 'uit'-scenario. Houd rekening met het geval waarin een of meer exemplaren van error_data worden geretourneerd.

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

Een P/Invoke die één instantietype, geen verzameling, retourneert, wordt gecategoriseerd als een MarshalMode.ManagedToUnmanagedOut. Normaal gesproken gebruikt u een verzameling om meerdere elementen te retourneren, en in dit geval wordt er een Array gebruikt. De marshaller voor een verzamelingsscenario, die overeenkomt met de MarshalMode.ElementOut modus, retourneert meerdere elementen en wordt later beschreven.

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

De conversie van ErrorDataUnmanaged naar ErrorData is de inverse van wat u voor de modus 'in' hebt gedaan. Vergeet niet dat u ook toewijzingen moet opschonen die de onbeheerde omgeving van u verwachtte om door te voeren. Het is ook belangrijk om te weten dat de functies hier zijn gemarkeerd static en daarom staatloos zijn, staatloos zijn een vereiste voor alle elementmodi. U zult ook merken dat er een ConvertToUnmanaged methode is zoals in de modus 'in'. Voor alle "Element"-modi is afhandeling vereist voor zowel 'in'- als 'out'-modi.

Voor de overgang van beheerde naar onbeheerde "out" marshaller, ga je iets bijzonders doen. De naam van het gegevenstype dat u verwerkt, heet error_data en .NET geeft doorgaans fouten weer als uitzonderingen. Sommige fouten hebben meer invloed dan andere en fouten die als 'fataal' worden geïdentificeerd, geven meestal een catastrofale of onherstelbare fout aan. U ziet dat er error_data een veld is om te controleren of de fout fataal is. U zet een error_data om naar beheerde code, en als het fataal is, werpt u een exceptie in plaats van het simpelweg om te zetten naar een ErrorData en het te retourneren.

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

Een 'out'-parameter wordt geconverteerd van een niet-beheerde context naar een beheerde context, zodat u de ConvertToManaged methode implementeert. Wanneer de onbeheerde ontvanger terugkeert en een ErrorDataUnmanaged object levert, kunt u het inspecteren met uw ElementOut marshaller en controleren of het als een fatale fout is gemarkeerd. In dat geval, is dat je signaal om te werpen in plaats van simpelweg de ErrorData terug te geven.

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

    return data;
}

Misschien gaat u niet alleen de systeemeigen bibliotheek gebruiken, maar u wilt ook uw werk delen met de community en een interoperabiliteitsbibliotheek bieden. Je kunt een impliciete marshaller opgeven voor ErrorData wanneer deze wordt gebruikt in een P/Invoke door [NativeMarshalling(typeof(ErrorDataMarshaller))] toe te voegen aan de ErrorData-definitie. Iedereen die uw definitie van dit type in een LibraryImport oproep gebruikt, krijgt nu het voordeel van uw marshallers. Ze kunnen uw marshallers altijd overrulen door MarshalUsing te gebruiken op de plaats van gebruik.

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

Zie ook