zhouqijie

结构体的封送



封送作为参数的结构体

非托管函数需要定义一个自己用到的非托管结构体。为了在托管代码中调用该非托管函数,同样需要在托管环境中定义一个等价的结构体,以供调用方作为函数参数使用。

最大的难题是:如何确保托管代码中的结构体和非托管代码中的结构体是等价的。

typedef struct vec3
{
    int x;
    int y;
    int z;
}
void _cdecl TestByValue(vec3 vec)
{
    //...
}
[StructLayout(LayoutKind.Sequential)]
private struct Vector3
{
    public int x;
    public int y;
    public int z;
}
[DllImport(dllName, CharSet = CharSet.Unicode, CallingConvertion = CallingConvertion.Cdecl)]
private extern static void TestByValue(Vector3 vec);

由于结构体是按值传递的,所以调用方并不能获得非托管函数对结构体的修改。

但是有时候需要用结构体作为参数返回期望的数据。可以传递指向结构体的指针来达到在函数内部修改结构体字段的目的。

void _cdecl TestByRef(vec3* pvec)
{
    ShowNativeStructSize(sizeof(vec3));
    //...
}
[DllImport(dllName, CharSet = CharSet.Unicode, CallingConvertion = CallingConvertion.Cdecl)]
private extern static void TestByValue(ref Vector3 vec);

StructLayout有三个非常重要的可设置字段,可以用来控制结构体和类的封送处理行为。

  1. CharSet:指定结构体或类中字符串字段是按照LPWSTR还是LPSTR封送。
  2. Pack:控制内存中的对齐方式。
  3. Size:指定结构体或类在非托管内存中的绝对大小。

使用StructLayout特性时,需要为其构造函数指定一个枚举参数LayoutKind,它包含3个值。

  1. Sequential(默认):各个字段按照它们被定义的顺序在内存中布局。该布局同时会受Pack的影响,可能不连续。
  2. Explicit:该选项可以精确控制结构体各个字段在非托管内存中的精确位置,每个字段必须用FieldOffset特性指定偏移位置。
  3. Auto:CLR会自动为指定了该值的结构体选择合适的内存布局。在平台调用中使用定义为该枚举的类型,会导致数据封送发生异常。

内存对齐是指结构体、类、联合体中的字段总是要与特定的内存边界对齐。而边界值来源于Pack值的倍数和字段大小的倍数二者中较小的值。

C++中也可以用#pragma pack(n)对编译器作出指示以调整结构体内存布局。

在托管代码中如果没有显式指定Pack值,则其默认值为8,与VisualC++中的默认值一致。

示例:(默认Pack值为8)

private struct SimpleStruct
{
    public int intValue;        // -> offset 0 = min(0x8, 0x4)
    public short shortValue;    // -> offset 4 = min(1x8, 2x2)
    public float floatValue;    // -> offset 8 = min(1x8, 2x4)
    public double doubleValue;  // -> offset 16 = min(2x8, 2x8)
}
int (4byte) short (2byte) ----NULL----
float(4byte) ----NULL---- ----NULL----
double(8byte)



封送从函数体内部返回的结构体

在C++中可以通过两种方式返回内部创建的结构体:

  1. 通过返回值:返回指向结构体的指针。
  2. 通过参数:指向结构体指针的指针。

以上两种方式都会涉及IntPtr类型。

由于CLR始终使用CoTaskMemFree方法释放非托管内存,因此除非非托管内存是由CoTaskMemAlloc方法分配的,否则在托管代码中无法直接释放掉非托管内存。所以还需要定义另一个非托管函数来提供释放内存的途径。

SimpleStruct* _cdecl NewStruct(void)
{
    SimpleStruct* ptr = new SimpleStruct();
    return ptr;
}  
void _cdecl DeleteStruct(SimpleStruct* ptr)
{
    if(NULL != ptr){delete ptr; ptr = NULL;}
}
[DllImport(dllName, CallingConvertion = CallingConvertion.Cdecl)]
private extern static IntPtr NewStruct();
[DllImport(dllName, CallingConvertion = CallingConvertion.Cdecl)]
private extern static void DeleteStruct(IntPtr pStruct);

static void Foo()
{
    IntPtr pStruct = NewStruct();
    ManagedSimpleStruct retStruct = (ManagedSimpleStruct)Marshal.PtrToStructure(pStruct, typeof(ManagedSimpleStruct));

    //释放内存  
    DeleteStruct(pStruct);
}

如果是用CoTaskMemAlloc(sizeof(SimpleStruct))而不是new来分配的内存,就要改用Marshal.FreeCoTaskMem()方法直接在托管代码中释放内存。

void _cdecl ReturnStructFromArg(SimpleStruct** ppStruct)
{
    //...
    *ppStruct = (SimpleStruct*)CoTaskMemAlloc(sizeof(SimpleStruct));
    //...
}
[DllImport(dllName, CallingComvertion = CallingComvertion.Cdecl)]
private extern static void  ReturnStructFromArg(ref IntPtr pStruct);

static void Foo()
{
    IntPtr ppStruct;
    ReturnStructFromArg(ref ppStruct);

    ManagedSimpleStruct struct1 = (ManagedSimpleStruct)Marshal.PtrToStructure(ppStruct, typeof(ManagedSimpleStruct));

    Marshal.FreeCoTaskMem(ppStruct);
}

(END)