类型封送Type marshaling

封送是当类型需要在托管代码和本机代码之间切换时转换类型的过程。Marshaling is the process of transforming types when they need to cross between managed and native code.

需要封送的原因在于托管代码与非托管代码中的类型并不相同。Marshaling is needed because the types in the managed and unmanaged code are different. 例如,在托管代码中,可指定 String。但在非托管环境中,字符串类型可以是 Unicode(“宽型”)、非 Unicode、null 结尾、ASCII 等。默认情况下,P/Invoke 子系统会尝试基于默认行为执行正确的操作,如本文中所述。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. 但是,如果需要额外的控制,可以使用 MarshalAs 属性指定要在非托管端上使用的预期类型。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. 例如,如果想要将字符串作为以 null 结尾的 ANSI 字符串发送,则可以执行如下操作: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);

封送通用类型的默认规则Default rules for marshaling common types

通常,运行时会尝试在封送时执行“正确的操作”,从而最大限地减少用户的工作。Generally, the runtime tries to do the "right thing" when marshaling to require the least amount of work from you. 以下表格介绍了每种类型在用于参数或字段时的默认封送方式。The following tables describe how each type is marshaled by default when used in a parameter or field. C99/C++11 固定宽度的整数和字符类型用于确保下表适用于所有平台。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.

第一个表介绍了其封送与 P/Invoke 和字段封送相同的各种类型的映射。This first table describes the mappings for various types for whom the marshaling is the same for both P/Invoke and field marshaling.

.NET 类型.NET Type 本机类型Native Type
byte uint8_t
sbyte int8_t
short int16_t
ushort uint16_t
int int32_t
uint uint32_t
long int64_t
ulong uint64_t
char charchar16_t 依赖于 P/Invoke 或结构的 CharSetEither char or char16_t depending on the CharSet of the P/Invoke or structure. 请参阅字符集文档See the charset documentation.
string char*char16_t* 依赖于 P/Invoke 或结构的 CharSetEither char* or char16_t* depending on the CharSet of the P/Invoke or structure. 请参阅字符集文档See the charset documentation.
System.IntPtr intptr_t
System.UIntPtr uintptr_t
.NET 指针类型(例如,.NET Pointer types (ex. void*)void*) void*
System.Runtime.InteropServices.SafeHandle 派生的类型Type derived from System.Runtime.InteropServices.SafeHandle void*
System.Runtime.InteropServices.CriticalHandle 派生的类型Type derived from System.Runtime.InteropServices.CriticalHandle void*
bool Win32 BOOL 类型Win32 BOOL type
decimal COM DECIMAL 结构COM DECIMAL struct
.NET 委托.NET Delegate 本机函数指针Native function pointer
System.DateTime Win32 DATE 类型Win32 DATE type
System.Guid Win32 GUID 类型Win32 GUID type

如果作为参数或结构进行封送,则部分封送类别具有不同的默认设置。A few categories of marshaling have different defaults if you're marshaling as a parameter or structure.

.NET 类型.NET Type 本机类型(参数)Native Type (Parameter) 本机类型(字段)Native Type (Field)
.NET 数组.NET array 指向数组元素的本机表示形式的数组开头的指针。A pointer to the start of an array of native representations of the array elements. 不允许不带 [MarshalAs] 属性Not allowed without a [MarshalAs] attribute
LayoutKindSequentialExplicit 的类A class with a LayoutKind of Sequential or Explicit 指向类的本机表示形式的指针A pointer to the native representation of the class 类的本机表示形式The native representation of the class

下表包含仅适用于 Windows 的默认封送规则。The following table includes the default marshaling rules that are Windows-only. 在非 Windows 平台上,无法封送这些类型。On non-Windows platforms, you cannot marshal these types.

.NET 类型.NET Type 本机类型(参数)Native Type (Parameter) 本机类型(字段)Native Type (Field)
object VARIANT IUnknown*
System.Array COM 接口COM interface 不允许不带 [MarshalAs] 属性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 表示自 1601 年 1 月 1 日午夜以来的时钟周期数int64_t representing the number of ticks since midnight on January 1, 1601 int64_t 表示自 1601 年 1 月 1 日午夜以来的时钟周期数int64_t representing the number of ticks since midnight on January 1, 1601

某些类型只能作为参数封送,而不能作为字段封送。Some types can only be marshaled as parameters and not as fields. 下表列出了这些类型:These types are listed in the following table:

.NET 类型.NET Type 本机类型(仅参数)Native Type (Parameter Only)
System.Text.StringBuilder char*char16_t* 依赖于 P/Invoke 的 CharSetEither char* or char16_t* depending on the CharSet of the P/Invoke. 请参阅字符集文档See the charset documentation.
System.ArgIterator va_list(仅在 Windows x86/x64/arm64 上)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 marshaled. 参数封送一文介绍自定义不同参数类型的封送方式的具体步骤。The parameter marshaling article walks you through how to customize how different parameter types are marshaled.

COM 方案中的默认封送Default marshaling in COM scenarios

在 .NET 中调用 COM 对象上的方法时,.NET 运行时会更改默认封送规则,以匹配常见的 COM 语义。When you are calling methods on COM objects in .NET, the .NET runtime changes the default marshaling rules to match common COM semantics. 下表列出了 .NET 运行时在 COM 方案中使用的规则:The following table lists the rules that .NET runtimes uses in COM scenarios:

.NET 类型.NET Type 本机类型(COM 方法调用)Native Type (COM method calls)
bool VARIANT_BOOL
StringBuilder LPWSTR
string BSTR
委托类型Delegate types 在 .NET Framework 中为 _Delegate*_Delegate* in .NET Framework. .NET Core 中不允许使用。Disallowed in .NET Core.
System.Drawing.Color OLECOLOR
.NET 数组.NET array SAFEARRAY
string[] BSTRSAFEARRAYSAFEARRAY of BSTRs

封送类和结构Marshaling classes and structs

有关类型封送的另一个问题是如何将结构传入非托管方法。Another aspect of type marshaling 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 marshaler how to map fields in the class to the unmanaged struct. 在此处,StructLayout 属性将会很有用。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);
}

前面的代码演示了如何调用 GetSystemTime() 函数的简单示例。The previous code shows a simple example of calling into GetSystemTime() function. 值得注意的部分是第 4 行。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 marshaling for your structure doesn't do what you need. 自定义结构封送一文介绍如何自定义结构的封送方式。The Customizing structure marshaling article teaches you how to customize how your structure is marshaled.