Partager via


Marshaling de types

Le marshaling est le processus de transformation des types lorsqu’ils doivent passer d’un code managé à un code natif.

Le marshaling est nécessaire, car les types dans le code managé et non managé sont différents. Dans le code managé, par exemple, vous avez un string, tandis que les chaînes non managées peuvent être l’encodage .NET string (UTF-16), l’encodage de page de codes ANSI, UTF-8, se terminant par null, ASCII, etc. Par défaut, le sous-système P/Invoke tente de faire la bonne chose en fonction du comportement par défaut, décrit dans cet article. Toutefois, pour les situations où vous avez besoin d’un contrôle supplémentaire, vous pouvez utiliser l’attribut MarshalAs pour spécifier le type attendu du côté non managé. Par exemple, si vous souhaitez que la chaîne soit envoyée en tant que chaîne UTF-8 terminée par null, vous pouvez le faire comme suit :

[LibraryImport("somenativelibrary.dll")]
static extern int MethodA([MarshalAs(UnmanagedType.LPUTF8Str)] string parameter);

// or

[LibraryImport("somenativelibrary.dll", StringMarshalling = StringMarshalling.Utf8)]
static extern int MethodB(string parameter);

Si vous appliquez l’attribut System.Runtime.CompilerServices.DisableRuntimeMarshallingAttribute à l’assembly, les règles de la section suivante ne s’appliquent pas. Pour plus d’informations sur la façon dont les valeurs .NET sont exposées au code natif lorsque cet attribut est appliqué, consultez le marshaling du runtime désactivé.

Règles par défaut de marshaling des types courants

En règle générale, le runtime tente de prendre la bonne décision en matière de marshaling, c’est-à-dire celle qui demande le moins de travail de votre part. Les tableaux suivants décrivent comment chaque type est marshalé par défaut lorsqu’il est utilisé dans un paramètre ou un champ. Les types entiers et caractères de largeur fixe C99/C++11 sont utilisés pour s’assurer que le tableau suivant est correct pour toutes les plateformes. Vous pouvez utiliser n’importe quel type natif qui a les mêmes exigences d’alignement et de taille que ces types.

La première table décrit les correspondances de différents types pour lesquels le marshaling P/Invoke et le marshaling des champs sont identiques.

Mot clé C# Type .NET Type natif
byte System.Byte uint8_t
sbyte System.SByte int8_t
short System.Int16 int16_t
ushort System.UInt16 uint16_t
int System.Int32 int32_t
uint System.UInt32 uint32_t
long System.Int64 int64_t
ulong System.UInt64 uint64_t
char System.Char Soit char soit char16_t selon l’encodage du P/Invoke ou de la structure. Consultez la documentation de l’ensemble de caractères.
System.Char Soit char* soit char16_t* selon l’encodage du P/Invoke ou de la structure. Consultez la documentation de l’ensemble de caractères.
nint System.IntPtr intptr_t
nuint System.UIntPtr uintptr_t
Types de pointeur .NET (par exemple void*) void*
Type dérivé de System.Runtime.InteropServices.SafeHandle void*
Type dérivé de System.Runtime.InteropServices.CriticalHandle void*
bool System.Boolean Type BOOL Win32
decimal System.Decimal Struct DECIMAL COM
Délégué .NET Pointeur de fonction natif
System.DateTime Type DATE Win32
System.Guid Type GUID Win32

Certaines catégories ont des valeurs par défaut différentes pour le marshaling comme paramètre ou comme structure.

Type .NET Type natif (paramètre) Type natif (champ)
Tableau .NET Pointeur vers le début d’un tableau de représentations natives des éléments du tableau Non autorisé sans [MarshalAs] attribut
Classe avec LayoutKind de Sequential ou de Explicit Pointeur vers la représentation native de la classe Représentation native de la classe

Le tableau suivant inclut les règles de marshaling par défaut qui sont uniquement pour Windows. Sur les autres plateformes, il n’est pas possible de marshaler ces types.

Type .NET Type natif (paramètre) Type natif (champ)
System.Object VARIANT IUnknown*
System.Array Interface COM Non autorisé sans [MarshalAs] attribut
System.ArgIterator va_list Non autorisé
System.Collections.IEnumerator IEnumVARIANT* Non autorisé
System.Collections.IEnumerable IDispatch* Non autorisé
System.DateTimeOffset int64_t représentant le nombre de cycles depuis le 1er janvier 1601 à minuit int64_t représentant le nombre de cycles depuis le 1er janvier 1601 à minuit

Certains types peuvent uniquement être marshalés en tant que paramètres et non en tant que champs. Ces types sont répertoriés dans le tableau suivant :

Type .NET Type natif (paramètre uniquement)
System.Text.StringBuilder char* ou char16_t* selon le CharSet du P/Invoke. Consultez la documentation sur les ensembles de caractères.
System.ArgIterator va_list (sur Windows x86/x64/arm64 uniquement)
System.Runtime.InteropServices.ArrayWithOffset void*
System.Runtime.InteropServices.HandleRef void*

Si ces valeurs par défaut ne font pas exactement ce que vous voulez, vous pouvez personnaliser la façon dont les paramètres sont marshalés. L’article de marshaling de paramètres vous guide tout au long de la façon dont différents types de paramètres sont marshalés.

Marshaling par défaut dans les scénarios COM

Lorsque vous appelez des méthodes sur des objets COM dans .NET, le runtime .NET modifie les règles de marshaling par défaut pour qu’elles correspondent à la sémantique COM courante. Le tableau suivant répertorie les règles que les runtimes .NET utilisent dans les scénarios COM :

Type .NET Type natif (appels de méthode COM)
System.Boolean VARIANT_BOOL
StringBuilder LPWSTR
System.String BSTR
Types délégués _Delegate* dans .NET Framework. Non autorisé dans .NET Core et .NET 5+.
System.Drawing.Color OLECOLOR
Tableau .NET SAFEARRAY
System.String[] SAFEARRAY sur BSTR

Marshaling de classes et de structures

Un autre aspect du marshaling de types consiste à passer une structure à une méthode non managée. Par exemple, certaines des méthodes non managées nécessitent un struct en tant que paramètre. Dans ces cas, vous devez créer un struct correspondant ou une classe dans une partie gérée du monde pour l’utiliser comme paramètre. Toutefois, la définition de la classe n’est pas suffisante, vous devez également indiquer au marshaller comment mapper les champs de la classe au struct non managé. Ici, l’attribut StructLayout devient utile.

using System;
using System.Runtime.InteropServices;

Win32Interop.GetSystemTime(out Win32Interop.SystemTime systemTime);

Console.WriteLine(systemTime.Year);

internal static partial class Win32Interop
{
    [LibraryImport("kernel32.dll")]
    internal static partial void GetSystemTime(out SystemTime systemTime);

    [StructLayout(LayoutKind.Sequential)]
    internal ref struct SystemTime
    {
        public ushort Year;
        public ushort Month;
        public ushort DayOfWeek;
        public ushort Day;
        public ushort Hour;
        public ushort Minute;
        public ushort Second;
        public ushort Millisecond;
    }
}

Le code précédent montre un exemple simple d'appel d'une fonction GetSystemTime(). La partie intéressante se trouve à la ligne 13. L’attribut spécifie que les champs de la classe doivent être mappés séquentiellement au struct de l’autre côté (non managé). Cela signifie que le nommage des champs n’est pas important, seul son ordre est important, car il doit correspondre au struct non managé, illustré dans l’exemple suivant :

typedef struct _SYSTEMTIME {
  WORD wYear;
  WORD wMonth;
  WORD wDayOfWeek;
  WORD wDay;
  WORD wHour;
  WORD wMinute;
  WORD wSecond;
  WORD wMilliseconds;
} SYSTEMTIME, *PSYSTEMTIME, *LPSYSTEMTIME;

Il est possible que le marshaling par défaut de votre structure ne vous convienne pas. L’article Personnalisation de l'assemblage de structure vous apprend à personnaliser la manière dont votre structure est assemblée.