C# interopservice之默认封组行为

参见default marshaling behaviour

interop封组器(marshaler)的内存管理

interop封组器总是会试图去释放由非托管代码分配的内存。这个行为和COM内存管理一致,但是和原生C++内存管理规则不同。

从C++中调用下面的函数不会自动释放任何内存

BSTR MethodOne (BSTR b) {
    return b;
}

注:BSTR(Basic string or binary string)是一种用于COM,Automation和Interop函数的字符串数据类型。它是一个复合数据类型,包含长度前缀,字符串和结束符。


然而,假如定义一个该函数的平台调用原型,使用String类型替换BSTR,然后调用该函数,公共语言运行库试图两次释放b的内存(第一次是当参数被传递到非托管端,第二次是从调用返回的时候)。可以通过使用IntPtr类型替代String类型来改变这种封组行为。

运行库总是使用CoTaskMemFree方法去释放内存。如果使用中的内存不是使用CoTaskMemAlloc方法分配的,就必须使用一个IntPtr来指向该内存(就不会被自动释放掉)然后再使用一个合适的方法将其手动释放掉。可以用这种方法在内存不能被释放的场合避免自动释放。

例如下面例子

C++

LPTSTR WINAPI GetCommandLine(void);

C#

[DllImport("Kernel32.dll", CharSet=CharSet.Auto)]
publicstaticextern IntPtr GetCommandLine();


[DllImport("Kernel32.dll", CharSet=CharSet.Auto)]
publicstaticextern string GetCommandLineFreeAuto();


==>C#调用

IntPtr cmdLineStr = LibWrap.GetCommandLine();
string commandLine = Marshal.PtrToStringAuto(cmdLineStr);


这里GetCommandLine返回的一段系统核心内存的数据,所以必须保证该内存段不能被自动释放,因而需要将返回类型写成IntPtr,然后再从该IntPtr指向的内存段拷贝数据。而假如使用GetCommandLineFreeAuto的话,interop封组器会去释放该string对象所表示的那块内存区域,会导致核心内存资源被释放从而导致系统问题。


方向属性

每个函数的参数都可以和InAttribute属性、OutAttribute属性或者两者关联起来。在设计函数原型的时候通过应用方向属性,可以在托管和非托管内存之间改变运行时的数据封组。


方向属性是可选的,如果想要改变封组器的默认行为的话可以将它们应用到函数参数。如果不使用方向属性,封组器通过参数的类型(值类型或引用类型)和它的修改器(如果有的话)来决定方向流向。


Visual Basic 2005

C#

IDL attribute

ByVal

No equivalent.

[in]

ByRef

ref

[in/out]

No equivalent.

out

[out]


ByRef,ref和out参数修改器会使函数的参数以引用的方式而非值的方式进行封组。函数的参数以值类型传递的会被封组成值类型传递到非托管代码的栈上,参数以引用传递的话会被封组成栈上的指针。下图显示了值类型和引用类型在修改器作用的时候的默认封组行为

C# interopservice之默认封组行为_第1张图片

默认情况下,出于性能考量引用类型(有类、数组、字符串和接口,而结构体是值类型的)以值的形式的传递的话会被封组为传入参数(in parameter)。除非对函数参数使用InAttribute和OutAttribute(或仅仅和OutAttribute),否则你看不到对这些类型的改变(PS:这应该指的是封组过程中参数类型,例如加上ref修改器,参数类型就变成了指向指针的指针)。但是StringBuilder类是个例外,它作为In/Out参数进行封组。

interop封组器对于方向属性保证下列行为:

  • interop封组器不会对传入参数执行写操作

  • 当一个被拷贝的对象包含了一个被分配内存的对象,例如BSTR,封组器会根据in/out设置执行正确顺序的分配和销毁


在代码中准确的应用方向属性很重要。在托管代码中适当的使用InAttribute和OutAttribute可以保证Type Liberay Exporter(TIbexp.exe)使用这些字节来在对应的类型库中设置in/out字节,这对于可以pin住的引用类型来说尤其重要,例如一些数组和类。



Blittable和Non-Blittable类型

大多数数据类型在托管和非托管内存中都有一致的表示,不需要由封组器进行特别的处理,这些类型被称为Blittable类型,它们在托管和非托管代码之间传递的时候不需要转换

从平台调用函数返回的结构体必须是blittable类型的,平台调用不支持以non-blittbale类型结构体作为返回类型。

下面的System命名空间里的类型是blittbale类型

System.Byte

System.SByte

System.Int16

System.UInt16

System.Int32

System.UInt32

System.Int64

System.UInt64

System.IntPtr

System.UIntPtr

System.Single

System.Double

下面的复合类型也是blittable类型

  • blittable类型的一维数组,但是包含一个blittable类型的变长数组的类型不是blittable类型

  • 格式化的值类型,该类型仅仅包括blittable类型(对应的类或者结构体)


对象引用不是blittable类型。这个包括本身为blittable的对象的引用数组,例如可以定义一个blittable类型的结构体,但是不能定义一个包含这些结构体的引用数组的类型为blittable类型。


作为一个优化措施,blittable类型的数组和仅仅包括blittable类型数据的类在封组的过程中被钉住(pinned)而不会被拷贝。这些类型以传入参数进行封组,假如要以in/out参数进行封组的话必须使用InAttribute和OutAttribute。


一些托管数据类型在非托管环境中需要不同的表示形式,这些类型被称为是non-blittable类型,这些non-blittable数据类型需要被转换成可以被封组的形式。例如,托管的字符串是non-blittable形式的,在它们可以被封组之前必须转换成string对象。


下面是System命名空间下的non-blittable类型。代理,作为用于引用静态方法或类实例的数据结构,也是non-blittable的


Non-blittable type

Description(对应到非托管端的数据类型)

System.Array

转换成C风格数组或者SAFEARRAY

System.Boolean

转换成长度为1、2或4字节,真值取1或-1

System.Char

Converts to a Unicode or ANSI character.

System.Class

Converts to a class interface.

System.Object

Converts to a variant or an interface.

System.Mdarray

Converts to a C-style array or a SAFEARRAY.

System.String

Converts to a string terminating in a null reference or to a BSTR.

System.Valuetype

Converts to a structure with a fixed memory layout.

System.Szarray

Converts to a C-style array or a SAFEARRAY.


也就是说,要将上面的类型传递给非托管代码的话,是需要进行转换的


Copying和Pinning

在封组数据的时候,interop封组器可以拷贝或者钉住被封组的数据。拷贝数据就是将数据从一个内存位置拷贝到另一个内存位置。

值类型参数以值和引用形式传递

C# interopservice之默认封组行为_第2张图片

以值形式传递的函数参数被以值的形式封组到非托管代码的栈上。拷贝的过程是直接的。引用类型的参数以栈上的指针形式传递。


引用的类型以值和引用形式传递

C# interopservice之默认封组行为_第3张图片

引用类型以值的形式传递,是将该类型的指针在栈上传递。引用类型以引用形式传递,是将该类型指针的指针在栈上传递。

Pining是临时性的将数据锁在当前的内存区域,防止它被公共语言运行库的垃圾回收器重新分配。封组器钉住数据可以减少数据拷贝的消耗因而提高性能。数据的类型决定了封组过程中是否被拷贝还是被钉住。string类型的对象在封组过程中被钉住,可以使用GCHandle类手动的钉住内存(参见例子GCHandle Sample)。


格式化的Blittable类

格式化的Blittable类在托管和非托管内存中有固定的内存分别和一致的数据表示。当这些类型进行封组的时候,处于堆上的对象的指针被直接传递给被调用者(即非托管端函数)。被调用者可以修改由指针指向的内存区域的内容。

注:如果参数被标记成out或者in/out的话被调用端就可以修改参数指针指向的内存。相反,如果参数被封组成传入参数(也就是在没有指定方向参数的时候默认情形)被调用端应该避免修改参数指向内容。修改传入对象的内容在相同类被导出到类库并且用于cross-apartment的调用中的时候会产生问题(也就是说,在一般情况下,不指定out方向属性的时候也可以修改参数的内容,但是应该避免这种做法,因为在某些cross-apartment场合还是会出问题的)

对应的一个示例:

C++

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

VOID GetSystemTime(LPSYSTEMTIME lpSystemTime);


C#

[StructLayout(LayoutKind.Sequential)]

   public class SystemTime

   {

       public ushort year;

       public ushort month;

       public ushort weekday;

       public ushort day;

       public ushort hour;

       public ushort minute;

       public ushort second;

       public ushort millisecond;

   }


   public class LibWrap

   {

       [DllImport("Kernel32.dll")]

       public static extern void GetSystemTime([In, Out] SystemTime st);

       //public static extern void GetSystemTime(SystemTime st);

   }

调用代码

SystemTime st =new SystemTime();
LibWrap.GetSystemTime(st);

在上面的LibWrap中,红色行的代码没有指定out方向属性,但是使用它也可以正确调用成功




格式化的non-blittable类

格式化的non-blittable类有固定的内存分布,但是在托管内存和非托管内存中有不同的数据表示。在下面的情形中数据需要进行转换

  • 如果一个non-blittable类以值的形式进行封组,被调用端接收一个该类型数据结构副本的一个指针

  • 如果一个non-blittable类以引用的形式进行封组,被调用端接收指向该类型数据结构副本指针的指针

  • If theInAttribute attribute is set, this copy is always initialized with the instance's state, marshaling as necessary.

  • If theOutAttribute attribute is set, the state is always copied back to the instance on return, marshaling as necessary.

  • If bothInAttribute and OutAttribute are set, both copies are required. If either attribute is omitted, the marshaler can optimize by eliminating either copy.


看一个示例

C++

typedef struct _OSVERSIONINFO
{
 DWORD dwOSVersionInfoSize;
 DWORD dwMajorVersion;
 DWORD dwMinorVersion;
 DWORD dwBuildNumber;
 DWORD dwPlatformId;
 TCHAR szCSDVersion[ 128 ];
} OSVERSIONINFO;

BOOL GetVersionEx(LPOSVERSIONINFO lpVersionInfo);

C#
[StructLayout(LayoutKind.Sequential)]
public class OSVersionInfo
{
    public int OSVersionInfoSize;
    public int MajorVersion;
    public int MinorVersion;
    public int BuildNumber;
    public int PlatformId;

    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)]
    public String CSDVersion;
}

[StructLayout(LayoutKind.Sequential)]
public struct OSVersionInfo2
{
    public int OSVersionInfoSize;
    public int MajorVersion;
    public int MinorVersion;
    public int BuildNumber;
    public int PlatformId;

    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)]
    public String CSDVersion;
}

public class LibWrap
{
    [DllImport("kernel32")]
    public static extern bool GetVersionEx([In, Out] OSVersionInfo osvi);

    [DllImport("kernel32", EntryPoint="GetVersionEx")]
    public static extern bool GetVersionEx2(ref OSVersionInfo2 osvi);
}
调用
OSVersionInfo osvi = new OSVersionInfo();
osvi.OSVersionInfoSize = Marshal.SizeOf(osvi);
LibWrap.GetVersionEx(osvi);

OSVersionInfo2 osvi2 = new OSVersionInfo2();
osvi2.OSVersionInfoSize = Marshal.SizeOf(osvi2);
LibWrap.GetVersionEx2(ref osvi2);



这里定义了两个版本的托管原型,分别使用类和结构体作为参数,两个版本调用后得到的结果是一致的。由于这里类和结构体都使用non-blittable类型的String类型,所以它们也是non-blittable的。


你可能感兴趣的:(C# interopservice之默认封组行为)