类型封送

封送是当类型需要在托管代码和本机代码之间切换时转换类型的过程。

需要封送的原因是托管代码与非托管代码中的类型并不相同。 例如,在托管代码中,有一个 string,但非托管字符串可以是 .NET string 编码 (UTF-16)、ANSI 代码页编码、UTF-8、null 结尾、ASCII 等。默认情况下,P/Invoke 子系统会尝试基于默认行为执行正确的操作,如本文中所述。 但是,对于需要额外控制的情况,可以使用 MarshalAs 属性来指定非托管端的预期类型。 例如,如果希望字符串作为以 null 结尾的 UTF-8 字符串发送,则可以按如下所示执行此作:

[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);

如果将属性应用于 System.Runtime.CompilerServices.DisableRuntimeMarshallingAttribute 程序集,则以下部分中的规则不适用。 有关在应用此属性时 .NET 值如何暴露给本机代码的信息,请参阅 禁用运行时封送处理

封送通用类型的默认规则

通常,运行时会尝试在封送时执行“正确的操作”,从而最大限地减少用户的工作。 下表介绍了每种类型在用于参数或字段时的默认封送方式。 为了确保以下表格在所有平台上的正确性,使用了 C99/C++11 固定宽度的整数和字符类型。 可以使用与这些类型具有相同对齐和大小要求的任何原生类型。

第一个表介绍了其封送与 P/Invoke 和字段封送相同的各种类型的映射。

C# 关键字 .NET 类型 原生类型
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 charchar16_t取决于 P/Invoke 或结构的编码。 请参阅 charset 文档
System.Char char*char16_t*取决于 P/Invoke 或结构的编码。 请参阅 charset 文档
nint System.IntPtr intptr_t
nuint System.UIntPtr uintptr_t
.NET 指针类型(例如 void* void*
System.Runtime.InteropServices.SafeHandle 派生的类型 void*
System.Runtime.InteropServices.CriticalHandle 派生的类型 void*
bool System.Boolean Win32 BOOL 类型
decimal System.Decimal COM DECIMAL 结构
.NET 委托 本机函数指针
System.DateTime Win32 DATE 类型
System.Guid Win32 GUID 类型

如果作为参数或结构进行封送,则几个封送类别具有不同的默认设置。

.NET 类型 原生类型(参数) 本机类型(字段)
.NET 数组 指向数组元素的本机表示形式的数组开头的指针。 不允许不带 [MarshalAs] 属性
LayoutKindSequentialExplicit 的类 指向类的本机表示形式的指针 类的本机表示形式

下表包含仅适用于 Windows 的默认封送规则。 在非 Windows 平台上,无法封送这些类型。

.NET 类型 原生类型(参数) 本机类型(字段)
System.Object VARIANT IUnknown*
System.Array COM 接口 不允许不带 [MarshalAs] 属性
System.ArgIterator va_list 不允许
System.Collections.IEnumerator IEnumVARIANT* 不允许
System.Collections.IEnumerable IDispatch* 不允许
System.DateTimeOffset int64_t 表示自 1601 年 1 月 1 日午夜以来的时钟周期数 int64_t 表示自 1601 年 1 月 1 日午夜以来的时钟周期数

某些类型只能作为参数进行封送,不能作为字段进行封送。 下表列出了这些类型:

.NET 类型 原生类型(仅限参数)
System.Text.StringBuilder char*char16_t* 依赖于 P/Invoke 的 CharSet。 请参阅 charset 文档
System.ArgIterator va_list (仅在 Windows x86/x64/arm64 上)
System.Runtime.InteropServices.ArrayWithOffset void*
System.Runtime.InteropServices.HandleRef void*

如果这些默认值没有完全实现你想要的功能,你可以自定义参数传递方式。 参数封送文章将指导你如何自定义不同参数类型的封送方式。

COM 方案中的默认封送

在 .NET 中对 COM 对象调用方法时,.NET 运行时将更改默认封送规则以匹配常见的 COM 语义。 下表列出了 .NET 运行时在 COM 方案中使用的规则:

.NET 类型 本机类型(COM 方法调用)
System.Boolean VARIANT_BOOL
StringBuilder LPWSTR
System.String BSTR
委托类型 在 .NET Framework 中为 _Delegate*。 不允许在 .NET Core 和 .NET 5+ 中使用。
System.Drawing.Color OLECOLOR
.NET 数组 SAFEARRAY
System.String[] SAFEARRAYBSTR

封送类和结构

有关类型封送的另一个问题是如何将结构传入非托管方法。 例如,某些非托管方法需要一个结构体作为参数。 在这些情况下,你需要在世界上的托管部分创建相应的结构或类,以将其用作参数。 但是,只需定义类是不够的,还需要指示封送器如何将类中的字段映射到非托管结构。 在这里,该 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);
}

前面的代码演示了调用 GetSystemTime() 函数的简单示例。 有趣的部分在第 4 行。 该属性指定应按顺序将类的字段映射到另一端(非托管端)上的结构。 这意味着字段的命名并不重要,只有其顺序很重要,因为它需要对应于非托管结构,如以下示例所示:

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

有时,默认结构封送不执行所需的操作。 自定义结构封送一文介绍如何自定义结构的封送方式。