Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
V tomto kurzu se dozvíte, jak implementovat marshaller a použít ho pro vlastní zařazování ve zdrojově generovaných voláních P/Invokes.
Implementujete marshaly pro vestavěný typ, přizpůsobíte marshaling pro konkrétní parametr a uživatelsky definovaný typ a zadáte výchozí marshaling pro uživatelsky definovaný typ.
Veškerý zdrojový kód použitý v tomto kurzu je k dispozici v úložišti dotnet/samples.
Přehled generátoru LibraryImport zdrojů
Typ System.Runtime.InteropServices.LibraryImportAttribute je vstupní bod uživatele pro generátor zdroje zavedený v .NET 7. Tento zdrojový generátor je navržen tak, aby generoval veškerý marshalling kód v době kompilace, nikoli za běhu. Vstupní body byly historicky zadány pomocí DllImport, ale tento přístup přichází s náklady, které nemusí být vždy přijatelné – další informace najdete v části Generování zdroje P/Invoke. Zdrojový LibraryImport generátor může vygenerovat veškerý marshalling kód a odebrat požadavek na generování spojený s běhovým prostředím pro DllImport.
K vyjádření podrobností potřebných k vygenerování kódu pro maršálování, a to jak pro běhový modul, tak pro uživatele, kteří si mohou přizpůsobit vlastní typy, je potřeba několik typů. V tomto kurzu se používají následující typy:
MarshalUsingAttribute– Atribut, který hledá generátor zdrojového kódu při použití a používá se k určení typu marshalleru pro přípravu proměnné s atributem.CustomMarshallerAttribute– Atribut použitý k označení maršalu pro typ a režim, ve kterém se mají provádět převody (například předávání odkazem ze spravovaného prostředí do nespravovaného prostředí).NativeMarshallingAttribute– Atribut použitý k označení, který marshaller se má použít pro atributovaný typ. To je užitečné pro autory knihoven, kteří poskytují typy a k nim příslušné marshallery.
Tyto atributy ale nejsou jedinými mechanismy, které jsou k dispozici autorovi vlastního marshalleru. Generátor zdrojů zkontroluje samotný marshaller a hledá různé další indikace, které informují o tom, jak by mělo dojít k zařazování.
Kompletní podrobnosti o návrhu najdete v úložišti dotnet/runtime .
Analyzátor generátoru zdrojového kódu a opravovač
Kromě samotného generátoru zdroje jsou k dispozici analyzátor i fixer. Analyzátor a fixer jsou ve výchozím nastavení povolené a dostupné od verze .NET 7 RC1. Analyzátor je navržený tak, aby vývojářům pomohl správně používat generátor zdrojů. Fixer poskytuje automatizované převody z mnoha DllImport vzorů do příslušného LibraryImport podpisu.
Představujeme nativní knihovnu
Použití generátoru LibraryImport zdrojů by znamenalo využívání nativní nebo nespravované knihovny. Nativní knihovna může být sdílená knihovna (tj. .dll, .so nebo dylib), která přímo volá rozhraní API operačního systému, jež není dostupné skrze .NET. Knihovna může být například silně optimalizovaná pro jazyk, který není spravován, a vývojář .NET ji chce využívat. V tomto kurzu vytvoříte vlastní sdílenou knihovnu, která zpřístupňuje plochu rozhraní API ve stylu jazyka C. Následující kód představuje uživatelem definovaný typ a dvě rozhraní API, která budete využívat z jazyka C#. Tato dvě rozhraní API představují režim "in", ale v ukázce existují další režimy.
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);
Předchozí kód obsahuje dva typy zájmu char32_t* a error_data.
char32_t* představuje řetězec kódovaný v kódování UTF-32, což není řetězcové kódování, které .NET historicky zařazuje.
error_data je uživatelem definovaný typ, který obsahuje 32bitové celočíselné pole, logické pole jazyka C++ a pole řetězce s kódováním UTF-32. Oba tyto typy vyžadují, abyste zdrojovému generátoru umožnili generování kódu maršálování.
Přizpůsobení zařazování pro předdefinovaný typ
Nejprve zvažte typ char32_t*, protože zpracování tohoto typu je vyžadováno typem definovaným uživatelem.
char32_t* představuje nativní stranu, ale potřebujete také reprezentaci ve spravovaném kódu. V .NET existuje pouze jeden typ "řetězec", string. Proto převádíte nativní řetězec kódovaný jako UTF-32 na a z typu string ve spravovaném kódu. Pro typ string již existuje několik vestavěných maršálů, které zařazují jako UTF-8, UTF-16, ANSI, a dokonce i jako typ Windows BSTR. Neexistuje však žádná metoda pro serializaci jako UTF-32. To je to, co potřebujete definovat.
Typ Utf32StringMarshaller je označen atributem CustomMarshaller , který popisuje, co dělá se zdrojovým generátorem. Prvním argumentem typu atributu string je typ, spravovaný typ pro zařazování, druhý je režim, který určuje, kdy použít marshaller, a třetí typ je Utf32StringMarshaller, typ, který se má použít pro zařazování. Vícekrát můžete použít CustomMarshaller k dalšímu určení režimu a typu marshalleru, který se má pro daný režim použít.
Aktuální příklad ukazuje "bezstavový" komponent pro serializaci/deserializaci dat, který přebírá určitý vstup a vrací data v serializované formě. Metoda Free existuje pro symetrii s nespravovaným marshallingem a garbage collector je "automatická" operace pro spravovaný marshaller. Implementátor je volný k provádění operací, které jsou žádoucí k zařazení vstupu do výstupu, ale nezapomeňte, že generátor zdroje explicitně nezachová žádný stav.
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();
}
}
Specifika, jak tento konkrétní marshaller provádí převod z string na char32_t*, lze nalézt v příkladu. Všimněte si, že všechna rozhraní .NET API je možné použít (například Encoding.UTF32).
Zvažte případ, kdy je žádoucí stav. Sledujte další CustomMarshaller a poznamenejte si konkrétnější režim . MarshalMode.ManagedToUnmanagedIn Tento specializovaný marshaller se implementuje jako stavový a může ukládat stav napříč voláním rozhraní. Další specializace a státní povolení umožňují optimalizace a přizpůsobené uspořádání pro konkrétní režim. Zdrojový generátor může být například instruován, aby poskytl vyrovnávací paměť přidělenou zásobníkem, která by se mohla vyhnout explicitnímu přidělení během zařazování. Chcete-li označit podporu vyrovnávací paměti přidělené zásobníku, marshaller implementuje vlastnost BufferSize a metodu FromManaged, která přijímá Span typu unmanaged. Tato BufferSize vlastnost označuje množství prostoru zásobníku – délku Span, kterou by marshaller chtěl získat při volání do 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();
}
}
}
Teď můžete volat první ze dvou nativních funkcí pomocí zařazovačů řetězců UTF-32. Následující deklarace používá LibraryImport atribut, stejně jako DllImport, ale spoléhá na MarshalUsing atribut, aby informoval generátor zdroje, který marshaller použít při volání nativní funkce. Není třeba vysvětlovat, jestli by se měl použít bezstavový nebo stavový marshaller. To zpracovává implementátor, který definuje MarshalMode na atributu/attributech marshalleru CustomMarshaller. Generátor zdroje vybere nejvhodnější marshaller na základě kontextu, ve kterém se použije MarshalUsing, přičemž MarshalMode.Default bude jako záloha.
// extern "C" DLL_EXPORT void STDMETHODCALLTYPE PrintString(char32_t* chars);
[LibraryImport(LibName)]
internal static partial void PrintString([MarshalUsing(typeof(Utf32StringMarshaller))] string s);
Přizpůsobení zařazování pro uživatelem definovaný typ
Přiřazování uživatelem definovaného typu vyžaduje definování nejen logiky řazení, ale také typu v jazyce C# pro zařazování do/z. Připomeňme si nativní typ, který se snažíme zprostředkovat.
struct error_data
{
int code;
bool is_fatal_error;
char32_t* message; /* UTF-32 encoded string */
};
Teď definujte, jak by vypadala v jazyce C#.
int má stejnou velikost jak v moderním jazyce C++, tak v .NET. A bool je kanonický příklad booleanovské hodnoty v .NET. Na základě Utf32StringMarshaller můžete char32_t* převést jako .NET string. Při účtování stylu .NET je výsledkem následující definice v jazyce C#:
struct ErrorData
{
public int Code;
public bool IsFatalError;
public string? Message;
}
Podle vzoru pojmenování pojmenujte marshaller ErrorDataMarshaller. Místo abyste zadali marshaller pro MarshalMode.Default, budete definovat marshallery pouze pro některé režimy. Pokud se v takovém případě použije marshaller pro režim, který není k dispozici, generátor zdroje selže. Začněte definováním marshalleru pro směr "in". Jedná se o "bezstavový" marshaller, protože se marshaller sám skládá pouze z static funkcí.
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 napodobuje strukturu nespravovaného typu. Převod z ErrorData na ErrorDataUnmanaged je nyní triviální díky Utf32StringMarshaller.
Marshalování int je zbytečné, protože jeho reprezentace je stejná v nespravovaném a spravovaném kódu. Binární bool reprezentace hodnoty není definována v .NET, takže použijte její aktuální hodnotu k definování nuly a nenulové hodnoty v nespravovaném typu. Potom znovu použijte UTF-32 marshaller k převodu string pole na .uint*
public static ErrorDataUnmanaged ConvertToUnmanaged(ErrorData managed)
{
return new ErrorDataUnmanaged
{
Code = managed.Code,
IsFatal = (byte)(managed.IsFatalError ? 1 : 0),
Message = Utf32StringMarshaller.ConvertToUnmanaged(managed.Message),
};
}
Vzpomeňte si, že tento zařazovač definujete jako "in", takže je nutné vyčistit všechna přidělení provedená během zařazování. Pole int a bool nepřidělila žádnou paměť, ale pole Message ano. Znovu použijte Utf32StringMarshaller k vyčištění zařazovaného řetězce.
public static void Free(ErrorDataUnmanaged unmanaged)
=> Utf32StringMarshaller.Free(unmanaged.Message);
Pojďme se krátce podívat na scénář "out". Vezměte v úvahu případ, kdy se vrátí jeden nebo více příkladů error_data.
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);
P/Invoke, který vrací jeden instanční typ, který není kolekcí, je kategorizován jako MarshalMode.ManagedToUnmanagedOut. Kolekci obvykle používáte k vrácení více prvků a v tomto případě se Array používá. Marshaller pro scénář kolekce, který odpovídá režimu MarshalMode.ElementOut, bude vracet více prvků, což je popsáno později.
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();
}
}
}
Převod z ErrorDataUnmanaged na ErrorData je inverzní oproti tomu, co jste udělali pro "in" režim. Nezapomeňte, že je také potřeba vyčistit všechny alokace, které nespravované prostředí očekávalo, že provedete. Je také důležité si uvědomit, že zde uvedené funkce jsou označené static , a proto jsou bezstavové, bezstavové je požadavek pro všechny režimy elementů. Také si všimnete, že zde existuje metoda ConvertToUnmanaged podobně jako v režimu "in". Všechny režimy "Element" vyžadují zpracování pro režimy "in" i "out".
Pro přechod z řízeného do neřízeného prostředí pro "out" marshaller, budete dělat něco zvláštního. Název datového typu, který zařazujete, se volá error_data a .NET obvykle vyjadřuje chyby jako výjimky. Některé chyby jsou závažnější než jiné a chyby identifikované jako "závažné" obvykle značí katastrofické nebo neopravitelné chyby.
error_data Všimněte si, že je pole pro kontrolu, jestli je chyba závažná. Převedete error_data do spravovaného kódu, a pokud je to fatální, vyvoláte výjimku místo toho, abyste ho jen převedli na ErrorData a vrátili ho.
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();
}
}
}
Parametr out převede z nespravovaného kontextu na spravovaný kontext, takže implementujete metodu ConvertToManaged . Když se nespravovaný příjemce hovoru vrátí a poskytne objekt ErrorDataUnmanaged, můžete jej zkontrolovat pomocí maršála režimu ElementOut a ověřit, jestli je označený jako závažná chyba. Pokud ano, je to váš pokyn k vyhození místo pouhého vrácení ErrorData.
public static ErrorData ConvertToManaged(ErrorDataUnmanaged unmanaged)
{
ErrorData data = Out.ConvertToManaged(unmanaged);
if (data.IsFatalError)
throw new ExternalException(data.Message, data.Code);
return data;
}
Možná budete používat nejen nativní knihovnu, ale také chcete sdílet svou práci s komunitou a poskytnout knihovnu vzájemné spolupráce. Implicitní marshaller můžete zadat ErrorData pokaždé, když se použije při použití v P/Invoke, přidáním [NativeMarshalling(typeof(ErrorDataMarshaller))] do ErrorData určení. Teď každý, kdo použije vaši definici tohoto typu v LibraryImport hovoru, získá výhodu vašich seřaďovačů. Vždy mohou přepsat vaše zařazovače pomocí MarshalUsing na místě použití.
[NativeMarshalling(typeof(ErrorDataMarshaller))]
struct ErrorData { ... }