Customize structure marshalling
Sometimes the default marshalling rules for structures aren't exactly what you need. The .NET runtimes provide a few extension points for you to customize your structure's layout and how fields are marshalled. Customizing structure layout is supported for all scenarios, but customizing field marshalling is only supported for scenarios where runtime marshalling is enabled. If runtime marshalling is disabled, then any field marshalling must be done manually.
Note
This article doesn't cover customizing marshalling for source-generated interop. If you're using source-generated interop for P/Invokes or COM, see customizing marshalling.
Customize structure layout
.NET provides the System.Runtime.InteropServices.StructLayoutAttribute attribute and the System.Runtime.InteropServices.LayoutKind enumeration to allow you to customize how fields are placed in memory. The following guidance will help you avoid common issues.
✔️ CONSIDER using LayoutKind.Sequential
whenever possible.
✔️ DO only use LayoutKind.Explicit
in marshalling when your native struct also has an explicit layout, such as a union.
❌ AVOID using classes to express complex native types through inheritance.
❌ AVOID using LayoutKind.Explicit
when marshalling structures on non-Windows platforms if you need to target runtimes before .NET Core 3.0. The .NET Core runtime before 3.0 doesn't support passing explicit structures by value to native functions on Intel or AMD 64-bit non-Windows systems. However, the runtime supports passing explicit structures by reference on all platforms.
Customizing Boolean field marshalling
Native code has many different Boolean representations. On Windows alone, there are three ways to represent Boolean values. The runtime doesn't know the native definition of your structure, so the best it can do is make a guess on how to marshal your Boolean values. The .NET runtime provides a way to indicate how to marshal your Boolean field. The following examples show how to marshal .NET bool
to different native Boolean types.
Boolean values default to marshalling as a native 4-byte Win32 BOOL
value as shown in the following example:
public struct WinBool
{
public bool b;
}
struct WinBool
{
public BOOL b;
};
If you want to be explicit, you can use the UnmanagedType.Bool value to get the same behavior as above:
public struct WinBool
{
[MarshalAs(UnmanagedType.Bool)]
public bool b;
}
struct WinBool
{
public BOOL b;
};
Using the UnmanagedType.U1
or UnmanagedType.I1
values below, you can tell the runtime to marshal the b
field as a 1-byte native bool
type.
public struct CBool
{
[MarshalAs(UnmanagedType.U1)]
public bool b;
}
struct CBool
{
public bool b;
};
On Windows, you can use the UnmanagedType.VariantBool value to tell the runtime to marshal your Boolean value to a 2-byte VARIANT_BOOL
value:
public struct VariantBool
{
[MarshalAs(UnmanagedType.VariantBool)]
public bool b;
}
struct VariantBool
{
public VARIANT_BOOL b;
};
Note
VARIANT_BOOL
is different than most bool types in that VARIANT_TRUE = -1
and VARIANT_FALSE = 0
. Additionally, all values that aren't equal to VARIANT_TRUE
are considered false.
Customizing array field marshalling
.NET also includes a few ways to customize array marshalling.
By default, .NET marshals arrays as a pointer to a contiguous list of the elements:
public struct DefaultArray
{
public int[] values;
}
struct DefaultArray
{
int32_t* values;
};
If you're interfacing with COM APIs, you may have to marshal arrays as SAFEARRAY*
objects. You can use the System.Runtime.InteropServices.MarshalAsAttribute and the UnmanagedType.SafeArray value to tell the runtime to marshal an array as a SAFEARRAY*
:
public struct SafeArrayExample
{
[MarshalAs(UnmanagedType.SafeArray)]
public int[] values;
}
struct SafeArrayExample
{
SAFEARRAY* values;
};
If you need to customize what type of element is in the SAFEARRAY
, then you can use the MarshalAsAttribute.SafeArraySubType and MarshalAsAttribute.SafeArrayUserDefinedSubType fields to customize the exact element type of the SAFEARRAY
.
If you need to marshal the array in-place, you can use the UnmanagedType.ByValArray value to tell the marshaller to marshal the array in-place. When you're using this marshalling, you also must supply a value to the MarshalAsAttribute.SizeConst field for the number of elements in the array so the runtime can correctly allocate space for the structure.
public struct InPlaceArray
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
public int[] values;
}
struct InPlaceArray
{
int values[4];
};
Note
.NET doesn't support marshalling a variable length array field as a C99 Flexible Array Member.
Customizing string field marshalling
.NET also provides a wide variety of customizations for marshalling string fields.
By default, .NET marshals a string as a pointer to a null-terminated string. The encoding depends on the value of the StructLayoutAttribute.CharSet field in the System.Runtime.InteropServices.StructLayoutAttribute. If no attribute is specified, the encoding defaults to an ANSI encoding.
[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.
};
If you need to use different encodings for different fields or just prefer to be more explicit in your struct definition, you can use the UnmanagedType.LPStr or UnmanagedType.LPWStr values on a System.Runtime.InteropServices.MarshalAsAttribute attribute.
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.
};
If you want to marshal your strings using the UTF-8 encoding, you can use the UnmanagedType.LPUTF8Str value in your MarshalAsAttribute.
public struct UTF8String
{
[MarshalAs(UnmanagedType.LPUTF8Str)]
public string str;
}
struct UTF8String
{
char* str;
};
Note
Using UnmanagedType.LPUTF8Str requires either .NET Framework 4.7 (or later versions) or .NET Core 1.1 (or later versions). It isn't available in .NET Standard 2.0.
If you're working with COM APIs, you may need to marshal a string as a BSTR
. Using the UnmanagedType.BStr value, you can marshal a string as a BSTR
.
public struct BString
{
[MarshalAs(UnmanagedType.BStr)]
public string str;
}
struct BString
{
BSTR str;
};
When using a WinRT-based API, you may need to marshal a string as an HSTRING
. Using the UnmanagedType.HString value, you can marshal a string as a HSTRING
. HSTRING
marshalling is only supported on runtimes with built-in WinRT support. WinRT support was removed in .NET 5, so HSTRING
marshalling is not supported in .NET 5 or newer.
public struct HString
{
[MarshalAs(UnmanagedType.HString)]
public string str;
}
struct BString
{
HSTRING str;
};
If your API requires you to pass the string in-place in the structure, you can use the UnmanagedType.ByValTStr value. Do note that the encoding for a string marshalled by ByValTStr
is determined from the CharSet
attribute. Additionally, it requires that a string length is passed by the MarshalAsAttribute.SizeConst field.
[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.
};
Customizing decimal field marshalling
If you're working on Windows, you might encounter some APIs that use the native CY
or CURRENCY
structure. By default, the .NET decimal
type marshals to the native DECIMAL
structure. However, you can use a MarshalAsAttribute with the UnmanagedType.Currency value to instruct the marshaller to convert a decimal
value to a native CY
value.
public struct Currency
{
[MarshalAs(UnmanagedType.Currency)]
public decimal dec;
}
struct Currency
{
CY dec;
};
Unions
A union is a data type that can contain different types of data atop the same memory. It's a common form of data in the C language. A union can be expressed in .NET using LayoutKind.Explicit
. It's recommended to use structs when defining a union in .NET. Using classes can cause layout issues and produce unpredictable behavior.
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;
}
}
Marshal System.Object
On Windows, you can marshal object
-typed fields to native code. You can marshal these fields to one of three types:
By default, an object
-typed field will be marshalled to an IUnknown*
that wraps the object.
public struct ObjectDefault
{
public object obj;
}
struct ObjectDefault
{
IUnknown* obj;
};
If you want to marshal an object field to an IDispatch*
, add a MarshalAsAttribute with the UnmanagedType.IDispatch value.
public struct ObjectDispatch
{
[MarshalAs(UnmanagedType.IDispatch)]
public object obj;
}
struct ObjectDispatch
{
IDispatch* obj;
};
If you want to marshal it as a VARIANT
, add a MarshalAsAttribute with the UnmanagedType.Struct value.
public struct ObjectVariant
{
[MarshalAs(UnmanagedType.Struct)]
public object obj;
}
struct ObjectVariant
{
VARIANT obj;
};
The following table describes how different runtime types of the obj
field map to the various types stored in a VARIANT
:
.NET Type | VARIANT Type |
---|---|
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 |