Partager via


Tutoriel : Utiliser des marshaleurs personnalisés dans les appels P/Invoke générés par la source

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 marshaleurs 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 dépôt dotnet/samples.

Vue d’ensemble du générateur de source LibraryImport

Le type System.Runtime.InteropServices.LibraryImportAttribute est le point d’entrée utilisateur d’un générateur de source introduit dans .NET 7. Ce générateur de source est conçu pour générer l’ensemble du code de marshaling au moment de la compilation et non au moment de l’exécution. Les points d’entrée sont traditionnellement spécifiés à l’aide de DllImport, mais cette approche entraîne des coûts qui ne sont pas toujours acceptables. Pour plus d’informations, consultez Génération d’appels P/Invoke par la source. 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 tout au long de 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 utilisé afin d’indiquer le marshaleur à utiliser pour le type ayant des attributs associés. 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 dépôt dotnet/runtime.

Analyseur et correcteur de générateur de source

En plus du générateur de source lui-même, un analyseur et un correcteur sont tous deux fournis. L’analyseur et le correcteur 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 de source. Le correcteur fournit des conversions automatisées à partir de nombreux modèles DllImport vers la signature LibraryImport appropriée.

Présentation de la bibliothèque native

L’utilisation du générateur de source LibraryImport implique la consommation d’une bibliothèque native ou non managée. Une bibliothèque native peut être une bibliothèque partagée (c’est-à-dire .dll, .so ou dylib) qui appelle directement une API de système d’exploitation non exposée via .NET. La bibliothèque peut également être 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 en C. Le code suivant représente un type défini par l’utilisateur et deux API que vous allez consommer en C#. Ces deux API représentent le mode « in » (entrant), mais il existe d’autres modes à 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 qui vous intéressent, char32_t* et error_data. char32_t* représente une chaîne codée au format UTF-32, lequel n’est pas un format de codage de chaînes traditionnellement marshalé par .NET. error_data est un type défini par l’utilisateur, qui contient un champ d’entier 32 bits, un champ booléen C++ et un champ de chaîne codée au format 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 en code managé. Dans .NET, il n’existe qu’un seul type de « chaîne », 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 à vous de le définir.

Le type Utf32StringMarshaller est marqué avec un attribut CustomMarshaller, qui décrit ce qu’il fait pour le générateur de 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 CustomMarshaller à plusieurs reprises afin de spécifier plus précisément le mode et le type de marshaleur à 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é. Le responsable de l’implémentation est libre d’effectuer toutes les opérations souhaitées pour marshaler l’entrée vers la sortie, mais n’oubliez pas qu’aucun état n’est explicitement conservé par le générateur de 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 détails relatifs à la façon dont ce marshaleur 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 CustomMarshaller supplémentaire ainsi que 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 à présent appeler la première des deux fonctions natives à l’aide de vos marshaleurs de chaînes au format UTF-32. La déclaration suivante utilise l’attribut LibraryImport, tout comme DllImport, mais elle repose sur l’attribut MarshalUsing pour indiquer au générateur de source quel marshaleur utiliser au moment 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 à quoi il doit ressembler idéalement en C#. Un int a la même taille en C++ moderne et dans .NET. Un bool est l’exemple canonique d’une valeur booléenne dans .NET. En partant de Utf32StringMarshaller, vous pouvez marshaler char32_t* en tant que string .NET. En prenant en compte le style .NET, il en résulte 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 le cas présent, si le marshaleur est utilisé pour un mode qui n’est pas fourni, l’exécution du générateur de source se solde par un échec. 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 en ErrorDataUnmanaged est désormais triviale avec Utf32StringMarshaller.

Le marshaling d’un int n’est pas nécessaire, car sa représentation est identique dans le code non managé et le code managé. La représentation binaire d’une valeur bool n’étant pas définie dans .NET, utilisez sa valeur actuelle pour définir une valeur égale à zéro et une valeur différente de zéro dans le type non managé. Réutilisez ensuite votre marshaleur au format UTF-32 pour convertir le champ string 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),
    };
}

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. Les champs int et bool n’ont pas alloué de mémoire, contrairement au champ Message. 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). Prenons 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 appel P/Invoke qui retourne un seul type d’instance, non lié à une collection, est catégorisé en tant que MarshalMode.ManagedToUnmanagedOut. En règle générale, vous utilisez une collection pour retourner plusieurs éléments. Dans le cas présent, un Array est utilisé. Le marshaleur d’un scénario de collection, correspondant au mode MarshalMode.ElementOut, retourne plusieurs éléments et sera décrit plus tard.

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 en ErrorData est l’inverse de ce que vous avez fait pour le mode « in » (entrant). 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 comme étant static et qu’elles sont donc « sans état ». Le fait d’être sans état est une obligation pour tous les modes « Element ». Vous remarquerez également qu’il existe une méthode ConvertToUnmanaged comme en 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 dont vous effectuez le marshaling s’appelle error_data, et .NET exprime généralement les erreurs sous forme d’exceptions. Certaines erreurs ont plus d’impact que d’autres, et les erreurs identifiées comme « irrécupérables » indiquent généralement une erreur grave ou impossible à corriger. 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 » (sortant) convertit un contexte non managé en un contexte managé. Vous implémentez donc la méthode ConvertToManaged. 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 n’allez pas seulement consommer la bibliothèque native. Peut-être que vous souhaitez également partager votre travail avec la communauté et fournir une bibliothèque d’interopérabilité. Vous pouvez fournir à ErrorData un marshaleur implicite chaque fois qu’il est utilisé dans un appel P/Invoke en ajoutant [NativeMarshalling(typeof(ErrorDataMarshaller))] à la définition 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 { ... }

Voir aussi