默认封送处理行为

互操作封送处理根据规则进行操作,该规则指定与方法参数相关联的数据在托管和非托管内存之间传递时的行为方式。 这些内置规则控制封送活动,例如数据类型转换、被调用方是否可以更改传递给它的数据并将更改返回给调用方,以及在何种情况下封送器能够提供性能优化。

本部分确定互操作封送处理服务的默认行为特征。 其中详细介绍了封送数组、布尔类型、字符类型、委托、类、对象、字符串和结构。

注释

不支持泛型类型的封送处理。 有关详细信息,请参阅使用 泛型类型进行互作

使用互操作封送处理程序进行内存管理

互操作封送处理程序始终尝试释放由非托管代码分配的内存。 此行为符合 COM 内存管理规则,但不同于管理本机C++的规则。

使用为指针自动释放内存的平台调用时,如果你预期有本机 C++ 行为(无内存释放),则可能产生混淆。 例如,从 C++ DLL 调用以下非托管方法不会自动释放任何内存。

非托管的签名

BSTR MethodOne (BSTR b) {  
     return b;  
}  

但是,如果将该方法定义为平台调用原型,请将每个 BSTR 类型替换为一个 String 类型并调用 MethodOne,则公共语言运行时将尝试释放 b 两次。 可使用 IntPtr 类型而不是字符串类型来更改封送处理行为

运行时始终使用 Windows 上的 CoTaskMemFree 方法和其他平台上 的免费 方法来释放内存。 如果您正在使用的内存不是在 Windows 上通过 CoTaskMemAlloc 方法或在其他平台上使用 malloc 方法分配的,那么您必须使用 IntPtr 并使用相应的方法手动释放内存。 同样,在某些内存不应被释放的情况下,你可以避免自动释放内存,例如从 Kernel32.dll使用 GetCommandLine 函数时,该函数返回一个指向内核内存的指针。 有关手动释放内存的详细信息,请参阅 缓冲区示例

类的默认封送处理

类仅能由 COM 互操作封送,并总是作为接口封送。 在某些情况下用来将该类封送的接口称为类接口。 有关使用所选接口替代类接口的信息,请参阅 类接口简介

向 COM 传递类

将托管类传递给 COM 时,互作封送器会自动将类包装为 COM 代理,并将代理生成的类接口传递给 COM 方法调用。 然后,代理将类接口上的所有调用委托回托管对象。 该代理还公开了未由类显式实现的其他接口。 代理代表类自动实现接口,如 IUnknown 和 IDispatch

向 .NET 代码传递类

Coclass 通常不用作 COM 中的方法参数。 相反,通常传递的是默认接口,而不是 coclass。

当接口传递到托管代码中时,互操作封送处理程序负责用适当的包装来包装接口,并将该包装传递给托管方法。 确定要使用的包装器可能很困难。 无论对象实现的接口数如何,COM 对象的每个实例都有一个唯一包装器。 例如,实现五个不同的接口的单个 COM 对象只有一个包装器。 同一包装器公开所有五个接口。 如果创建了 COM 对象的两个实例,则会创建包装器的两个实例。

要使包装器在其整个生存期内保持相同的类型,互操作封送器必须在对象公开的接口首次由封送器传递时标识出正确的包装器。 编组器通过查看对象实现的一个接口来标识对象。

例如,封送处理程序确定应使用类包装来包装已传递到托管代码的接口。 当接口首次通过封送器传递时,封送器将检查接口是否来自已知对象。 此检查在两种情况下发生:

  • 一个接口正在由另一个传递到 COM 其他位置的托管对象实现。 封送器可以轻松识别托管对象公开的接口,并且能够将接口与提供实现的托管对象匹配。 然后,托管对象将传递给该方法,无需包装器。

  • 已包装的对象正在实现接口。 为了确定是否是这种情况,封送器会查询对象的 IUnknown 接口,并将返回的接口与已包装的其他对象的接口进行比较。 如果接口与另一个包装器相同,则对象具有相同的标识,并且现有包装器将传递给该方法。

如果接口不是来自已知对象,封送器将执行以下作:

  1. 封送器查询 IProvideClassInfo2 接口的对象。 如果提供,封送器将使用从 IProvideClassInfo2.GetGUID 返回的 CLSID 来标识提供接口的 coclass。 使用 CLSID 时,如果程序集以前已注册,封送器可以从注册表中找到包装器。

  2. 封送处理程序向接口查询 IProvideClassInfo 接口。 如果提供,封送器将使用从 IProvideClassInfo.GetClassinfo 返回的 ITypeInfo 来确定公开接口的类的 CLSID。 封送器可以使用 CLSID 查找包装器的元数据。

  3. 如果封送器仍无法标识该类,它将使用名为 System.__ComObject 的泛型包装类包装接口。

委托的默认封送处理

托管委托基于以下调用机制封送为 COM 接口或函数指针:

  • 对于平台调用,默认情况下,委托作为非托管函数指针进行封送。

  • 对于 COM 互操作,默认情况下,委托作为 _Delegate 类型的 COM 接口进行封送_Delegate接口在 Mscorlib.tlb 类型库中定义,并包含Delegate.DynamicInvoke该方法,这使你可以调用委托引用的方法。

下表显示托管委托数据类型的封送处理选项。 MarshalAsAttribute 属性提供若干 UnmanagedType 枚举值来封送委托。

枚举类型 非托管格式说明
UnmanagedType.FunctionPtr 非托管函数指针。
UnmanagedType.Interface mscorlib.tlb 中定义的 _Delegate类型的接口。

请考虑以下示例代码, DelegateTestInterface 其中的方法导出到 COM 类型库。 请注意,仅使用 ref(或 ByRef)关键字标记的委托会被作为输入/输出参数传递。

using System;  
using System.Runtime.InteropServices;  
  
public interface DelegateTest {  
void m1(Delegate d);  
void m2([MarshalAs(UnmanagedType.Interface)] Delegate d);
void m3([MarshalAs(UnmanagedType.Interface)] ref Delegate d);
void m4([MarshalAs(UnmanagedType.FunctionPtr)] Delegate d);
void m5([MarshalAs(UnmanagedType.FunctionPtr)] ref Delegate d);
}  

类型库表示形式

importlib("mscorlib.tlb");  
interface DelegateTest : IDispatch {  
[id(…)] HRESULT m1([in] _Delegate* d);  
[id(…)] HRESULT m2([in] _Delegate* d);  
[id(…)] HRESULT m3([in, out] _Delegate** d);  
[id()] HRESULT m4([in] int d);  
[id()] HRESULT m5([in, out] int *d);  
   };  

函数指针可以被取消引用,就像任何其他非托管函数指针也可以被取消引用一样。

在此示例中,当两个委托作为 UnmanagedType.FunctionPtr 封送时,得到的结果是一个 int 和一个指向 int 的指针。 由于要封送委托类型,此处 int 表示指向 void (void*) 的指针,这是该委托在内存中的地址。 换句话说,此结果特定于 32 位 Windows 系统,因为 int 此处表示函数指针的大小。

注释

对指向由非托管代码持有的托管委托的函数指针的引用不会阻止公共语言运行时对托管对象执行垃圾收集。

例如,以下代码是不正确的,因为传递给cb方法的SetChangeHandler对象的引用在cb方法的生命周期之外无法保持Test的活跃状态。 一旦cb对象被垃圾回收,传递给SetChangeHandler的函数指针将不再有效。

public class ExternalAPI {  
   [DllImport("External.dll")]  
   public static extern void SetChangeHandler(  
      [MarshalAs(UnmanagedType.FunctionPtr)]ChangeDelegate d);  
}  
public delegate bool ChangeDelegate([MarshalAs(UnmanagedType.LPWStr) string S);  
public class CallBackClass {  
   public bool OnChange(string S){ return true;}  
}  
internal class DelegateTest {  
   public static void Test() {  
      CallBackClass cb = new CallBackClass();  
      // Caution: The following reference on the cb object does not keep the
      // object from being garbage collected after the Main method
      // executes.  
      ExternalAPI.SetChangeHandler(new ChangeDelegate(cb.OnChange));
   }  
}  

若要补偿意外的垃圾回收,只要非托管的函数指针在使用中时,调用方必须确保 cb 对象保持活动状态。 (可选)当不再需要函数指针时,可以让非托管代码通知托管代码,如以下示例所示。

internal class DelegateTest {  
   CallBackClass cb;  
   // Called before ever using the callback function.  
   public static void SetChangeHandler() {  
      cb = new CallBackClass();  
      ExternalAPI.SetChangeHandler(new ChangeDelegate(cb.OnChange));  
   }  
   // Called after using the callback function for the last time.  
   public static void RemoveChangeHandler() {  
      // The cb object can be collected now. The unmanaged code is
      // finished with the callback function.  
      cb = null;  
   }  
}  

值类型的默认封送处理

大多数值类型,例如整数和浮点数,都是blittable的,不需要进行封送处理。 其他非 blittable 类型在托管和非托管内存中具有不同的表示形式,且需要封送处理。 其他类型还需要在跨越互操作边界时进行显式格式化。

本部分提供有关以下格式化值类型的信息:

除了描述格式化类型之外,本主题还标识具有异常封送行为的 系统值类型

格式化类型是一种复杂类型,其中包含显式控制其成员在内存中的布局的信息。 成员布局信息是使用该 StructLayoutAttribute 属性提供的。 布局可以是以下 LayoutKind 枚举值之一:

  • LayoutKind.Auto

    指示公共语言运行时可以自由地对类型的成员进行重新排序,以便提高效率。 但是,将值类型传递给非托管代码时,成员的布局是可预测的。 尝试将这种结构进行自动封送处理会导致异常。

  • LayoutKind.Sequential

    指示类型的成员将在非托管内存中按照托管类型定义的顺序排列。

  • LayoutKind.Explicit

    指示根据随每个字段提供的 FieldOffsetAttribute 对成员进行布局。

平台调用中使用的值类型

在以下示例中, PointRect 类型使用 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 Windows API PtInRect 函数,如下所示:

BOOL PtInRect(const RECT *lprc, POINT pt);  

可以使用下面的平台调用定义传递结构:

Friend Class NativeMethods
    Friend Declare Auto Function PtInRect Lib "User32.dll" (
        ByRef r As Rect, p As Point) As Boolean
End Class
internal static class NativeMethods
{
   [DllImport("User32.dll")]
   internal static extern bool PtInRect(ref Rect r, Point p);
}

Rect 类型必须通过引用传递,因为非托管 API 期望一个指向 RECT 的指针被传递给函数。 Point 值类型按值传递,因为非托管 API 要求 POINT 通过堆栈传递。 这种微妙的差异非常重要。 引用作为指针传递到非托管代码。 值将传递到堆栈上的非托管代码。

注释

当格式化类型作为结构进行封送时,只能访问该类型中的字段。 如果类型具有方法、属性或事件,则无法从非托管代码访问它们。

类也可以作为 C 样式结构封送到非托管代码,前提是它们具有固定成员布局。 类的成员布局信息也通过StructLayoutAttribute属性提供。 具有固定布局的值类型与具有固定布局的类之间的主要区别在于它们被封送到非托管代码的方式。 值类型以值的方式(在堆栈上)传递,因此调用方无法看到被调用方对类型成员所做的任何更改。 引用类型通过引用传递(对类型的引用在堆栈上传递);因此,被调用方对类型中可变类型成员所做的所有更改都会被调用者看到。

注释

如果引用类型具有非直接复制到本机结构中的类型成员,则需要进行两次转换:第一次是当参数传递到非托管端时,第二次是从调用返回时。 由于此增加的开销,如果调用方想要查看被调用方所做的更改,必须将输入/输出参数显式应用到某个参数。

在以下示例中,该 SystemTime 类具有顺序成员布局,可以传递给 Windows 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 的等效平台调用定义如下所示:

Friend Class NativeMethods
    Friend Declare Auto Sub GetSystemTime Lib "Kernel32.dll" (
        ByVal sysTime As SystemTime)
End Class
internal static class NativeMethods
{
   [DllImport("Kernel32.dll", CharSet = CharSet.Auto)]
   internal 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的指针会在堆栈上传递。 互操作封送拆收器不支持任一方向更高级别的间接寻址 (Point **)。

注释

LayoutKind将枚举值设置为 Explicit 的结构不能用于 COM 互作,因为导出的类型库无法表达显式布局。

系统值类型

命名空间 System 有多个值类型,这些类型表示运行时基元类型的装箱形式。 例如,值类型System.Int32结构表示ELEMENT_TYPE_I4的封箱形式。 不像其他格式化类型将这些类型作为结构进行封送处理,而是以它们装箱的基元类型的相同方式将它们封送处理。 因此,System.Int32 作为 ELEMENT_TYPE_I4 封送处理,而不是作为包含类型的单个成员的结构封送处理。 下表包含 系统 命名空间中值类型的列表,这些类型是基元类型的装箱表示形式。

系统值类型 元素类型
System.Boolean ELEMENT_TYPE_BOOLEAN
System.SByte ELEMENT_TYPE_I1
System.Byte ELEMENT_TYPE_UI1
System.Char ELEMENT_TYPE_CHAR
System.Int16 ELEMENT_TYPE_I2
System.UInt16 ELEMENT_TYPE_U2
System.Int32 ELEMENT_TYPE_I4
System.UInt32 ELEMENT_TYPE_U4
System.Int64 ELEMENT_TYPE_I8
System.UInt64 ELEMENT_TYPE_U8
System.Single ELEMENT_TYPE_R4
System.Double ELEMENT_TYPE_R8
System.String ELEMENT_TYPE_STRING
System.IntPtr ELEMENT_TYPE_I
System.UIntPtr ELEMENT_TYPE_U

系统命名空间中的一些其他值类型以不同的方式进行处理。 由于非托管代码已具备这些类型的完善格式,因此封送处理程序具有用于将其封送的特殊规则。 下表列出系统命名空间中的特殊值类型,以及其封送到的非托管类型

系统值类型 IDL 类型
System.DateTime DATE
System.Decimal 十进制
System.Guid GUID (英文)
System.Drawing.Color OLE_COLOR

以下代码显示了 Stdole2 类型库中非托管类型 DATEGUIDDECIMALOLE_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);  
};  

另请参阅