Notes
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Dans ce tutoriel, vous allez apprendre à implémenter un marshaleur et à l’utiliser pour un marshaling personnalisé dans des appels P/Invoke générés par la source.
Vous allez implémenter des marshallers pour un type intégré, personnaliser le marshaling pour un paramètre spécifique et un type défini par l’utilisateur et spécifier le marshaling par défaut pour un type défini par l’utilisateur.
Tout le code source utilisé dans ce tutoriel est disponible dans le référentiel dotnet/samples.
Vue d’ensemble du LibraryImport
générateur source
Le System.Runtime.InteropServices.LibraryImportAttribute
type est le point d’entrée utilisateur d’un générateur source introduit dans .NET 7. Ce générateur source est conçu pour générer tout le code de marshaling au moment de la compilation au lieu du moment de l’exécution. Les points d'entrée ont historiquement été spécifiés à l'aide de DllImport
, mais cette méthode entraîne des coûts qui peuvent ne pas toujours être acceptables—pour plus d'informations, consultez la génération de code source P/Invoke. Le générateur de source LibraryImport
peut générer l’ensemble du code de marshaling et supprimer l’obligation de génération au moment de l’exécution intrinsèque à DllImport
.
Afin d’exprimer les détails nécessaires au code de marshaling généré, aussi bien pour le runtime que pour permettre aux utilisateurs de personnaliser leurs propres types, plusieurs types sont nécessaires. Les types suivants sont utilisés dans ce tutoriel :
MarshalUsingAttribute
- Attribut recherché par le générateur de source sur les sites d’utilisation, et qui permet de déterminer le type de marshaleur nécessaire au marshaling de la variable ayant des attributs associés.CustomMarshallerAttribute
- Attribut utilisé pour indiquer le marshaleur d’un type ainsi que le mode dans lequel les opérations de marshaling doivent être effectuées (par exemple, par référence, du managé au non managé).NativeMarshallingAttribute
: attribut qui sert à indiquer quel marshaller utiliser pour le type attribué. Cela est utile pour les auteurs de bibliothèques qui fournissent des types avec les marshaleurs qui les accompagnent.
Toutefois, ces attributs ne sont pas les seuls mécanismes disponibles pour un auteur de marshaleur personnalisé. Le générateur de source inspecte le marshaleur lui-même à la recherche d’autres indications spécifiant la façon dont le marshaling doit s’effectuer.
Vous trouverez des détails complets sur la conception dans le référentiel dotnet/runtime .
Analyseur et correcteur de générateur de source
En plus du générateur source lui-même, un analyseur et un fixateur sont fournis. L’analyseur et le fixateur sont activés et disponibles par défaut depuis .NET 7 RC1. L’analyseur est conçu pour aider les développeurs à utiliser correctement le générateur source. Le correcteur fournit des conversions automatisées de nombreux modèles DllImport
vers la signature appropriée LibraryImport
.
Présentation de la bibliothèque native
L’utilisation du LibraryImport
générateur source signifie consommer une bibliothèque native ou non managée. Une bibliothèque native peut être une bibliothèque partagée (autrement dit, .dll
ou .so
dylib
) qui appelle directement une API de système d’exploitation qui n’est pas exposée via .NET. La bibliothèque peut également être une bibliothèque fortement optimisée dans un langage non managé qu’un développeur .NET souhaite consommer. Pour ce tutoriel, vous allez créer votre propre bibliothèque partagée qui expose une surface d’API de style C. Le code suivant représente un type défini par l’utilisateur et deux API que vous utiliserez à partir de C#. Ces deux API représentent le mode « in », mais il existe des modes supplémentaires à explorer dans l’exemple.
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);
Le code précédent contient les deux types d’intérêt, char32_t*
et error_data
. char32_t*
représente une chaîne encodée en UTF-32, qui n’est pas un encodage de chaîne que .NET marshale historiquement. error_data
est un type défini par l’utilisateur qui contient un champ entier 32 bits, un champ booléen C++ et un champ de chaîne encodé en UTF-32. Dans ces deux cas, vous devez fournir au générateur de source un moyen de générer du code de marshaling.
Personnaliser le marshaling pour un type intégré
Prenez d’abord en compte le type char32_t*
, car le marshaling de ce type est imposé par le type défini par l’utilisateur. char32_t*
représente le côté natif, mais vous avez également besoin d’une représentation dans le code managé. Dans .NET, il n’existe qu’un seul type string
« string », . Ainsi, vous allez effectuer le marshaling d’une chaîne native codée au format UTF-32 vers et depuis le type string
dans du code managé. Il existe déjà plusieurs marshaleurs intégrés pour le type string
, qui sont marshalés au format UTF-8, UTF-16, ANSI et même en tant que type BSTR
Windows. Toutefois, il n’en existe aucun pour le marshaling au format UTF-32. C’est ce que vous devez définir.
Le Utf32StringMarshaller
type est marqué avec un CustomMarshaller
attribut, qui décrit ce qu’il fait au générateur source. Le premier argument de type de l’attribut est le type string
, le type managé à marshaler, le second est le mode, qui indique quand utiliser le marshaleur, et le troisième est Utf32StringMarshaller
, le type à utiliser pour le marshaling. Vous pouvez appliquer le CustomMarshaller
plusieurs fois pour spécifier davantage le mode et le type de marshaller à utiliser pour ce mode.
L’exemple actuel présente un marshaleur « sans état », qui accepte certaines entrées et retourne les données sous la forme marshalée. La méthode Free
existe pour des raisons de symétrie avec le marshaling non managé. Le récupérateur de mémoire est une opération « libre » du marshaleur managé. L’implémenteur est libre d’effectuer les opérations souhaitées pour marshaler l’entrée vers la sortie, mais n’oubliez pas qu’aucun état ne sera explicitement conservé par le générateur source.
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();
}
}
Les spécificités de la façon dont ce marshaller particulier effectue la conversion de string
à char32_t*
se trouvent dans l'exemple. Notez que toutes les API .NET peuvent être utilisées (par exemple, Encoding.UTF32).
Prenons un cas où l’état est souhaitable. Observez le mode supplémentaire CustomMarshaller
et notez le mode plus spécifique, MarshalMode.ManagedToUnmanagedIn
. Ce marshaleur spécialisé est implémenté « avec état » et peut stocker l’état tout au long de l’appel d’interopérabilité. Avec une spécialisation plus poussée et davantage d’états, il est possible d’obtenir des optimisations et un marshaling personnalisé pour un mode. Par exemple, le générateur de source peut recevoir pour instruction de fournir un tampon alloué par la pile, ce qui permet d’éviter une allocation explicite durant le marshaling. Pour indiquer la prise en charge d’un tampon alloué par la pile, le marshaleur implémente une propriété BufferSize
et une méthode FromManaged
qui accepte un Span
de type unmanaged
. La propriété BufferSize
indique la quantité d’espace de pile (longueur du Span
à passer à FromManaged
) que le marshaleur souhaite obtenir durant l’appel du marshaling.
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();
}
}
}
Vous pouvez maintenant appeler la première des deux fonctions natives à l’aide de vos marshalleurs de chaîne UTF-32. La déclaration suivante utilise l’attribut LibraryImport
, tout comme DllImport
, mais s’appuie sur l’attribut MarshalUsing
pour indiquer au générateur source quel marshaller utiliser lors de l’appel de la fonction native. Il n’est pas nécessaire de préciser si le marshaleur sans état ou avec état doit être utilisé. Cela est géré par le responsable de l’implémentation, qui définit le MarshalMode
sur le ou les attributs CustomMarshaller
du marshaleur. Le générateur de source sélectionne le marshaleur le plus approprié en fonction du contexte dans lequel le MarshalUsing
est appliqué, MarshalMode.Default
étant celui de secours.
// extern "C" DLL_EXPORT void STDMETHODCALLTYPE PrintString(char32_t* chars);
[LibraryImport(LibName)]
internal static partial void PrintString([MarshalUsing(typeof(Utf32StringMarshaller))] string s);
Personnaliser le marshaling pour un type défini par l’utilisateur
Le marshaling d’un type défini par l’utilisateur nécessite de définir non seulement la logique de marshaling, mais également le type en C# à marshaler en entrée/en sortie. Rappelez-vous le type natif que nous essayons de marshaler.
struct error_data
{
int code;
bool is_fatal_error;
char32_t* message; /* UTF-32 encoded string */
};
À présent, définissez ce qu’il ressemblerait idéalement en C#. Un int
a la même taille dans le C++ moderne et dans .NET. Il bool
s’agit de l’exemple canonique d’une valeur booléenne dans .NET. En s’appuyant sur Utf32StringMarshaller
, vous pouvez transformer char32_t*
en string
.NET. En prenant en compte le style .NET, le résultat est la définition suivante en C#.
struct ErrorData
{
public int Code;
public bool IsFatalError;
public string? Message;
}
En suivant le modèle de nommage, nommez le marshaleur ErrorDataMarshaller
. Au lieu de spécifier un marshaleur pour MarshalMode.Default
, vous allez définir uniquement des marshaleurs pour certains modes. Dans ce cas, si le marshaller est utilisé pour un mode qui n’est pas fourni, le générateur source échoue. Commencez par définir un marshaleur pour la direction « in » (entrante). Il s’agit d’un marshaleur « sans état », car il se compose uniquement de fonctions static
.
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
imite la forme du type non managé. La conversion d’un ErrorData
à un ErrorDataUnmanaged
est désormais triviale avec Utf32StringMarshaller
.
Le marshaling de int
est inutile, car sa représentation est identique dans le code géré et non géré. La représentation binaire d’une bool
valeur n’est pas définie dans .NET. Utilisez donc sa valeur actuelle pour définir une valeur zéro et non nulle dans le type non managé. Ensuite, réutilisez votre marshalleur UTF-32 pour convertir le string
champ en un uint*
.
public static ErrorDataUnmanaged ConvertToUnmanaged(ErrorData managed)
{
return new ErrorDataUnmanaged
{
Code = managed.Code,
IsFatal = (byte)(managed.IsFatalError ? 1 : 0),
Message = Utf32StringMarshaller.ConvertToUnmanaged(managed.Message),
};
}
Rappelez-vous que vous définissez ce marshaleur en tant que marshaleur « in » (entrant), vous devez donc nettoyer toutes les allocations effectuées durant le marshaling. Le champ int
et le champ bool
n'ont pas alloué de mémoire, mais le champ Message
l'a fait. Réutilisez Utf32StringMarshaller
pour nettoyer la chaîne marshalée.
public static void Free(ErrorDataUnmanaged unmanaged)
=> Utf32StringMarshaller.Free(unmanaged.Message);
Examinons brièvement le scénario « out » (sortant). Considérez le cas où une ou plusieurs instances de error_data
sont retournées.
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);
Un P/Invoke qui retourne un type d’instance unique, qui n’est pas une collection, est classé comme un MarshalMode.ManagedToUnmanagedOut
. En règle générale, vous utilisez une collection pour retourner plusieurs éléments, et dans ce cas, une Array
collection est utilisée. Le marshaleur pour un scénario de collection, correspondant au mode MarshalMode.ElementOut
, retourne plusieurs éléments et est décrit ci-dessous.
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();
}
}
}
La conversion de ErrorDataUnmanaged
vers ErrorData
est l’inverse de ce que vous avez fait pour le mode « in ». N’oubliez pas que vous devez également nettoyer toutes les allocations que l’environnement non managé s’attend à ce que vous effectuiez. Il est également important de noter que les fonctions ici sont marquées static
et sont donc « sans état », étant sans état est une exigence pour tous les modes « Élément ». Vous remarquerez également qu’il existe une méthode ConvertToUnmanaged
comme dans le mode « in ». Tous les modes « Élément » nécessitent la gestion des modes « in » et « out ».
Pour le marshaleur « out » (sortant) du code managé en code non managé, vous allez faire quelque chose de spécial. Le nom du type de données que vous marshalez est appelé error_data
et .NET exprime généralement des erreurs en tant qu’exceptions. Certaines erreurs sont plus impactantes que d’autres et les erreurs identifiées comme « irrécupérables » indiquent généralement une erreur catastrophique ou irrécupérable. Notez que error_data
a un champ permettant de vérifier si l’erreur est irrécupérable. Vous allez marshaler un error_data
en code managé et, s’il s’agit d’une erreur irrécupérable, vous lèverez une exception au lieu de la convertir simplement en ErrorData
et de la retourner.
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();
}
}
}
Un paramètre « out » se convertit d’un contexte non managé en contexte managé, de sorte que vous implémentez la ConvertToManaged
méthode. Quand l’appelé non managé est retourné et qu’il fournit un objet ErrorDataUnmanaged
, vous pouvez l’inspecter à l’aide de votre marshaleur en mode ElementOut
pour vérifier s’il est marqué en tant qu’erreur irrécupérable. Si tel est le cas, vous devez lancer cette indication au lieu de retourner simplement le ErrorData
.
public static ErrorData ConvertToManaged(ErrorDataUnmanaged unmanaged)
{
ErrorData data = Out.ConvertToManaged(unmanaged);
if (data.IsFatalError)
throw new ExternalException(data.Message, data.Code);
return data;
}
Peut-être que vous allez non seulement consommer la bibliothèque native, mais vous souhaitez également partager votre travail avec la communauté et fournir une bibliothèque d’interopérabilité. Vous pouvez fournir un marshaller implicite à ErrorData
chaque fois qu’il est utilisé dans un P/Invoke en ajoutant [NativeMarshalling(typeof(ErrorDataMarshaller))]
à la définition de ErrorData
. Désormais, quiconque utilise votre définition de ce type dans un appel à LibraryImport
peut bénéficier de vos marshaleurs. Ils peuvent toujours remplacer vos marshaleurs en employant MarshalUsing
sur le site d’utilisation.
[NativeMarshalling(typeof(ErrorDataMarshaller))]
struct ErrorData { ... }