Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
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 DllImport
angegeben, 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 Utf32StringMarshaller
der 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.Default
anzugeben, 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 { ... }