值类型的默认封送处理
更新:2007 年 11 月
大多数值类型(如整数和浮点数)都是可直接复制到本机结构中的,且不需要进行封送处理。其他非直接复制到本机结构中的类型在托管和非托管内存中具有不同的表示形式,且需要进行封送处理。还有一些其他类型需要跨交互操作边界进行显式格式设置。
本主题提供下列有关格式化值类型的信息:
平台调用中使用的值类型
COM 互操作中使用的值类型
除描述格式化类型外,本主题还识别具有非常规封送处理行为的系统值类型。
格式化类型是一种复杂类型,它包含显式控制其成员在内存中的布局的信息。成员布局信息是使用 StructLayoutAttribute 属性提供的。布局可以是下列 LayoutKind 枚举值之一:
LayoutKind.Automatic
指示公共语言运行库可以随意重新排列该类型的成员的顺序以提高效率。但是,将值类型传递给非托管代码时,成员的布局是可预知的。视图自动封送这样的结构将导致异常。
LayoutKind.Sequential
指示该类型的成员要在非托管内存中以它们在托管类型定义中出现的同一顺序布局。
LayoutKind.Explicit
指示成员根据随每个字段提供的 FieldOffsetAttribute 进行布局。
平台调用中使用的值类型
在下面的示例中,Point 和 Rect 类型使用 StructLayoutAttribute 提供成员布局信息。
Imports System.Runtime.InteropServices
<StructLayout(LayoutKind.Sequential)> Public Structure Point
Public x As Integer
Public y As Integer
End Structure
<StructLayout(LayoutKind.Explicit)> Public Structure Rect
<FieldOffset(0)> Public left As Integer
<FieldOffset(4)> Public top As Integer
<FieldOffset(8)> Public right As Integer
<FieldOffset(12)> Public bottom As Integer
End Structure
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
public struct Point {
public int x;
public int y;
}
[StructLayout(LayoutKind.Explicit)]
public struct Rect {
[FieldOffset(0)] public int left;
[FieldOffset(4)] public int top;
[FieldOffset(8)] public int right;
[FieldOffset(12)] public int bottom;
}
当封送到非托管代码时,这些格式化类型作为 C 样式的结构封送。这就提供了一种调用具有结构参数的非托管 API 的简单方法。例如,POINT 和 RECT 结构可以通过下面的方式传递给 Microsoft Win32 API PtInRect 函数:
BOOL PtInRect(const RECT *lprc, POINT pt);
可以使用下面的平台调用定义传递结构:
Class Win32API
Declare Auto Function PtInRect Lib "User32.dll" _
(ByRef r As Rect, p As Point) As Boolean
End Class
class Win32API {
[DllImport("User32.dll")]
public static extern Bool PtInRect(ref Rect r, Point p);
}
Rect 值类型必须通过引用传递,原因是非托管 API 要求将指向 RECT 的指针传递给该函数。而因为非托管 API 要求在堆栈上传递 POINT,因此 Point 值类型通过值传递。这种细微的差异极其重要。引用是作为指针传递给非托管代码的。值在堆栈上传递给非托管代码。
说明: |
---|
当格式化的类型作为结构封送时,只有该类型内的字段是可访问的。如果该类型具有方法、属性或事件,将无法从非托管代码访问它们。 |
类还可以作为 C 样式的结构封送到非托管代码,条件是它们具有固定的成员布局。类的成员布局信息也是用 StructLayoutAttribute 属性提供的。具有固定布局的值类型与具有固定布局的类之间的主要差异在于将它们封送到非托管代码的方式。值类型通过值传递(在堆栈上),因此被调用方对类型的成员所做的任何更改对于调用方都是不可见的。引用类型通过引用传递(对类型的引用在堆栈上传递);因此,被调用方对类型的可直接复制到本机结构中的类型成员所做的所有更改对于调用方都是可见的。
说明: |
---|
如果引用类型具有非直接复制到本机结构中的类型的成员,则需要进行两次转换:第一次在将参数传递到非托管端时进行,而第二次在从调用返回时进行。由于这项附加的开销,如果调用方希望看见被调用方所做的更改,则必须将 In/Out 参数显式应用于参数。 |
在下面的示例中,SystemTime 类具有连续的成员布局,并且可以传递给 Win32 API GetSystemTime 函数。
<StructLayout(LayoutKind.Sequential)> Public Class SystemTime
Public wYear As System.UInt16
Public wMonth As System.UInt16
Public wDayOfWeek As System.UInt16
Public wDay As System.UInt16
Public wHour As System.UInt16
Public wMinute As System.UInt16
Public wSecond As System.UInt16
Public wMilliseconds As System.UInt16
End Class
[StructLayout(LayoutKind.Sequential)]
public class SystemTime {
public ushort wYear;
public ushort wMonth;
public ushort wDayOfWeek;
public ushort wDay;
public ushort wHour;
public ushort wMinute;
public ushort wSecond;
public ushort wMilliseconds;
}
GetSystemTime 函数以下面的方式定义:
void GetSystemTime(SYSTEMTIME* SystemTime);
GetSystemTime 的等效平台调用定义如下所示:
Public Class Win32
Declare Auto Sub GetSystemTime Lib "Kernel32.dll" (ByVal sysTime _
As SystemTime)
End Class
class Win32API {
[DllImport("Kernel32.dll", CharSet=CharSet.Auto)]
public static extern void GetSystemTime(SystemTime st);
}
请注意,SystemTime 参数未类型化为引用参数,原因是 SystemTime 是类而不是值类型。与值类型不同,类始终要通过引用来传递。
下面的代码示例显示一个不同的 Point 类,它具有称为 SetXY 的方法。由于该类型具有连续布局,因此可将它传递给非托管代码并作为结构封送。但是,即使通过引用传递对象,仍然不能从非托管代码调用 SetXY 成员。
<StructLayout(LayoutKind.Sequential)> Public Class Point
Private x, y As Integer
Public Sub SetXY(x As Integer, y As Integer)
Me.x = x
Me.y = y
End Sub
End Class
[StructLayout(LayoutKind.Sequential)]
public class Point {
int x, y;
public void SetXY(int x, int y){
this.x = x;
this.y = y;
}
}
COM 互操作中使用的值类型
还可以将格式化类型传递给 COM 互操作方法调用。事实上,当导出到类型库时,值类型被自动转换为结构。如下面的示例所示,Point 值类型变为名为 Point 的类型定义 (typedef)。类型库中其他地方的所有对 Point 值类型的引用都被替换为 Point typedef。
类型库表示形式
typedef struct tagPoint {
int x;
int y;
} Point;
interface _Graphics {
…
HRESULT SetPoint ([in] Point p)
HRESULT SetPointRef ([in,out] Point *p)
HRESULT GetPoint ([out,retval] Point *p)
}
在通过 COM 接口进行封送处理时,使用的规则与封送值和对平台调用的调用的引用时使用的规则相同。例如,将 Point 值类型的实例从 .NET Framework 传递给 COM 时,Point 通过值传递。如果 Point 值类型通过引用传递,则指向 Point 的指针在堆栈上传递。Interop 封送拆收器不支持任何一个方向上的更高级别的间接寻址 (Point **)。
说明: |
---|
将 LayoutKind 枚举值设置为 Explicit 的结构不能在 COM 互操作中使用,原因是导出的类型库不能表示显式布局。 |
系统值类型
System 命名空间具有若干个表示已装箱形式的运行库基元类型的值类型。例如,值类型 System.Int32 结构表示 ELEMENT_TYPE_I4 的已装箱形式。将这些类型以与封送它们所装箱的基元类型相同的方式封送,而不是像其他格式化类型那样作为结构封送。因此,System.Int32 被封送为 ELEMENT_TYPE_I4 而不是包含一个 long 类型的成员的结构。下表包含 System 命名空间中的值类型(它们是基元类型的装箱表示形式)的列表。
系统值类型 |
元素类型 |
---|---|
ELEMENT_TYPE_BOOLEAN |
|
ELEMENT_TYPE_I1 |
|
ELEMENT_TYPE_UI1 |
|
ELEMENT_TYPE_CHAR |
|
ELEMENT_TYPE_I2 |
|
ELEMENT_TYPE_U2 |
|
ELEMENT_TYPE_I4 |
|
ELEMENT_TYPE_U4 |
|
ELEMENT_TYPE_I8 |
|
ELEMENT_TYPE_U8 |
|
ELEMENT_TYPE_R4 |
|
ELEMENT_TYPE_R8 |
|
ELEMENT_TYPE_STRING |
|
ELEMENT_TYPE_I |
|
ELEMENT_TYPE_U |
System 命名空间中的其他一些值类型以不同的方式处理。由于非托管代码对这些类型有现成的格式,因此,封送拆收器具有特殊的用于封送它们的规则。下表列出 System 命名空间中的特殊值类型,以及将其封送到的非托管类型。
系统值类型 |
IDL 类型 |
---|---|
DATE |
|
DECIMAL |
|
GUID |
|
OLE_COLOR |
下面的代码显示 Stdole2 类型库中的非托管类型 DATE、GUID、DECIMAL 和 OLE_COLOR 的定义。
类型库表示形式
typedef double DATE;
typedef DWORD OLE_COLOR;
typedef struct tagDEC {
USHORT wReserved;
BYTE scale;
BYTE sign;
ULONG Hi32;
ULONGLONG Lo64;
} DECIMAL;
typedef struct tagGUID {
DWORD Data1;
WORD Data2;
WORD Data3;
BYTE Data4[ 8 ];
} GUID;
下面的代码显示托管 IValueTypes 接口中的相应定义。
Public Interface IValueTypes
Sub M1(d As System.DateTime)
Sub M2(d As System.Guid)
Sub M3(d As System.Decimal)
Sub M4(d As System.Drawing.Color)
End Interface
public interface IValueTypes {
void M1(System.DateTime d);
void M2(System.Guid d);
void M3(System.Decimal d);
void M4(System.Drawing.Color d);
}
类型库表示形式
[…]
interface IValueTypes : IDispatch {
HRESULT M1([in] DATE d);
HRESULT M2([in] GUID d);
HRESULT M3([in] DECIMAL d);
HRESULT M4([in] OLE_COLOR d);
};