Kurz: Použití vlastních zařazovačů ve zdrojovém vygenerovaném volání nespravovaného kódu
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 marshallery pro předdefinovaný typ, přizpůsobíte řazení pro konkrétní parametr a uživatelem definovaný typ a zadáte výchozí zařazování pro uživatelem 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 místo za běhu vygeneroval veškerý zařazovaný kód v době kompilace. 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 tématu Generování zdroje volání nespravovaného kódu. Generátor LibraryImport
zdroje může vygenerovat veškerý zařazovací kód a odebrat požadavek na generování za běhu vnitřní do DllImport
.
K vyjádření podrobností potřebných k vygenerování kódu pro modul runtime i pro uživatele, kteří si můžou 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 zdroje při použití lokality a používá se k určení typu marshaller pro zařazování atributu proměnné.CustomMarshallerAttribute
– Atribut použitý k označení marshalleru pro typ a režim, ve kterém se mají provádět operace seřazování (například podle odkazu ze spravovaného do nespravovaného).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ří pro tyto typy poskytují typy a doprovodné marshallery.
Tyto atributy ale nejsou jedinými mechanismy, které jsou k dispozici vlastnímu autorovi 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, které není vystaveno prostřednictvím .NET. Knihovna může být také ta, která je silně optimalizovaná v nespravovaném jazyce, který chce vývojář .NET 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 poskytli způsob generování zařazování kódu.
Přizpůsobení zařazování pro předdefinovaný typ
char32_t*
Nejprve zvažte typ, protože seřazování tohoto typu vyžaduje typ definovaný 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 zařazujete nativní řetězec kódování UTF-32 do a z typu ve spravovaném string
kódu. Pro typ, který zařazuje jako UTF-8, UTF-16, ANSI, a dokonce i jako typ WindowsBSTR
, již existuje několik předdefinovaných zařazovačůstring
. Pro zařazování jako UTF-32 ale neexistuje. 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ý označuje, kdy použít marshaller, a třetí typ je Utf32StringMarshaller
typ, který se má použít pro seř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ý" marshaller, který přebírá určitý vstup a vrací data v zařazované podobě. Metoda Free
existuje pro symetrii s nespravovaným zařazováním a uvolňování paměti je "bezplatná" 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 způsobu, jakým tento konkrétní marshaller provádí převod z string
toho, na který char32_t*
se dá v ukázce najít. 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 zprostředkovatele komunikace. Další specializace a stav umožňují optimalizace a přizpůsobené zařazování pro 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 BufferSize
vlastnost a metodu FromManaged
, která přebírá Span
typ unmanaged
. Tato BufferSize
vlastnost označuje množství prostoru zásobníku – délku Span
, do FromManaged
které se má předat – marshaller by chtěl dostat během zařazování.
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 informovat generátor zdroje, který marshaller použít při volání nativní funkce. Není nutné objasnit, jestli by se měl použít bezstavový nebo stavový marshaller. To zpracovává implementátor definující MarshalMode
atributy marshalleru CustomMarshaller
. Generátor zdroje vybere nejvhodnější marshaller na základě kontextu, ve kterém MarshalUsing
se použije, s MarshalMode.Default
tím, že je náhradní.
// 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 pokoušíme zařazuje.
struct error_data
{
int code;
bool is_fatal_error;
char32_t* message; /* UTF-32 encoded string */
};
Teď definujte, jak by vypadala v jazyce C#. Je int
stejná velikost v moderním jazyce C++ i v .NET. A bool
je kanonický příklad logické hodnoty v .NET. Stavět nad Utf32StringMarshaller
, můžete zařašovat char32_t*
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 zadání marshalleru pro MarshalMode.Default
některé režimy definujete pouze marshallery. 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 tvar nespravovaného typu. Převod z na an ErrorData
ErrorDataUnmanaged
je nyní triviální s Utf32StringMarshaller
.
Zařazování je zbytečné, protože jeho reprezentace je stejná v nespravovaném a spravovaném int
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
bool
nepřidělila žádnou paměť, ale Message
pole 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í jedna nebo více instancí 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);
Volání nespravovaného kódu, které vrací jeden typ instance, 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á MarshalMode.ElementOut
režimu, vrátí více prvků a je popsán 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í funkce k tomu, co jste udělali pro režim "in". Nezapomeňte, že potřebujete také vyčistit všechny přidělení, 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ů. Všimněte si také, že existuje ConvertToUnmanaged
metoda jako v režimu "v". Všechny režimy "element" vyžadují zpracování pro režimy "in" i "out".
Pro správu nespravovaného "out" marshallera, 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 více ovlivněné než jiné a chyby identifikované jako "závažná" obvykle značí katastrofickou nebo neopravitelnou chybu. error_data
Všimněte si, že je pole pro kontrolu, jestli je chyba závažná. Zařazujete ho do spravovaného error_data
kódu, a pokud je závažná, vyvoláte výjimku, a ne jenom ji převedete na ErrorData
spravovaný kód a vrátíte 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ý volaný vrátí a poskytne ErrorDataUnmanaged
objekt, můžete ho zkontrolovat pomocí ElementOut
zařazovače režimu a zkontrolovat, jestli je označený jako závažná chyba. Pokud ano, znamená to, že místo toho, aby se vrátila 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 v volání nespravovaného kódu přidáním [NativeMarshalling(typeof(ErrorDataMarshaller))]
do ErrorData
definice. 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 můžou přepsat zařazovače pomocí MarshalUsing
webu use.
[NativeMarshalling(typeof(ErrorDataMarshaller))]
struct ErrorData { ... }