类型封送
封送是当类型需要在托管代码和本机代码之间切换时转换类型的过程。
需要封送的原因是托管代码与非托管代码中的类型并不相同。 例如,在托管代码中,有一个 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 |
char 或 char16_t ,具体取决于 P/Invoke 或结构的编码。 请参阅字符集文档。 |
System.Char |
char* 或 char16_t* ,具体取决于 P/Invoke 或结构的编码。 请参阅字符集文档。 |
|
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] 属性 |
LayoutKind 为 Sequential 或 Explicit 的类 |
指向类的本机表示形式的指针 | 类的本机表示形式 |
下表包含仅适用于 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 。 请参阅字符集文档。 |
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[] |
BSTR 的 SAFEARRAY |
封送类和结构
有关类型封送的另一个问题是如何将结构传入非托管方法。 例如,某些非托管方法需要使用结构作为参数。 在这种情况下,需要在环境的托管部分中创建相应的结构或类,以便将它用作参数。 不过,仅仅是定义类并不足够,还需要告知封送处理程序如何将类中的字段映射到非托管结构。 在此处,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;
有时,默认结构封送不执行所需的操作。 自定义结构封送一文介绍如何自定义结构的封送方式。