Partage via


Meilleures pratiques pour l’interopérabilité native

.NET vous offre différentes façons de personnaliser votre code d’interopérabilité natif. Cet article inclut les conseils que les équipes de Microsoft .NET suivent pour l'interopérabilité native.

Règle générale

Les instructions de cette section s’appliquent à tous les scénarios d’interopérabilité.

  • ✔️ Utilisez [LibraryImport], si possible, lorsque vous ciblez .NET 7+.
    • Il existe des cas où l’utilisation de est appropriée. Un analyseur de code avec ID SYSLIB1054 vous indique quand c’est le cas.
  • ✔️ À FAIRE : utilisez les mêmes noms et la même mise en majuscules pour vos méthodes et paramètres que pour la méthode native que vous souhaitez appeler.
  • ✔️ À ENVISAGER : utiliser les mêmes noms et la même mise en majuscules pour les valeurs constantes.
  • ✔️ Définissez les signatures P/Invoke et pointeur de fonction qui correspondent aux arguments de la fonction C.
  • ✔️ Utilisez les types .NET correspondant le plus au type natif. Par exemple, en C#, utilisez lorsque le type natif est .
  • ✔️ Préférez exprimer des types natifs de niveau supérieur à l’aide de .NET structs plutôt que des classes.
  • ✔️ Préférez utiliser des pointeurs de fonction et plutôt que des types, lors du passage de rappels à des fonctions non managées en C#. Pour plus d’informations, consultez .
  • ✔️ Utilisez les attributs et sur les paramètres de tableau.
  • ✔️ N’utilisez pas et les attributs sur d’autres types que lorsque le comportement souhaité diffère du comportement par défaut.
  • ✔️ À ENVISAGER : utiliser pour regrouper les mémoires tampons du tableau natif.
  • ✔️ À ENVISAGER : encapsuler les déclarations P/Invoke dans une classe portant le même nom et utilisant la même mise en majuscules que votre bibliothèque native.
    • Vos attributs ou pourront ainsi utiliser la fonctionnalité du langage C# pour transmettre le nom de la bibliothèque native et vérifier qu’il ne comporte pas d’erreur.
  • ✔️ Utilisez des gestionnaires pour gérer la durée de vie des objets qui encapsulent des ressources non managées. Pour plus d’informations, consultez Nettoyage des ressources non managées.
  • ÉVITEZ les finaliseurs pour gérer la durée de vie des objets qui encapsulent des ressources non managées. Pour plus d'informations, consultez Implémenter une méthode Dispose.

Paramètres de l’attribut LibraryImport

Un analyseur de code, avec ID SYSLIB1054, vous guidera avec . Dans la plupart des cas, l’utilisation de requiert une déclaration explicite plutôt que de s’appuyer sur les paramètres par défaut. Cette conception est intentionnelle et permet d’éviter un comportement inattendu dans les scénarios d’interopérabilité.

Paramètres des attributs DllImport

Réglage Par défaut Recommandation Détails
PreserveSig true conservez l’URL par défaut . S’il est défini explicitement sur false, les valeurs de retour HRESULT en échec sont converties en exceptions (et la valeur de retour de la définition devient ainsi Null).
SetLastError false Dépend de l'API Définissez-le sur true si l’API utilise GetLastError et Marshal.GetLastWin32Error pour obtenir la valeur. Si l’API définit une condition indiquant une erreur, récupérez l’erreur avant d’effectuer d’autres appels, de façon à éviter de la remplacer par inadvertance.
CharSet Défini par le compilateur (spécifié dans la documentation sur le jeu de caractères) Utilisez explicitement ou lorsque des chaînes ou des caractères sont présents dans la définition Cela permet de spécifier le comportement de marshaling des chaînes et le rôle de lorsque . Notez que est en réalité UTF-8 sur Unix. Most du temps Windows utilise Unicode tandis qu’Unix utilise UTF8. Pour plus d'informations, voir la documentation sur les charsets.
ExactSpelling false true Définissez-le sur true pour augmenter légèrement les performances. En effet, le runtime ne recherche pas de noms de fonctions de remplacement avec un suffixe « A » ou « W » selon la valeur du paramètre (« A » pour et « W » pour ).

Paramètres de chaîne

Un est épinglé et utilisé directement par le code natif (plutôt que copié) lorsqu'il est transmis par valeur (pas ou ) et l'une des conditions suivantes est remplie :

  • est défini comme .
  • L’argument est explicitement marqué comme .
  • est .

NE PAS utiliser les paramètres . Les paramètres de chaînes passés en valeur avec l’attribut risquent de déstabiliser le runtime s’il s’agit de chaînes centralisées. Pour plus d’informations sur la centralisation des chaînes, voir la documentation de .

✔️ CONSIDÉREZ les tables ou d’un lorsque du code natif est censé remplir une mémoire tampon de caractères. Cela nécessite de transmettre l’argument en tant que .

Conseils spécifiques à DllImport

✔️ ENVISAGEZ de définir la propriété dans afin que le runtime connaisse l’encodage de chaîne attendu.

✔️ ENVISAGEZ d’éviter les paramètres . Le marshaling crée toujours une copie de tampon native. Il peut donc se révéler extrêmement inefficace. Prenez l'exemple typique d'appel d'une API Windows qui prend une chaîne :

  1. Créez un de la capacité souhaitée (alloue de la capacité gérée) .
  2. Appelez :
    1. Alloue une mémoire tampon native .
    2. Copie le contenu si (valeur par défaut d’un paramètre ).
    3. Copie la mémoire tampon native dans un tableau managé nouvellement alloué si (également la valeur par défaut pour ).
  3. alloue encore un autre tableau managé .

Il s’agit d’allocations pour obtenir une chaîne du code natif. Le mieux que l’on puisse faire pour limiter ce nombre est de réutiliser dans un autre appel, mais cela ne permet d’enregistrer qu’une seule allocation. Il est préférable d’utiliser et de mettre en cache une mémoire tampon de caractères à partir de . Vous pouvez ensuite revenir à l’allocation de la lors des appels suivants.

L’autre problème de est qu’il copie toujours la sauvegarde de la mémoire tampon de retour sur la première valeur Null. Si la chaîne transmise n'est pas correctement terminée ou est une chaîne à double terminaison nulle, votre P/Invoke est au mieux incorrect.

Si vous utilisez, le dernier piège est que la capacité n’inclut pas de valeur Null masquée, qui est toujours prise en compte dans l’interopérabilité. Il est courant de se tromper, car la plupart des API demandent la taille du tampon, y compris le nul. Il en résulte parfois des allocations gaspillées/inutiles. En outre, cet écueil empêche le runtime d’optimiser le marshaling afin de réduire les copies.

Pour plus d’informations sur le marshaling des chaînes, voir Marshaling par défaut pour les chaînes et Personnaliser le marshaling des chaînes.

Windows Spécifique Pour les chaînes [Out], le CLR utilise CoTaskMemFree par défaut pour libérer des chaînes ou SysStringFree pour les chaînes marquées comme UnmanagedType.BSTR. Pour la plupart des API avec une mémoire tampon de chaîne de sortie : le nombre de caractères transmis doit inclure la valeur Nul. Si la valeur de retour est inférieure au nombre de caractères transmis, c’est le signe que l’appel a réussi et que la valeur correspond au nombre de caractères sans la valeur Null de fin. Sinon, le nombre représente la taille requise de la mémoire tampon, incluant le caractère nul.

  • Nombre transmis = 5, valeur de retour = 4 : la chaîne comporte 4 caractères avec une valeur Nul de fin.
  • Nombre transmis = 5, valeur de retour = 6 : la chaîne comporte 5 caractères et a besoin d’une mémoire tampon de 6 caractères pour conserver la valeur Nul. Types de données Windows pour les chaînes

Paramètres et champs booléens

Il est facile de se perdre avec les booléens. Par défaut, un .NET bool est converti vers Windows BOOL, où il s'agit d'une valeur de 4 octets. À l’inverse, les types et en C et C++ correspondent à un seul octet. Il peut alors être difficile de traquer les bogues, car la valeur de retour est à moitié ignorée, ce qui ne modifie que potentiellement le résultat. Pour plus d’informations sur le transfert des valeurs .NET vers des types C ou C++ , consultez la documentation sur la personnalisation du transfert des champs booléens .

GUIDs

Les GUID sont directement utilisables dans les signatures. De nombreuses API Windows prennent des alias de type GUID& comme REFIID. Lorsque la signature de méthode contient un paramètre de référence, placez un mot clé ou un attribut sur la déclaration de paramètre GUID.

GUID GUID en référence
KNOWNFOLDERID REFKNOWNFOLDERID

NE PAS utiliser pour autre chose que des paramètres GUID .

Types blittables

Les types blittables sont des types qui ont la même représentation au niveau du bit dans le code managé et dans le code natif. Il n’est donc pas nécessaire de les convertir dans un autre format pour les marshaler vers et à partir du code natif. Ils sont à privilégier en raison de l’amélioration des performances qui en résulte. Certains types ne sont pas blittables, mais sont connus pour contenir du contenu blittable. Ces types ont des optimisations similaires à celles des types blittables, lorsqu’ils ne sont pas contenus dans un autre type, mais ne sont pas considérés comme blittables dans des champs de structs ou pour les besoins de .

Types blittables lorsque le marshaling du runtime est activé

Types blittables :

  • , , , , , , , , ,
  • structs avec une disposition fixe qui n'ont que des types de valeurs blittables pour les champs d'instance
    • La disposition fixe exige ou .
    • par défaut, les structs sont

Types avec contenu blittable :

  • tableaux unidimensionnels non imbriqués de types simples blittables (par exemple, )
  • classes avec disposition fixe qui n'ont que des types de valeur blittable pour les champs d'instance
    • La disposition fixe exige ou .
    • par défaut, les classes sont

NON blittable :

  • bool

PARFOIS blittable :

  • char

Types avec contenu PARFOIS blittable :

  • string

Lorsque les types blittables sont transmis par référence avec , ou ou lorsque les types avec du contenu blittable sont transmis par valeur, ils sont simplement épinglés par le marshaleur plutôt que d’être copiés dans une mémoire tampon intermédiaire.

est blittable dans un tableau unidimensionnel ou s'il fait partie d'un type qui le contient explicitement marqué avec avec .

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct UnicodeCharStruct
{
    public char c;
}

contient un contenu blittable s'il n'est pas contenu dans un autre type et s'il est passé par valeur (pas ou ) en tant qu'argument et si l'une des conditions suivantes est remplie :

  • est défini comme .
  • L’argument est explicitement marqué comme .
  • est Unicode.

Pour savoir si un type est blittable ou contient du contenu blittable, essayez de créer un épinglé. Si le type n’est pas une chaîne ou n’est pas considéré comme blittable, lèvera une .

Types blittables lorsque le marshaling du runtime est désactivé

Lorsque le marshaling du runtime est désactivé, les règles pour lesquelles les types sont blittables sont considérablement plus simples. Tous les types de type C# et qui n’ont pas de champs marqués avec sont blittables. Tous les types qui ne sont pas de type C# ne sont pas blittables. Le concept de types avec du contenu blittable, tels que des tableaux ou des chaînes, ne s’applique pas lorsque le marshaling du runtime est désactivé. Tout type qui n’est pas considéré comme blittable par la règle ci-dessus n’est pas pris en charge lorsque le marshaling du runtime est désactivé.

Ces règles diffèrent du système intégré principalement dans les situations où et sont utilisés. Lorsque le marshaling est désactivé, est transmis en tant que valeur de 1 octet et non normalisé et est toujours transmis en tant que valeur de 2 octets. Lorsque le marshaling du runtime est activé, peut être mappé à une valeur de 1, 2 ou 4 octets et est toujours normalisé et mappé à une valeur de 1 ou 2 octets en fonction de .

✔️ À FAIRE : rendez vos structures blittables dans la mesure du possible.

Pour plus d'informations, consultez les pages suivantes :

  • Types blittables et non blittables
  • Type Marshalling

Maintenir actifs les objets gérés

garantit qu'un objet reste accessible jusqu'à ce que la méthode KeepAlive soit invoquée.

permet au marshaller de maintenir un objet en vie pendant la durée d'un P/Invoke. Il peut être utilisé à la place de dans les signatures de méthode. remplace cette classe et doit être utilisé à la place.

permet d'épingler un objet géré et d'obtenir son pointeur natif. En voici le modèle de base :

GCHandle handle = GCHandle.Alloc(obj, GCHandleType.Pinned);
IntPtr ptr = handle.AddrOfPinnedObject();
handle.Free();

L’épinglage n’est pas le comportement par défaut de . L’autre grand modèle sert à faire passer une référence à un objet géré à travers le code natif, puis à nouveau au code managé, en général avec un rappel. En voici le modèle :

GCHandle handle = GCHandle.Alloc(obj);
SomeNativeEnumerator(callbackDelegate, GCHandle.ToIntPtr(handle));

// In the callback
GCHandle handle = GCHandle.FromIntPtr(param);
object managedObject = handle.Target;

// After the last callback
handle.Free();

N’oubliez pas que doit être explicitement libéré pour éviter les fuites de mémoire.

Types de données Windows courants

Voici une liste des types de données couramment utilisés dans les API Windows et les types C# à utiliser lors de l’appel au code Windows.

Les types suivants sont de la même taille sur les Windows 32 bits et 64 bits, malgré leurs noms.

Largeur Windows C# Alternatif
32 BOOL int bool
8 BOOLEAN byte [MarshalAs(UnmanagedType.U1)] bool
8 BYTE byte
8 UCHAR byte
8 UINT8 byte
8 CCHAR byte
8 CHAR sbyte
8 CHAR sbyte
8 INT8 sbyte
16 CSHORT short
16 INT16 short
16 SHORT short
16 ATOM ushort
16 UINT16 ushort
16 USHORT ushort
16 WORD ushort
32 INT int
32 INT32 int
32 LONG int Consultez et .
32 LONG32 int
32 CLONG uint Consultez et .
32 DWORD uint Consultez et .
32 DWORD32 uint
32 UINT uint
32 UINT32 uint
32 ULONG uint Consultez et .
32 ULONG32 uint
64 INT64 long
64 LARGE_INTEGER long
64 LONG64 long
64 LONGLONG long
64 QWORD long
64 DWORD64 ulong
64 UINT64 ulong
64 ULONG64 ulong
64 ULONGLONG ulong
64 ULARGE_INTEGER ulong
32 HRESULT int
32 NTSTATUS int

Comme il s’agit de pointeurs, les types suivants suivent la largeur de la plateforme. Utilisez pour eux .

Types de pointeurs signés (utilisez ) Types de pointeurs non signés (utilisez )
HANDLE WPARAM
HWND UINT_PTR
HINSTANCE ULONG_PTR
LPARAM SIZE_T
LRESULT
LONG_PTR
INT_PTR

Un Windows PVOID, qui est un C void*, peut être marshalisé comme IntPtr ou UIntPtr, mais préférez le void* lorsque cela est possible.

Types de données de Windows

Plages de types de données

Anciennement des types pris en charge intégrés

Il existe de rares cas où le support intégré pour un type est supprimé.

La prise en charge intégrée des marshals UnmanagedType.HString et UnmanagedType.IInspectable a été supprimée dans la version .NET 5. Vous devez recompiler les fichiers binaires qui utilisent ce type de marshaling et qui ciblent une infrastructure précédente. Il est encore possible de manipuler ce type, mais vous devez le faire manuellement, comme le montre l'exemple de code suivant. Ce code fonctionnera à l’avenir et est également compatible avec les infrastructures précédentes.

public sealed class HStringMarshaler : ICustomMarshaler
{
    public static readonly HStringMarshaler Instance = new HStringMarshaler();

    public static ICustomMarshaler GetInstance(string _) => Instance;

    public void CleanUpManagedData(object ManagedObj) { }

    public void CleanUpNativeData(IntPtr pNativeData)
    {
        if (pNativeData != IntPtr.Zero)
        {
            Marshal.ThrowExceptionForHR(WindowsDeleteString(pNativeData));
        }
    }

    public int GetNativeDataSize() => -1;

    public IntPtr MarshalManagedToNative(object ManagedObj)
    {
        if (ManagedObj is null)
            return IntPtr.Zero;

        var str = (string)ManagedObj;
        Marshal.ThrowExceptionForHR(WindowsCreateString(str, str.Length, out var ptr));
        return ptr;
    }

    public object MarshalNativeToManaged(IntPtr pNativeData)
    {
        if (pNativeData == IntPtr.Zero)
            return null;

        var ptr = WindowsGetStringRawBuffer(pNativeData, out var length);
        if (ptr == IntPtr.Zero)
            return null;

        if (length == 0)
            return string.Empty;

        return Marshal.PtrToStringUni(ptr, length);
    }

    [DllImport("api-ms-win-core-winrt-string-l1-1-0.dll")]
    [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
    private static extern int WindowsCreateString([MarshalAs(UnmanagedType.LPWStr)] string sourceString, int length, out IntPtr hstring);

    [DllImport("api-ms-win-core-winrt-string-l1-1-0.dll")]
    [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
    private static extern int WindowsDeleteString(IntPtr hstring);

    [DllImport("api-ms-win-core-winrt-string-l1-1-0.dll")]
    [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
    private static extern IntPtr WindowsGetStringRawBuffer(IntPtr hstring, out int length);
}

// Example usage:
[DllImport("api-ms-win-core-winrt-l1-1-0.dll", PreserveSig = true)]
internal static extern int RoGetActivationFactory(
    /*[MarshalAs(UnmanagedType.HString)]*/[MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(HStringMarshaler))] string activatableClassId,
    [In] ref Guid iid,
    [Out, MarshalAs(UnmanagedType.IUnknown)] out object factory);

Considérations relatives aux types de données multiplateformes

Il existe des types en langage C/C++ qui ont une latitude dans leur mode de définition. Lors de l’écriture de l’interopérabilité multiplateforme, des cas peuvent survenir où les plateformes diffèrent et peuvent entraîner des problèmes si elles ne sont pas prises en compte.

C/C++

de C/C++ et de C# n’ont pas nécessairement la même taille.

Le type en C/C++ est défini pour avoir « au moins 32 » bits. Cela signifie qu’il existe un nombre minimal de bits requis, mais que les plateformes peuvent choisir d’utiliser davantage de bits si vous le souhaitez. Le tableau suivant illustre les différences de bits fournis pour le type de données C/C++ entre les plateformes.

Plateforme 32 bits 64 bits
Windows 32 32
macOS/*nix 32 64

En revanche, C# est toujours en 64 bits. Pour cette raison, il est préférable d’éviter d’utiliser de C# pour l’interopérabilité avec C/C++ .

(Ce problème avec de C/C++ n’existe pas pour , , et de C/C++, car leur taille est respectivement de 8, 16, 32 et 64 bits sur toutes ces plateformes.)

Dans .NET versions 6 et ultérieures, utilisez les types CLong et CULong pour l’interopérabilité avec les types de données C/C++ long et unsigned long. L’exemple suivant concerne , mais vous pouvez utiliser pour effectuer une abstraction de la même manière.

// Cross platform C function
// long Function(long a);
[DllImport("NativeLib")]
extern static CLong Function(CLong a);

// Usage
nint result = Function(new CLong(10)).Value;

Lorsque vous ciblez .NET 5 et versions antérieures, vous devez déclarer des signatures distinctes Windows et non Windows pour gérer le problème.

static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);

// Cross platform C function
// long Function(long a);

[DllImport("NativeLib", EntryPoint = "Function")]
extern static int FunctionWindows(int a);

[DllImport("NativeLib", EntryPoint = "Function")]
extern static nint FunctionUnix(nint a);

// Usage
nint result;
if (IsWindows)
{
    result = FunctionWindows(10);
}
else
{
    result = FunctionUnix(10);
}

Structures

Les structs managés sont créés sur la pile et ne sont pas supprimés tant que la méthode n’a pas retourné de valeur. Ils sont donc par définition « épinglés » (ils ne sont pas déplacés par le récupérateur de mémoire). Vous pouvez aussi simplement utiliser l’adresse dans des blocs de code unsafe si le code natif n’utilise pas le pointeur une fois la méthode actuelle terminée.

Les structs blittables sont beaucoup plus performants, car ils sont directement utilisables par la couche de marshaling. Essayez de rendre les structs blittables (par exemple, évitez ). Pour plus d'informations, voir la section Types blittables.

Si la structure est blittable, utilisez au lieu de pour de meilleures performances. Comme nous l’avons mentionné plus haut, vous pouvez valider que le type est blittable en essayant de créer un épinglé. Si le type n’est pas une chaîne ou n’est pas considéré comme blittable, lèvera une .

Les pointeurs vers des structs dans les définitions doivent être passés soit par ou soit utiliser et .

✔️ ESSAYEZ DE faire correspondre la structure gérée aussi fidèlement que possible à la forme et aux noms utilisés dans l’en-tête ou dans la documentation de la plateforme officielle.

✔️ À FAIRE : utilisez le C# au lieu de pour les structures blittables afin d’améliorer les performances.

❌ Ne dépendez pas de la représentation interne des types de struct exposés par les bibliothèques runtime .NET, sauf si cela est explicitement documenté.

ÉVITEZ d’utiliser des classes pour exprimer des types natifs complexes via l’héritage.

ÉVITEZ d’utiliser des champs ou pour représenter des champs de pointeur de fonctions dans des structures.

Étant donné que et n’ont pas de signature requise, ils ne garantissent pas que le délégué transmis correspondra à la signature attendue par le code natif. En outre, dans .NET Framework et .NET Core, le marshaling d'un struct contenant un System.Delegate ou System.MulticastDelegate de sa représentation native à un objet managé peut déstabiliser l'exécution si la valeur du champ dans la représentation native n'est pas un pointeur de fonction qui encapsule un délégué managé. Dans .NET 5 et versions ultérieures, le marshaling d’un champ System.Delegate ou System.MulticastDelegate d’une représentation native vers un objet managé n’est pas pris en charge. Utilisez un type de délégué spécifique au lieu de ou .

Mémoires tampons fixes

Un tableau comme doit être marshalé en deux champs , et . Lorsque le tableau natif est un type primitif, il est possible d’utiliser le mot clé pour que le code soit un peu plus propre. Par exemple, se présente ainsi dans l’en-tête natif :

typedef struct _SYSTEM_PROCESS_INFORMATION {
    ULONG NextEntryOffset;
    ULONG NumberOfThreads;
    BYTE Reserved1[48];
    UNICODE_STRING ImageName;
...
} SYSTEM_PROCESS_INFORMATION;

On peut l’écrire ainsi en C# :

internal unsafe struct SYSTEM_PROCESS_INFORMATION
{
    internal uint NextEntryOffset;
    internal uint NumberOfThreads;
    private fixed byte Reserved1[48];
    internal Interop.UNICODE_STRING ImageName;
    ...
}

Toutefois, les mémoires tampons fixes présentent quelques pièges. Les mémoires tampons fixes de types non blittables ne sont pas marshalées correctement ; par conséquent, le tableau sur place doit être étendu à plusieurs champs différents. En outre, dans .NET Framework et .NET Core avant la version 3.0, si une structure contenant un champ tampon fixe est imbriquée dans une structure non blittable, le champ tampon fixe ne sera pas correctement marshalisé vers le code natif.

Résoudre les problèmes liés aux échecs P/Invoke

Le tableau suivant mappe les symptômes courants à leur cause probable et à leur correctif recommandé.

Symptôme Cause probable Réparer
DllNotFoundException Bibliothèque introuvable lors de l’exécution Vérifiez le nom de la bibliothèque, le chemin d’accès et la plateforme. Permet de tester le chargement. Sur Linux, vérifiez ou .
EntryPointNotFoundException Incompatibilité de nom d’exportation Inspectez les exportations natives (dumpbin /exports sur Windows, nm -D sur Linux). Vérifiez l'obfuscation des noms C++ (manquant). Définissez explicitement.
AccessViolationException Incompatibilité de signature, utilisation après libération ou épinglage manquant Comparez les signatures managées et natives. Vérifiez les tailles de struct par rapport à la version native. Vérifiez la durée de vie de la mémoire. Utiliser une signature blittable pour résoudre le problème de marshaling
Altération silencieuse des données Taille ou encodage de type incorrect Ajoutez l'enregistrement des limites. Comparez avec la version native. Testez avec des paires d’entrée/sortie connues.
Incidents intermittents GC a déplacé un objet non épinglé ou collecté un délégué Encourage les délégués de rappel pour la durée totale de leur cycle de vie. Utilisez [option A] ou [option B] pour les pointeurs conservés entre les appels.
Corruption du tas gratuitement Mauvais allocateur Mettre en correspondance l’allocateur : ne jamais mélanger avec ou . Utilisez la propre fonction gratuite de la bibliothèque.

Empêcher la collecte de délégués avec

Lorsque vous utilisez pour convertir un délégué en pointeur de fonction, le garbage collector ne suit pas la relation entre le pointeur retourné et le délégué source. Si le délégué est éligible pour la collecte avant que le code natif ait fini d'utiliser le pointeur, l'application se bloque.

Permet d’empêcher la collecte :

var callback = new MyDelegate((level, msgPtr) =>
{
    string msg = Marshal.PtrToStringUTF8(msgPtr) ?? string.Empty;
    Console.WriteLine($"[{level}] {msg}");
});

IntPtr fnPtr = Marshal.GetFunctionPointerForDelegate(callback);
NativeUsesCallback(fnPtr);
GC.KeepAlive(callback); // Prevent collection — fnPtr does not root the delegate

Si le code natif stocke le pointeur de fonction au-delà de l’appel (par exemple, en tant que rappel persistant), le délégué doit être enraciné pendant toute sa durée de vie, généralement en le stockant dans un champ.

Résoudre les conflits entre la documentation et les en-têtes natifs

Lorsque vous écrivez des signatures P/Invoke, il est possible de rencontrer des différences entre la documentation de l’API en ligne et les fichiers d’en-tête natifs réels. Les fichiers d’en-tête sont la source faisant autorité pour les signatures de fonction, les structures de données, les tailles des types et les conventions d’appel. En cas de doute, vérifiez vos signatures P/Invoke sur l’en-tête plutôt que de vous appuyer uniquement sur la documentation.