Självstudie: Använda anpassade marshallers i källgenererade P/Invokes
I den här självstudien 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 marshallingkod vid kompileringstillfället 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 marshallingkod och ta bort körningsgenereringskravet som är inbyggt i DllImport
.
För att uttrycka den information som behövs för att generera kod för marshalling både för körningen och för användare att anpassa för sina egna typer, behövs flera typer. Följande typer används i den här självstudien:
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 själva marshallern för olika andra indikationer som informerar om hur marshalling ska ske.
Fullständig information om designen finns i dotnet/runtime-lagringsplatsen .
Källgeneratoranalysator och korrigering
Tillsammans med själva källgeneratorn tillhandahålls både en analysator och en korrigering. 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 mycket optimerat på ett ohanterat språk som en .NET-utvecklare vill använda. 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 och char32_t*
error_data
. char32_t*
representerar en sträng som är kodad i UTF-32, vilket inte är en strängkodning som .NET historiskt sett konverterar. 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 kod för marshalling.
Anpassa marshalling för en inbyggd typ
Tänk på typen char32_t*
först eftersom marshalling 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 samla en inbyggd UTF-32-kodad sträng till och från string
typen i hanterad kod. Det finns redan flera inbyggda marshallers för den string
typ som marskalk som UTF-8, UTF-16, ANSI och även som Windows-typ 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 string
typen, den hanterade typen som ska marskalkas, det andra är läget, vilket anger när marshaller ska användas och den tredje typen är Utf32StringMarshaller
, den typ som ska användas för marshalling. 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 marshallformat. 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 ett fall där tillstånd är önskvärt. Observera ytterligare CustomMarshaller
och notera det mer specifika läget, MarshalMode.ManagedToUnmanagedIn
. Den här specialiserade marshallern implementeras som "tillståndskänslig" och kan lagra tillstånd över interop-anropet. Mer specialisering och tillstånd tillåter optimeringar och skräddarsydd marshalling för ett läge. Till exempel kan källgeneratorn instrueras att tillhandahålla en stackallokerad buffert som kan undvika en explicit allokering under marshalling. För att ange stöd för en stackallokerad buffert implementerar marshaller en BufferSize
egenskap och en FromManaged
metod som tar en av en unmanaged
Span
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äng marshallers. 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. Det finns ingen anledning att klargöra om den tillståndslösa eller tillståndskänsliga marshallern ska användas. Detta hanteras av implementeraren som MarshalMode
definierar på marshaller-attributen CustomMarshaller
. Källgeneratorn väljer den lämpligaste marshallern baserat på den kontext där MarshalUsing
används, med MarshalMode.Default
återställningen.
// 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 kunna ordna en användardefinierad typ måste du definiera inte bara marshallinglogik, utan även typen i C# 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 marskalka char32_t*
som .NET string
. Resultatet är följande definition i C#:
struct ErrorData
{
public int Code;
public bool IsFatalError;
public string? Message;
}
Efter namngivningsmönstret namnger du marshallern ErrorDataMarshaller
. I stället för att ange en marshaller för MarshalMode.Default
definierar 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 igen 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å "out"-scenariot. 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
. Marshaller för ett samlingsscenario, som motsvarar MarshalMode.ElementOut
läget, returnerar 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 rensa 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 effektfulla än andra och fel som identifieras som "allvarliga" indikerar vanligtvis ett oåterkalleligt eller oåterkalleligt 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 din indikation att kasta i stället för att ErrorData
bara returnera .
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 underförstådd marshaller när den används i en P/Invoke genom att lägga [NativeMarshalling(typeof(ErrorDataMarshaller))]
till i ErrorData
definitionen. Nu får alla som använder din definition av den här typen i ett LibraryImport
samtal förmånen av dina marshallers. De kan alltid åsidosätta dina marshallers med hjälp MarshalUsing
av på användningsplatsen.
[NativeMarshalling(typeof(ErrorDataMarshaller))]
struct ErrorData { ... }