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 marshallcode genereren en de vereiste voor het genereren van runtime verwijderen voor DllImport.

Voor het uitdrukken van de details die nodig zijn voor het genereren van marshallcode voor zowel de runtime als voor gebruikers die voor hun eigen typen moeten worden aangepast, zijn er verschillende typen nodig. 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 – Kenmerk dat wordt gebruikt om een marshaller aan te geven voor een type en de modus waarin de marshallbewerkingen moeten worden uitgevoerd (bijvoorbeeld door ref 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 kenmerken zijn echter niet de enige mechanismen die beschikbaar zijn voor een aangepaste marshallerauteur. 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 .

Brongeneratoranalyse en fixer

Samen met de brongenerator zelf worden er beide een analyse en 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.

Inleiding tot de systeemeigen bibliotheek

Het gebruik van de LibraryImport brongenerator zou betekenen dat een systeemeigen of onbeheerde bibliotheek wordt gebruikt. Een systeemeigen bibliotheek kan een gedeelde bibliotheek zijn (dat wil .dllgezegd, .soof 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 interesse en char32_t*error_data. char32_t* vertegenwoordigt een tekenreeks die is gecodeerd in UTF-32. Dit is geen tekenreeks die .NET historisch marshals coderen. 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 voor de brongenerator om marshallcode te 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 systeemeigen UTF-32 gecodeerde tekenreeks naar en van het string type beheerde code instellen. Er zijn al verschillende ingebouwde marshallers voor het string type dat marshal als UTF-8, UTF-16, ANSI en zelfs als het Type Windows BSTR . Er is echter niet één 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 aan marshal, 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 'stateless' marshaller die enige invoer inneemt en gegevens retourneert in het marshallformulier. De Free methode bestaat voor symmetrie met de onbeheerde marshalling en de garbagecollector is de 'vrije' bewerking voor de beheerde marshaller. De implementeerfunctie is vrij om alle bewerkingen uit te voeren die nodig zijn om de invoer in de uitvoer te marshalen, 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 specifieke details van hoe deze specifieke marshaller de conversie uitvoert van waaruit string deze te char32_t* vinden is, vindt u in de steekproef. 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 staat optimalisaties en op maat gemaakte marshalling voor een modus toe. De brongenerator kan bijvoorbeeld worden geïnstrueerd om een stack-toegewezen buffer te bieden die expliciete toewijzing tijdens marshalling kan voorkomen. Om ondersteuning voor een stack-toegewezen buffer aan te geven, implementeert de marshaller een BufferSize eigenschap en een FromManaged methode die een Spanunmanaged type gebruikt. De BufferSize eigenschap geeft de hoeveelheid stapelruimte aan ( de lengte van de Span te worden doorgegeven FromManaged) de marshaller zou willen krijgen tijdens de marshal call.

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 stateful marshaller moet worden gebruikt. Dit wordt afgehandeld door de implementeerfunctie die de MarshalMode kenmerken van CustomMarshaller de marshaller definieert. De brongenerator selecteert de meest geschikte marshaller op basis van de context waarin de MarshalUsing toepassing 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 niet alleen de marshallinglogica, maar ook het type in C# tot marshal naar/van. Het systeemeigen type dat we proberen te marshal.

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. Utf32StringMarshallerVerder kunt u marshal char32_t* als .NET stringgebruiken. Accounting voor .NET-stijl, het resultaat is de volgende definitie in C#:

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

Geef na het naamgevingspatroon de marshaller ErrorDataMarshallereen naam. 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 staatloze marshaller omdat de marshaller zelf alleen uit functies bestaat static .

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. Hergebruik Utf32StringMarshaller opnieuw om de marshallreeks op te schonen.

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

Laten we kort eens kijken naar het 'out'-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 exemplaartype, niet-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 door de onbeheerde omgeving moeten worden uitgevoerd. 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 elementmodi is verwerking vereist voor zowel 'in' als 'out'-modi.

Voor de onbeheerde "out" marshaller, ga je iets speciaals doen. De naam van het gegevenstype dat u marshallt, wordt aangeroepen error_data en .NET geeft doorgaans fouten weer als uitzonderingen. Sommige fouten zijn meer van invloed dan andere en fouten die als 'fataal' worden geïdentificeerd, geven meestal een onherstelbare of onherstelbare fout aan. U ziet dat er error_data een veld is om te controleren of de fout fataal is. U krijgt een error_data marshal in beheerde code en als het fataal is, genereert u een uitzondering in plaats van deze alleen te converteren naar een ErrorData code en deze 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 aanwijzer terugkeert en een ErrorDataUnmanaged object levert, kunt u het inspecteren met behulp van uw ElementOut modus marshaller en controleren of het is gemarkeerd als een fatale fout. Zo ja, dan is dat je indicatie om te gooien in plaats van gewoon de ErrorData.

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. U kunt een impliciete marshaller opgeven ErrorData wanneer deze wordt gebruikt in een P/Invoke door deze toe te voegen [NativeMarshalling(typeof(ErrorDataMarshaller))] 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 overschrijven met behulp van MarshalUsing de site.

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

Zie ook