Freigeben über


Lernprogramm: Verwenden von benutzerdefinierten Marshallern in quellgenerierten P/Invokes

In diesem Lernprogramm erfahren Sie, wie Sie einen Marshaller implementieren und für das benutzerdefinierte Marshalling in quellgenerierten P/Invokes verwenden.

Sie implementieren Marshaller für einen integrierten Typ, passen das Marshalling für einen bestimmten Parameter und einen benutzerdefinierten Typ an und spezifizieren das Standard-Marshalling für einen benutzerdefinierten Typ.

Der in diesem Lernprogramm verwendete Quellcode ist im Repository dotnet/samples verfügbar.

Übersicht über den LibraryImport Quellgenerator

Der System.Runtime.InteropServices.LibraryImportAttribute Typ ist der Benutzereinstiegspunkt für einen Quellgenerator, der in .NET 7 eingeführt wurde. Dieser Quellgenerator ist so konzipiert, dass der gesamte Marshallcode zur Kompilierzeit statt zur Laufzeit generiert wird. Einstiegspunkte wurden historisch unter Verwendung DllImportangegeben, aber dieser Ansatz ist mit Kosten verbunden, die möglicherweise nicht immer akzeptabel sind – weitere Informationen finden Sie unter P/Invoke-Quellgenerierung. Der LibraryImport-Quellgenerator kann den gesamten Marshallcode generieren und die bei DllImport intrinsische Anforderung zur Laufzeitgenerierung vermeiden.

Zum Ausdrücken der Details, die zum Generieren von Marshallcode zur Anpassung für eigene Typen sowohl für die Runtime als auch für Benutzer erforderlich sind, werden mehrere Typen benötigt. Die folgenden Typen werden in diesem Lernprogramm verwendet:

  • MarshalUsingAttribute – Attribut, das vom Quellgenerator an Verwendungsstandorten gesucht wird und verwendet wird, um den Marshallertyp zum Marshallen der Attributvariablen zu bestimmen.

  • CustomMarshallerAttribute: Attribut, das verwendet wird, um einen Marshaller für einen Typ und den Modus anzugeben, in dem die Marshallvorgänge ausgeführt werden sollen (z. B. durch Verweis von verwaltet in nicht verwaltet).

  • NativeMarshallingAttribute – Attribut, das verwendet wird, um anzugeben, welcher Marshaller für den Attributtyp verwendet werden soll. Dies ist für Bibliotheksautoren nützlich, die Typen und begleitende Marshaller für diese Typen bereitstellen.

Diese Attribute sind jedoch nicht die einzigen Mechanismen, auf die ein Entwickler eines benutzerdefinierten Marshallers zugreifen kann. Der Quellgenerator prüft den Marshaller selbst auf verschiedene andere Hinweise, die darüber informieren, wie das Marshallen erfolgen soll.

Vollständige Details zum Design finden Sie im dotnet/runtime-Repository .

Analyse und Korrekturregel für den Quellgenerator

Zusammen mit dem Quellgenerator selbst werden sowohl ein Analyzer als auch ein Fixer bereitgestellt. Der Analyzer und der Fixer sind seit .NET 7 RC1 standardmäßig aktiviert und verfügbar. Der Analyzer wurde entwickelt, um Entwicklern bei der ordnungsgemäßen Verwendung des Quellgenerators zu helfen. Der Fixer stellt automatisierte Konvertierungen von vielen DllImport Mustern in die entsprechende LibraryImport Signatur bereit.

Einführung in die native Bibliothek

Die Verwendung des LibraryImport Quellgenerators würde bedeuten, eine systemeigene oder nicht verwaltete Bibliothek zu verwenden. Eine systemeigene Bibliothek kann eine freigegebene Bibliothek sein (d. h. .dll, .so oder dylib), die direkt eine Betriebssystem-API aufruft, die nicht über .NET bereitgestellt wird. Die Bibliothek kann auch eine sein, die in einer nicht verwalteten Sprache stark optimiert ist, die ein .NET-Entwickler nutzen möchte. In diesem Lernprogramm erstellen Sie Ihre eigene freigegebene Bibliothek, die eine API-Oberfläche im C-Stil verfügbar macht. Der folgende Code stellt einen benutzerdefinierten Typ und zwei APIs dar, die Sie von C# aus nutzen. Diese beiden APIs stellen den Modus "in" dar, es gibt jedoch zusätzliche Modi, die im Beispiel untersucht werden können.

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

Der vorangehende Code enthält die beiden interessanten Typen char32_t* und error_data. char32_t* stellt eine Zeichenfolge dar, die in UTF-32 codiert ist, was keine Zeichenfolgencodierung ist, die .NET historisch unterstützt. error_data ist ein benutzerdefinierter Typ, der ein 32-Bit-Ganzzahlfeld, ein boolesches C++-Feld und ein UTF-32-codiertes Zeichenfolgenfeld enthält. Beide Typen erfordern, dass Sie eine Möglichkeit zum Generieren von Marshallcode für den Quellgenerator bereitstellen.

Anpassen des Marshallings für einen integrierten Typ

Berücksichtigen Sie zuerst den char32_t*-Typ, da das Marshallen dieses Typs für den benutzerdefinierten Typ erforderlich ist. char32_t* stellt die systemeigene Seite dar, aber Sie benötigen auch eine Repräsentation im verwalteten Code. In .NET gibt es nur einen "string"-Typ. string Aus diesem Grund marshallen Sie eine native UTF-32-codierte Zeichenfolge in den und aus dem Typ string in verwaltetem Code. Es gibt bereits mehrere integrierte Marshaller für den string Typ, die als UTF-8, UTF-16, ANSI und sogar als Windows-Typ BSTR marshallen. Es gibt jedoch keinen für das Marshallen als UTF-32. Das müssen Sie definieren.

Der Utf32StringMarshaller Typ ist mit einem CustomMarshaller Attribut gekennzeichnet, das beschreibt, was er für den Quellgenerator tut. Das erste Typargument für das Attribut ist der string Typ, der verwaltete Typ zum Marshallen, der zweite der Modus, der angibt, wann der Marshaller verwendet werden soll, und der dritte Typ ist Utf32StringMarshallerder Typ, der für die Marshalling verwendet werden soll. Sie können den CustomMarshaller Modus mehrfach anwenden, um den Modus weiter zu verfeinern und den zu verwendenden Marshallertyp für diesen Modus anzugeben.

Das aktuelle Beispiel zeigt einen „zustandslosen“ Marshaller, der eine Eingabe annimmt und Daten in gemarshallter Form zurückgibt. Die Free-Methode ist aus Gründen der Symmetrie mit dem nicht verwalteten Marshalling vorhanden, und der Garbage Collector ist der „freie“ Vorgang für den verwalteten Marshaller. Der Implementierer kann alle Vorgänge ausführen, die zum Marshallen der Eingabe an die Ausgabe erforderlich sind, aber denken Sie daran, dass kein Zustand vom Quellgenerator explizit beibehalten wird.

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

Die Besonderheiten, wie dieser bestimmte Marshaller die Konvertierung von string zu char32_t* durchführt, finden Sie im Beispiel. Beachten Sie, dass alle .NET-APIs verwendet werden können (z. B. Encoding.UTF32).

Berücksichtigen Sie einen Fall, in dem der Zustand wünschenswert ist. Richten Sie Ihr Augenmerk auf den zusätzlichen CustomMarshaller, und beachten Sie den spezifischeren Modus MarshalMode.ManagedToUnmanagedIn. Dieser spezialisierte Marshaller wird als „zustandsbehaftet“ implementiert und kann den Zustand über den Interop-Aufruf hinweg speichern. Weitere Spezialisierung und der Zustand ermöglichen Optimierungen und maßgeschneidertes Marshalling für einen Modus. Beispielsweise kann der Quellgenerator angewiesen werden, einen stapelzugeordneten Puffer bereitzustellen, der eine explizite Zuordnung während des Marshallings vermeiden kann. Um die Unterstützung für einen Stack-allokierten Puffer anzugeben, implementiert der Marshaller eine BufferSize-Eigenschaft und eine FromManaged-Methode, die einen Span-Parameter vom unmanaged-Typ verwendet. Die Eigenschaft BufferSize gibt die Menge des Stapelraums an – die Länge des Span, der an FromManaged übergeben wird – die der Marshaller während des Marshallaufrufs abrufen möchte.

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

Sie können jetzt die ersten der beiden systemeigenen Funktionen mit Ihren UTF-32-Zeichenfolgen-Marshallern aufrufen. Die folgende Deklaration verwendet das LibraryImport-Attribut, genau wie das DllImport-Attribut, basiert jedoch auf dem MarshalUsing-Attribut, um dem Quellgenerator mitzuteilen, welcher Marshaller beim Aufrufen der nativen Funktion verwendet werden soll. Es ist nicht erforderlich, zu klären, ob der zustandslose oder zustandsbehaftete Marshaller verwendet werden soll. Dies wird vom Implementierer übernommen, der den MarshalMode für die CustomMarshaller-Attribute des Marshallers definiert. Der Quellgenerator wählt den am besten geeigneten Marshaller basierend auf dem Kontext aus, in dem MarshalUsing angewendet wird, wobei MarshalMode.Default als Fallback dient.

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

Marshalling für einen benutzerdefinierten Typ anpassen

Für das Marshallen eines benutzerdefinierten Typs ist nicht nur die Marshalllogik erforderlich, sondern außerdem der Typ in C#, in den bzw. aus dem das Marshallen erfolgen soll. Erinnern Sie sich an den nativen Typ, den wir marshallen möchten.

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

Definieren Sie nun, wie sie im Idealfall in C# aussehen würde. Eine int ist die gleiche Größe sowohl in modernen C++ als auch in .NET. A bool ist das kanonische Beispiel für einen booleschen Wert in .NET. Auf der Grundlage von Utf32StringMarshaller können Sie char32_t* als .NET-string marshallen. Entsprechend dem .NET-Stil ergibt sich die folgende Definition in C#:

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

Benennen Sie nach dem Benennungsmuster den Marshaller ErrorDataMarshaller. Anstatt einen Marshaller für MarshalMode.Defaultanzugeben, definieren Sie nur Marshaller für einige Modi. Wenn der Marshaller in diesem Fall für einen nicht bereitgestellten Modus verwendet wird, schlägt der Quellgenerator fehl. Beginnen Sie mit dem Definieren eines Marshallers für die „Ein“-Richtung. Dies ist ein „zustandsloser“ Marshaller, da der eigentliche Marshaller nur aus static-Funktionen besteht.

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 Imitiert die Form des nicht verwalteten Typs. Die Umwandlung von einem ErrorData zu einem ErrorDataUnmanaged ist jetzt trivial mit Utf32StringMarshaller.

Die Marshalling eines Elements int ist unnötig, da seine Darstellung in nicht verwaltetem und verwaltetem Code identisch ist. Die binäre Darstellung eines bool Werts ist in .NET nicht definiert. Verwenden Sie daher den aktuellen Wert, um einen Null- und Nicht-Null-Wert im nicht verwalteten Typ zu definieren. Verwenden Sie dann Ihren UTF-32-Marshaller, um das string-Feld in ein uint* zu konvertieren.

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

Denken Sie daran, dass Sie diesen Marshaller als „ein“ definieren, sodass Sie alle Zuordnungen bereinigen müssen, die während des Marshallings ausgeführt werden. Die Felder int und bool haben keinen Speicher zugewiesen, aber das Feld Message hat dies getan. Verwenden Sie Utf32StringMarshaller erneut, um die gemarshallte Zeichenfolge zu bereinigen.

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

Betrachten wir kurz das „Aus“-Szenario. Sehen wir uns den Fall an, dass eine oder mehrere Instanzen von error_data zurückgegeben werden.

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

Ein P/Invoke, das einen einzelnen Instanztyp zurückgibt und keine Sammlung ist, wird als MarshalMode.ManagedToUnmanagedOut kategorisiert. In der Regel verwenden Sie eine Auflistung, um mehrere Elemente zurückzugeben, und in diesem Fall wird eine Array verwendet. Der Marshaller für ein Sammlungsszenario, das dem MarshalMode.ElementOut Modus entspricht, gibt mehrere Elemente zurück und wird später beschrieben.

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

Die Umwandlung von ErrorDataUnmanaged zu ErrorData ist das Gegenteil von dem, was Sie für den "in"-Modus gemacht haben. Denken Sie daran, dass Sie auch alle Zuordnungen bereinigen müssen, die von der nicht verwalteten Umgebung erwartet wurden. Es ist auch wichtig zu beachten, dass die hier aufgeführten Funktionen als static markiert und daher "zustandslos" sind, da "zustandslos" eine Anforderung für alle "Element"-Modi ist. Sie werden auch feststellen, dass es eine ConvertToUnmanaged-Methode wie im eingehenden Modus gibt. Alle Element-Modi erfordern die Behandlung sowohl für den eingehenden als auch für den ausgehenden Modus".

Für den „Aus“-Marshaller von verwaltet zu nicht verwaltet müssen Sie etwas Besonderes erledigen. Der Name des Datentyps, den Sie übergeben, heißt error_data, und .NET gibt in der Regel Fehler als Ausnahmen aus. Einige Fehler sind wirkungsvoller als andere und Fehler, die als "schwerwiegend" identifiziert wurden, weisen in der Regel auf einen katastrophalen oder nicht behebbaren Fehler hin. Beachten Sie, dass ein error_data Feld vorhanden ist, um zu überprüfen, ob der Fehler schwerwiegend ist. Sie marshallen einen error_data in verwalteten Code, und wenn er schwerwiegend ist, lösen Sie eine Ausnahme aus, anstatt ihn nur in ein ErrorData zu konvertieren und zurückzugeben.

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

Ein "out"-Parameter konvertiert aus einem nicht verwalteten Kontext in einen verwalteten Kontext, sodass Sie die ConvertToManaged Methode implementieren. Wenn der nicht verwaltete Aufgerufene bei der Rückgabe ein ErrorDataUnmanaged-Objekt angibt, können Sie es mit Ihrem Marshaller für den ElementOut-Modus untersuchen und überprüfen, ob es als schwerwiegender Fehler markiert ist. Wenn dies der Fall ist, ist dies Ihr Hinweis, eine Ausnahme auszulösen, anstatt ErrorData einfach zurückzugeben.

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

    return data;
}

Vielleicht werden Sie nicht nur die native Bibliothek nutzen, sondern auch Ihre Arbeit mit der Community teilen und eine Interoperabilitätsbibliothek bereitstellen. Sie können ErrorData bei jeder Verwendung in einem P/Invoke mit einem impliziten Marshaller angeben, indem Sie der [NativeMarshalling(typeof(ErrorDataMarshaller))]-Definition ErrorData hinzufügen. Alle Benutzer, die Ihre Definition dieses Typs in einem LibraryImport-Aufruf verwenden, profitieren nun von Ihren Marshallern. Sie können Ihre Marshaller jederzeit außer Kraft setzen, indem sie am Einsatzort MarshalUsing verwenden.

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

Siehe auch