Marshaling de types

Le marshaling est le processus qui consiste à transformer les types quand ils doivent naviguer entre du code managé et du code natif.

La raison pour laquelle le marshaling est nécessaire est que les types diffèrent entre le code managé et le code non managé. 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 prendre la bonne décision en fonction du comportement par défaut, décrit dans cet article. Toutefois, dans les cas où vous avez besoin de plus de contrôle, vous pouvez employer l’attribut MarshalAs pour spécifier le type attendu du côté du code non managé. Par exemple, pour que la chaîne soit envoyée sous forme de chaîne UTF-8 terminée par null, vous pouvez procéder ainsi :

[LibraryImport("somenativelibrary.dll")]
static extern int MethodA([MarshalAs(UnmanagedType.LPStr)] 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 Marshaling de 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 indiquent comment chaque type est marshalé par défaut lorsqu’il est utilisé dans un paramètre ou un champ. Les types de caractères et d’entiers de longueur fixe C99/C ++11 garantissent que le tableau suivant est correct pour toutes les plateformes. Vous pouvez utiliser n’importe quel type natif ayant les mêmes exigence 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 char ou char16_t selon l’encodage de P/Invoke ou la structure. Voir la documentation charset.
System.Char char* ou char16_t* selon l’encodage de P/Invoke ou la structure. Voir la documentation charset.
nint System.IntPtr intptr_t
nuint System.UIntPtr uintptr_t
Types pointeur .NET (p. ex. 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 attribut [MarshalAs]
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 présente les règles de marshaling par défaut propres à 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 attribut [MarshalAs]
System.ArgIterator va_list Non autorisé
System.Collections.IEnumerator IEnumVARIANT* Non autorisé
System.Collections.IEnumerable IDispatch* Non autorisé
System.DateTimeOffset représentant le nombre de cycles depuis le 1er janvier 1601 à minuit représentant le nombre de cycles depuis le 1er janvier 1601 à minuit

Certains types ne peuvent être marshalés que comme paramètres, et non comme champs :

Type .NET Type natif (paramètre uniquement)
System.Text.StringBuilder char* ou char16_t* selon le CharSet de P/Invoke. Voir la documentation charset.
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 vous conviennent pas tout à fait, vous pouvez personnaliser la façon dont les paramètres sont marshalés. L’article Marshaling des paramètres explique comment faire, pour différents types de paramètres.

Marshaling par défaut dans les scénarios COM

Quand 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 liste les règles utilisées par les runtimes .NET dans les scénarios COM :

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

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 une structure comme paramètre. Il faut dans ce cas créer une classe ou un struct correspondant dans la partie managée de l’environnement pour l’utiliser comme paramètre. Toutefois, il ne suffit pas de définir la classe : il est également nécessaire d’indiquer au marshaleur comment mapper des champs de la classe au struct non managé. C’est ici qu’intervient l’attribut StructLayout.

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

[StructLayout(LayoutKind.Sequential)]
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;
}

public static void Main(string[] args)
{
    SystemTime st = new SystemTime();
    GetSystemTime(st);
    Console.WriteLine(st.Year);
}

Le code précédent illustre de manière simple les appels dans la fonction GetSystemTime(). L’élément digne d’intérêt se trouve sur la ligne 4. L’attribut spécifie que les champs de la classe doivent être mappés séquentiellement à la structure de l’autre côté (non managé). Cela signifie que le nom des champs n’a pas d’importance ; seul leur ordre compte, car il doit correspondre au struct non managé, comme 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;

Il est possible que le marshaling par défaut de votre structure ne vous convienne pas. L’article Personnaliser le marshaling des structures explique comment personnaliser la façon dont les structures sont marshalées.