zhouqijie

封送结构体中的字符串



结构体中的字符指针字段

typedef struct Employee
{
    UINT employeeID;
    short employedYear;
    char* name;
    char* alias;
}

void _cdecl GetEmployeeInfo(Employee* ptr)
{
    if(NULL != ptr)
    {
        ptr -> employedYear = 2;
        ptr -> alias = (char*)CoTaskMemAlloc(255);
        ptr -> name = (char*)CoTaskMemAlloc(255);

        strcpy_s(ptr->alias, 255, "xcui");
        strcpy_s(ptr->name, 255, "ZQJ");
    }
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
private struct Employee
{
    public uint employeeID;
    public short employedYear;
    public string name;
    public string alias;
}
//...
[DllImport(dllName, CallingConvertion = CallingConvertion.Cdecl, CharSet = CharSet.Ansi)]
private extern static void GetEmployeeInfo(ref Employee employee);

//...
static void Foo()
{
    Employee e1 = new Employee();
    e1.employeeID = 10001;
    GetEmployeeInfo(ref e1);
    //打印
    Console.WriteName("姓名:" + e1.name);
}

封送拆收器会将字符串结果赋值给托管结构体中的string类型字段,然后自动释放为字符串分配的非托管内存。
托管结构体中的namealias字段也可以定义为IntPtr类型,这样就需要手动调用Marshal.FreeCoTaskMem()来释放非托管内存。



结构体中的字符数组字段

在结构体中定义字符数组(inline)以存储字符串数据,也是C++中非常常见的形式。这结构体中的字符指针完全不同。

对于结构体中的字符指针:结构体只存放字符指针。
对于结构体中的字符数组:结构体包含全部字符数组所占的内存。

typedef struct Employee
{
    UINT id;
    short year;
    char name[255];
    char alias[255];
}

void _cdecl GetEmployeeInfo(Employee* ptr)
{
    ptr.year = 2;
    strcpy_s(ptr->alias, 255, "AAA");
    strcpy_s(ptr->name, 255, "ZQJ");
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
private struct Employee
{
    public uint id;
    public short year;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 255)]
    public string name;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 255)]
    public string alias;
}
[DllImport(dllName, CallingConvertion = CallingConvertion.Cdecl, CharSet = CharSet.Ansi)]
private extern static void GetEmplyeeInfo(ref Employee employee);
//...
static void Foo()
{
    Employee e1 = new Employee();
    e1.id = 10002;
    GetEmployeeInfo(ref e1);
    //打印
    Console.WriteLine(e1.name);
}

仍然食用string类型的字符串字段,但还需要MarshalAs进行修饰,以指明该字段的封送行为。由于字段指定了UnmanagedType.ByValTStr枚举值,因此在平台调用中,此字段将被封送成结构体内定长字符数组,元素个数则由SizeConst指定。

包含字符数组的结构在C/C++代码中很常见。此外,很多WindowsAPI也都是接收这种类型的结构体作为参数。





控制结构体字段的封送行为

有时候需要逐一指定结构体字段的封送方式,可以用MarshalAs特性为每一个字段指定平台调用的封送行为。

typedef struct EmployeeEX
{
    UINT id;
    wchar_t* name;
    char* alias;
    bool isInRedmond;
    Bool isFemale;
}

name字段定义为了Unicode字符指针。
两个新增的字段是不同的布尔类型。C++的bool是一字节,而Win32的BOOL是4字节。

[StructLayout(LayoutKind.Sequential)]
private struct EmployeeEX
{
    public uint id;

    [MarshalAs(UnmanagedType.LPWStr)]
    public string name;

    [MarshalAs(UnmanagedType.LPStr)]
    public string alias;

    [MarshalAs(UnmanagedType.I1)]
    public bool IsInRedmond;

    [MarshalAs(UnmanagedType.Bool)]
    public bool isFemale;
}

逐个字段使用MarshalAs特性的好处:

  1. 可读性增强,可以很直观地了解对应的非托管类型。
  2. 避免使用默认的封送方式,可以避免dotnet版本升级可能带来的问题。
枚举值 非托管格式的说明
UnmanagedType.BStr 具有预设长度并包含Unicode字符的COM样式的BSTR
UnmanagedType.LPStr 指向以null终止的ANSI字符数组的指针
UnmanagedType.LPTStr 指向以null终止的平台相关的字符数组的指针
UnmanagedType.LPWStr 指向以null终止的Unicode字符数组的指针
UnmanagedType.ByValTStr 定长的字符数组。数组的类型由包含数组的结构的字符集确定





控制结构体的内存布局

StructLayout的字段LayoutKind指定了对象被封送到非托管内存中的布局。前面的示例使用的都是都是Sequential,这种方式指示封送拆收器在将对象封送到非托管内存时,按照其定义的布局顺序(收到Pack的影响)。以下将介绍LayoutKind.Explicit的方式,这种方式的各个字段准确位置由FieldOffset特性指定,可以实现内存布局的精确控制。

[StructLayout(LayoutKind.Explicit)]
private struct EmployeeEXX
{
    [FieldOffset(0)] public uint id;
    [FieldOffset(4)] public ushort year;
    //...
}
  1. 如果非托管函数中涉及位结构体字段分配内存,那么在托管代码中定义结构体时,必须声明该字段。否则托管代码没有相应字段封送拆收器就无法完成资源封送和释放,导致内存泄漏。
  2. 最好指定StructLayout的Size字段,从而避免结构体大小带来的潜在问题。

联合体也可以使用LayoutKind.Explicit方式和FieldOffset特性进行封送。其中FieldOffset的值都指定为”0”。





封送嵌套的结构体

定义结构体时,不仅可以用简单类型,还可以用结构体或者指向结构体的指针作为字段,称为嵌套结构体。



结构体指针字段嵌套

typedef struct PersonName
{
    char* first;
    char* last;
    char* display;
}
typedef struct Person
{
    PersonName* pName;
    int age;
}

void _cdecl Test(Person* ptr)
{
    size_t firstLen = strlen(ptr->pName->first);
    size_t lastLen = strlen(ptr->pName->last);
    char* temp = (char*)CoTaskMemAlloc(sizeof(char) * (firstLen + lastLen + 2));
    //...
    CoTaskMemFree(ptr->pName->display);
    ptr->pName->display = temp;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct PersonName
{
    public string first;
    public string last;
    public string display;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct Person
{
    public IntPtr name;
    public int age;
}
//...
static void Foo()
{
    PersonName name = new PersonName();
    name.last = "Z";
    name.first = "QJ"

    Person person = new Person();
    person.age = 27;

    IntPtr nameBuffer = Marshal.AllocCoTaskMem(Marshal.Sizeof(name));
    Marshal.StructureToPtr(name, nameBuffer, false);
    person.name = nameBuffer;

    //调用Test
    Test(ref person);

    //新的PersonName值  
    PersonName newvalue = (PersonName)Marshal.PtrToStructure(person.name, typeof(PersonName));
    //释放非托管代码中分配的PersonName内存  
    Marshal.DestroyStructure(nameBuffer, typeof(PersonName));
    
    //打印
    Console.WriteLine("调用后DisplayName为:" + newvalue.display);
}

Marshal.StructureToPtr(Object structure, IntPtr ptr, bool fDelOld)方法用于将第一个参数的结构体数据复制到预先分配的非托管内存中(第二个二参数),第三个参数指示是否在复制操作前释放非托管内存中的资源。
平台调用过程中,Person结构体作为参数并且按照引用方式进行传递,封送拆收器会负责其内存的清理工作。但作为Person类型子结构体的PersonName就另当别论了。
DestroyStructure不仅能释放结构本身,还能释放器字段所指向的字符串内存资源。



结构体字段直接嵌套

如果一个结构体直接作为另一个结构体的字段,那么对这种类型的嵌套结构就需要采用不同的数据封送方式。

typedef struct Person
{
    PersonName name;//第一个字段不再是指针,而是其实例本身  
    int age;
}
[StructureLayout(LayoutKind.Sequential)]
public struct Person
{
    public PersonName name;
    public int age;
}
//...
[DllImport(dllName, CallConvertion = CallConvertion.Cdecl, CharSet = CharSet.Ansi)]
private extern static void Test(ref Person person);
///...
static void Foo()
{
    Person person = new Person();
    person.name.last = "Z";
    person.name.first = "QJ";
    person.name.display = "";
    person.age = 26;

    Test(ref person);
}

(END)