Partager via


Personnaliser le marshaling de structure

Parfois, les règles de marshaling par défaut pour les structures ne sont pas exactement ce dont vous avez besoin. Les runtimes .NET fournissent quelques points d’extension qui vous permettent de personnaliser la disposition de votre structure et la manière dont les champs sont marshalés. La personnalisation de la disposition de structure est prise en charge pour tous les scénarios, mais la personnalisation du marshaling de champs n’est prise en charge que pour les scénarios dans lesquels le marshaling d’exécution est activé. Si le marshaling du runtime est désactivé, tout marshaling de champ doit être effectué manuellement.

Remarque

Cet article ne couvre pas la personnalisation du marshaling pour l’interopérabilité générée par la source. Si vous utilisez l’interopérabilité générée par la source pour P/Invokes ou COM, consultez Personnalisation de marshaling.

Personnaliser la structure des layouts

.NET fournit l’attribut System.Runtime.InteropServices.StructLayoutAttribute et l’énumération System.Runtime.InteropServices.LayoutKind pour vous permettre de personnaliser la façon dont les champs sont placés en mémoire. Les conseils suivants vous aideront à éviter les problèmes courants.

ENVISAGEZ de ✔️ d’utiliser LayoutKind.Sequential dans la mesure du possible.

✔️ FAITES uniquement usage de LayoutKind.Explicit dans le marshaling lorsque votre struct natif a également une disposition explicite, par exemple une union.

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

❌ ÉVITEZ d’utiliser LayoutKind.Explicit lors du marshaling des structures sur des plateformes non Windows si vous devez cibler des runtimes antérieurs à .NET Core 3.0. Le runtime .NET Core antérieur à 3.0 ne prend pas en charge le passage de structures explicites par valeur aux fonctions natives sur les systèmes Intel ou AMD 64 bits autres que Windows. Toutefois, le runtime prend en charge le passage de structures par référence sur toutes les plateformes.

Personnalisation du marshaling des champs booléens

Le code natif a de nombreuses représentations booléennes différentes. Sur Windows uniquement, il y a trois façons de représenter des valeurs booléennes. Le runtime ne connaissant pas la définition native de votre structure, le mieux est de faire une estimation sur la manière de marshaler vos valeurs booléennes. Le runtime .NET fournit un moyen d’indiquer comment marshaler votre champ booléen. Les exemples suivants montrent comment marshaler .NET bool pour différents types booléens natifs.

Les valeurs booléennes par défaut pour le marshaling sont une valeur native Win32 sur 4 octets BOOL, comme indiqué dans l’exemple suivant :

public struct WinBool
{
    public bool b;
}
struct WinBool
{
    public BOOL b;
};

Si vous souhaitez être explicite, vous pouvez utiliser la valeur UnmanagedType.Bool pour obtenir le même comportement que ci-dessus :

public struct WinBool
{
    [MarshalAs(UnmanagedType.Bool)]
    public bool b;
}
struct WinBool
{
    public BOOL b;
};

À l’aide des valeurs UnmanagedType.U1 ou UnmanagedType.I1 ci-dessous, vous pouvez indiquer au runtime de marshaler le champ b comme type bool natif sur 1 octet.

public struct CBool
{
    [MarshalAs(UnmanagedType.U1)]
    public bool b;
}
struct CBool
{
    public bool b;
};

Sur Windows, vous pouvez utiliser la valeur UnmanagedType.VariantBool pour indiquer au runtime de marshaler votre valeur booléenne dans une valeur VARIANT_BOOL sur 2 octets :

public struct VariantBool
{
    [MarshalAs(UnmanagedType.VariantBool)]
    public bool b;
}
struct VariantBool
{
    public VARIANT_BOOL b;
};

Notes

VARIANT_BOOL est différent de la plupart des types de bools dans VARIANT_TRUE = -1 et VARIANT_FALSE = 0. De plus, toutes les valeurs qui ne sont pas égales à VARIANT_TRUE sont considérées comme false.

Personnalisation du marshaling des champs de tableaux

.NET inclut également quelques façons de personnaliser le marshaling de tableaux.

Par défaut, .NET marshale des tableaux en tant que pointeur vers une liste contiguë d’éléments :

public struct DefaultArray
{
    public int[] values;
}
struct DefaultArray
{
    int32_t* values;
};

Si vous interagissez avec des API COM, vous devez marshaler des tableaux en tant qu’objets SAFEARRAY*. Vous pouvez utiliser les valeurs System.Runtime.InteropServices.MarshalAsAttribute et UnmanagedType.SafeArray pour indiquer au runtime de marshaler un tableau en tant que SAFEARRAY* :

public struct SafeArrayExample
{
    [MarshalAs(UnmanagedType.SafeArray)]
    public int[] values;
}
struct SafeArrayExample
{
    SAFEARRAY* values;
};

Si vous devez personnaliser le type d’élément dans le SAFEARRAY, vous pouvez utiliser les champs MarshalAsAttribute.SafeArraySubType et MarshalAsAttribute.SafeArrayUserDefinedSubType pour personnaliser le type d’élément exacte du SAFEARRAY.

Si vous devez marshaler le tableau sur place, vous pouvez utiliser la valeur UnmanagedType.ByValArray pour indiquer au marshaleur de le faire. Lorsque vous utilisez ce marshaling, vous devez également fournir une valeur au champ MarshalAsAttribute.SizeConst pour le nombre d’éléments du tableau afin que le runtime puisse allouer correctement de l’espace pour la structure.

public struct InPlaceArray
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
    public int[] values;
}
struct InPlaceArray
{
    int values[4];
};

Notes

.NET ne prend pas en charge le marshaling d’un champ de tableau de longueur variable en tant que membre de tableau flexible C99.

Personnalisation du marshaling des champs de chaînes

.NET fournit également un large éventail de personnalisations pour marshaler les champs de chaînes.

Par défaut, .NET marshale une chaîne en tant que pointeur vers une chaîne se terminant par Null. L’encodage dépend de la valeur du champ StructLayoutAttribute.CharSet dans le System.Runtime.InteropServices.StructLayoutAttribute. Si aucun attribut n’est spécifié, l’encodage par défaut est un encodage ANSI.

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct DefaultString
{
    public string str;
}
struct DefaultString
{
    char* str;
};
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct DefaultString
{
    public string str;
}
struct DefaultString
{
    char16_t* str; // Could also be wchar_t* on Windows.
};

Si vous devez utiliser différents codages pour les différents champs ou si vous préférez simplement être plus explicite dans votre définition de struct, vous pouvez utiliser les valeurs UnmanagedType.LPStr ou UnmanagedType.LPWStr sur un attribut System.Runtime.InteropServices.MarshalAsAttribute.

public struct AnsiString
{
    [MarshalAs(UnmanagedType.LPStr)]
    public string str;
}
struct AnsiString
{
    char* str;
};
public struct UnicodeString
{
    [MarshalAs(UnmanagedType.LPWStr)]
    public string str;
}
struct UnicodeString
{
    char16_t* str; // Could also be wchar_t* on Windows.
};

Si vous voulez marshaler vos chaînes à l’aide de l’encodage UTF-8, vous pouvez utiliser la valeur UnmanagedType.LPUTF8Str dans votre MarshalAsAttribute.

public struct UTF8String
{
    [MarshalAs(UnmanagedType.LPUTF8Str)]
    public string str;
}
struct UTF8String
{
    char* str;
};

Notes

L’utilisation de UnmanagedType.LPUTF8Str requiert .NET Framework 4.7 (ou versions ultérieures) ou .NET Core 1.1 (ou versions ultérieures). Elle n’est pas disponible dans .NET Standard 2.0.

Si vous travaillez avec des API COM, vous devrez peut-être marshaler une chaîne comme BSTR. À l’aide de la valeur UnmanagedType.BStr, vous pouvez marshaler une chaîne comme BSTR.

public struct BString
{
    [MarshalAs(UnmanagedType.BStr)]
    public string str;
}
struct BString
{
    BSTR str;
};

Lorsque vous utilisez une API WinRT, vous devez marshaler une chaîne comme HSTRING. À l’aide de la valeur UnmanagedType.HString, vous pouvez marshaler une chaîne comme HSTRING. Le marshaling HSTRING n’est pris en charge que sur les runtimes avec une prise en charge winRT intégrée. La prise en charge de WinRT a été supprimée dans .NET 5, de sorte que le marshaling HSTRING n’est pas pris en charge dans .NET 5 ou version ultérieure.

public struct HString
{
    [MarshalAs(UnmanagedType.HString)]
    public string str;
}
struct BString
{
    HSTRING str;
};

Si votre API vous oblige à passer la chaîne sur place dans la structure, vous pouvez utiliser la valeur UnmanagedType.ByValTStr. Notez que l’encodage d’une chaîne marshalée par ByValTStr est déterminé à partir de l’attribut CharSet. Ceci requiert en outre le passage d’une longueur de chaîne par le champ MarshalAsAttribute.SizeConst.

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct DefaultString
{
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 4)]
    public string str;
}
struct DefaultString
{
    char str[4];
};
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct DefaultString
{
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 4)]
    public string str;
}
struct DefaultString
{
    char16_t str[4]; // Could also be wchar_t[4] on Windows.
};

Personnalisation du marshaling de champs décimaux

Si vous travaillez sur Windows, il est possible que certaines API utilisent la structure native CY ou CURRENCY. Par défaut, le type .NET decimal marshale vers la structure native DECIMAL. Toutefois, vous pouvez utiliser un MarshalAsAttribute avec la valeur UnmanagedType.Currency pour indiquer au marshaleur de convertir une valeur decimal en valeur CY native.

public struct Currency
{
    [MarshalAs(UnmanagedType.Currency)]
    public decimal dec;
}
struct Currency
{
    CY dec;
};

Unions

Une union est un type de données qui peut contenir différents types de données sur la même mémoire. Il s’agit d’une forme courante de données dans le langage C. Une union peut être exprimée dans .NET à l’aide de LayoutKind.Explicit. Il est recommandé d’utiliser des structs lors de la définition d’une union dans .NET. L’utilisation de classes peut entraîner des problèmes de disposition et produire un comportement imprévisible.

struct device1_config
{
    void* a;
    void* b;
    void* c;
};
struct device2_config
{
    int32_t a;
    int32_t b;
};
struct config
{
    int32_t type;

    union
    {
        device1_config dev1;
        device2_config dev2;
    };
};
public unsafe struct Device1Config
{
    void* a;
    void* b;
    void* c;
}

public struct Device2Config
{
    int a;
    int b;
}

public struct Config
{
    public int Type;

    public _Union Anonymous;

    [StructLayout(LayoutKind.Explicit)]
    public struct _Union
    {
        [FieldOffset(0)]
        public Device1Config Dev1;

        [FieldOffset(0)]
        public Device2Config Dev2;
    }
}

Marshaler System.Object

Sur Windows, vous pouvez marshaler des champs de type object dans du code natif. Vous pouvez marshaler ces champs dans l’un des trois types suivants :

Par défaut, un champ de type object sera marshalé dans un IUnknown* qui inclut l’objet dans un wrapper.

public struct ObjectDefault
{
    public object obj;
}
struct ObjectDefault
{
    IUnknown* obj;
};

Si vous voulez marshaler un champ d’objet pour un IDispatch*, ajoutez un MarshalAsAttribute avec la valeur UnmanagedType.IDispatch.

public struct ObjectDispatch
{
    [MarshalAs(UnmanagedType.IDispatch)]
    public object obj;
}
struct ObjectDispatch
{
    IDispatch* obj;
};

Si vous voulez le marshaler comme VARIANT, ajoutez un MarshalAsAttribute avec la valeur UnmanagedType.Struct.

public struct ObjectVariant
{
    [MarshalAs(UnmanagedType.Struct)]
    public object obj;
}
struct ObjectVariant
{
    VARIANT obj;
};

Le tableau suivant décrit comment les différents types de runtime du champ obj correspondent aux différents types stockés dans un VARIANT :

Type .NET Type VARIANT
byte VT_UI1
sbyte VT_I1
short VT_I2
ushort VT_UI2
int VT_I4
uint VT_UI4
long VT_I8
ulong VT_UI8
float VT_R4
double VT_R8
char VT_UI2
string VT_BSTR
System.Runtime.InteropServices.BStrWrapper VT_BSTR
object VT_DISPATCH
System.Runtime.InteropServices.UnknownWrapper VT_UNKNOWN
System.Runtime.InteropServices.DispatchWrapper VT_DISPATCH
System.Reflection.Missing VT_ERROR
(object)null VT_EMPTY
bool VT_BOOL
System.DateTime VT_DATE
decimal VT_DECIMAL
System.Runtime.InteropServices.CurrencyWrapper VT_CURRENCY
System.DBNull VT_NULL