Type marshalling

Marshalling is the process of transforming types when they need to cross between managed and native code.

Marshalling is needed because the types in the managed and unmanaged code are different. In managed code, for instance, you have a String, while in the unmanaged world strings can be Unicode ("wide"), non-Unicode, null-terminated, ASCII, etc. By default, the P/Invoke subsystem tries to do the right thing based on the default behavior, described on this article. However, for those situations where you need extra control, you can employ the MarshalAs attribute to specify what is the expected type on the unmanaged side. For instance, if you want the string to be sent as a null-terminated ANSI string, you could do it like this:

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

If you apply the System.Runtime.CompilerServices.DisableRuntimeMarshallingAttribute attribute to the assembly, the rules in the following section don't apply. For information on how .NET values are exposed to native code when this attribute is applied, see disabled runtime marshalling.

Default rules for marshalling common types

Generally, the runtime tries to do the "right thing" when marshalling to require the least amount of work from you. The following tables describe how each type is marshalled by default when used in a parameter or field. The C99/C++11 fixed-width integer and character types are used to ensure that the following table is correct for all platforms. You can use any native type that has the same alignment and size requirements as these types.

This first table describes the mappings for various types for whom the marshalling is the same for both P/Invoke and field marshalling.

C# keyword .NET Type Native Type
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 Either char or char16_t depending on the CharSet of the P/Invoke or structure. See the charset documentation.
System.Char Either char* or char16_t* depending on the CharSet of the P/Invoke or structure. See the charset documentation.
nint System.IntPtr intptr_t
nuint System.UIntPtr uintptr_t
.NET Pointer types (ex. void*) void*
Type derived from System.Runtime.InteropServices.SafeHandle void*
Type derived from System.Runtime.InteropServices.CriticalHandle void*
bool System.Boolean Win32 BOOL type
decimal System.Decimal COM DECIMAL struct
.NET Delegate Native function pointer
System.DateTime Win32 DATE type
System.Guid Win32 GUID type

A few categories of marshalling have different defaults if you're marshalling as a parameter or structure.

.NET Type Native Type (Parameter) Native Type (Field)
.NET array A pointer to the start of an array of native representations of the array elements. Not allowed without a [MarshalAs] attribute
A class with a LayoutKind of Sequential or Explicit A pointer to the native representation of the class The native representation of the class

The following table includes the default marshalling rules that are Windows-only. On non-Windows platforms, you cannot marshal these types.

.NET Type Native Type (Parameter) Native Type (Field)
System.Object VARIANT IUnknown*
System.Array COM interface Not allowed without a [MarshalAs] attribute
System.ArgIterator va_list Not allowed
System.Collections.IEnumerator IEnumVARIANT* Not allowed
System.Collections.IEnumerable IDispatch* Not allowed
System.DateTimeOffset int64_t representing the number of ticks since midnight on January 1, 1601 int64_t representing the number of ticks since midnight on January 1, 1601

Some types can only be marshalled as parameters and not as fields. These types are listed in the following table:

.NET Type Native Type (Parameter Only)
System.Text.StringBuilder Either char* or char16_t* depending on the CharSet of the P/Invoke. See the charset documentation.
System.ArgIterator va_list (on Windows x86/x64/arm64 only)
System.Runtime.InteropServices.ArrayWithOffset void*
System.Runtime.InteropServices.HandleRef void*

If these defaults don't do exactly what you want, you can customize how parameters are marshalled. The parameter marshalling article walks you through how to customize how different parameter types are marshalled.

Default marshalling in COM scenarios

When you are calling methods on COM objects in .NET, the .NET runtime changes the default marshalling rules to match common COM semantics. The following table lists the rules that .NET runtimes uses in COM scenarios:

.NET Type Native Type (COM method calls)
System.Boolean VARIANT_BOOL
StringBuilder LPWSTR
System.String BSTR
Delegate types _Delegate* in .NET Framework. Disallowed in .NET Core and .NET 5+.
System.Drawing.Color OLECOLOR
.NET array SAFEARRAY
System.String[] SAFEARRAY of BSTRs

Marshalling classes and structs

Another aspect of type marshalling is how to pass in a struct to an unmanaged method. For instance, some of the unmanaged methods require a struct as a parameter. In these cases, you need to create a corresponding struct or a class in managed part of the world to use it as a parameter. However, just defining the class isn't enough, you also need to instruct the marshaller how to map fields in the class to the unmanaged struct. Here the StructLayout attribute becomes useful.

[DllImport("kernel32.dll")]
static extern void GetSystemTime(SystemTime systemTime);

[StructLayout(LayoutKind.Sequential)]
class SystemTime {
    public ushort Year;
    public ushort Month;
    public ushort DayOfWeek;
    public ushort Day;
    public ushort Hour;
    public ushort Minute;
    public ushort Second;
    public ushort Milsecond;
}

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

The previous code shows a simple example of calling into GetSystemTime() function. The interesting bit is on line 4. The attribute specifies that the fields of the class should be mapped sequentially to the struct on the other (unmanaged) side. This means that the naming of the fields isn't important, only their order is important, as it needs to correspond to the unmanaged struct, shown in the following example:

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

Sometimes the default marshalling for your structure doesn't do what you need. The Customizing structure marshalling article teaches you how to customize how your structure is marshalled.