其实从理论上讲结构体的和一般的基本值数据类型的封送没有太大的区别,因为都是栈上内存块的处理(当然如果结构体内有引用类型的成员也需要处理堆上的内存块)。
Example:(最基本的结构体封送)
C++ Code:
struct Person
{
public:
LPCSTR name;
int age;
};
_declspec(dllexport) void _stdcall PrintPerson(Person person)
{
setlocale(LC_ALL,"chs");
wprintf(L"Person Name:%s, Age:%d",person.name,person.age);
}
{
public:
LPCSTR name;
int age;
};
_declspec(dllexport) void _stdcall PrintPerson(Person person)
{
setlocale(LC_ALL,"chs");
wprintf(L"Person Name:%s, Age:%d",person.name,person.age);
}
C#包装类:
public struct Person
{
public string Name;
public int Age;
}
[DllImport("native.dll", EntryPoint = "PrintPerson", CharSet = CharSet.Ansi)]
public static extern void PrintPerson(Person person);
{
public string Name;
public int Age;
}
[DllImport("native.dll", EntryPoint = "PrintPerson", CharSet = CharSet.Ansi)]
public static extern void PrintPerson(Person person);
C#调用代码:
NativeWrapper.PrintPerson(new Person() { Name = "汪熊", _age = 27 });
看上去和封送一个int类型差不多,只是需要定义一个结构体。其实和封送int类型还是有区别的,因为struct毕竟是一个复合类型。在这里我们看上去和封送一样是
因为编绎器为我们做了很多默认配置的工作。这些工作可以通过StructLayout这个Attribute来定制。
根据MSDN:
StructLayout是用来设定结构体的内存物理布局的,有以下几种类型的物理布局:
- Sequential(C#和C++编译器默认为使用该类型的布局)
设定结构体的物理内存布局为顺序的,成员按定义的顺序依次布局。每个成员所用的内存空间根据另一个参数Pack的值来确定。
- Explicit
设定结构体的物理内存布局为显示指定。通过为字段设定FieldOffset值来控制每个字段的物理位置。当然,每个字段所占用的内存空间也是根据Pack的值来确定。
- Auto
CLR在运行时会自动选择一个合适的内存布局。
让我们来尝试打破一下默认的设定,加入一些自己的内存布局控制。
C++ 代码:
#pragma pack(push)
#pragma pack(8)
struct Person
{
public:
LPCSTR name;
int age;
double weight;
};
#pragma pack(pop)
#pragma pack(8)
struct Person
{
public:
LPCSTR name;
int age;
double weight;
};
#pragma pack(pop)
_declspec(dllexport) void _stdcall PrintPerson(Person person)
{
setlocale(LC_ALL,"chs");
wprintf(L"Person Name:%s, Age:%d, Weight:%f",person.name,person.age,person.weight);
}
{
setlocale(LC_ALL,"chs");
wprintf(L"Person Name:%s, Age:%d, Weight:%f",person.name,person.age,person.weight);
}
C#包装类:
[StructLayout(LayoutKind.Explicit,Pack = 8,CharSet=CharSet.Ansi)]
public struct Person
{
[FieldOffset(0)]
public string Name;
[FieldOffset(4)]
public int Age;
[FieldOffset(8)]
public double Weight;
}
public struct Person
{
[FieldOffset(0)]
public string Name;
[FieldOffset(4)]
public int Age;
[FieldOffset(8)]
public double Weight;
}
[DllImport("native.dll", EntryPoint = "PrintPerson", CharSet = CharSet.Ansi)]
public static extern void PrintPerson(Person person);
public static extern void PrintPerson(Person person);
C#调用代码:
NativeWrapper.PrintPerson(new Person() { Name = "汪熊", Age = 27, Weight = 172.5 });
显示的控制结构体的布局主要是通过Pack(内存对齐)和控制每个字段相对于结构体的起始位置的偏移(FieldOffset)来实现。
这里需要补习一下内存对齐的知识,做C++的可能比较了解,但是C#程序员可能会了解得比较少。
说明:当采用默认Sequence类型的内存布局时,默认是按结构体中占用最多内存字节数的变量进行对齐。(在上面的例子中为8,这和C++中是一致的) 其实这应该是比较浪费空间的(上例中Name相当于占用了8个字节,浪费了4个字节)。如果不需要和非托管的代码交互,Struct我觉得最好不要采用默认的物理内存布局(Sequence).
-
结构体中包含字符
结构体中如果包含字符,则需要考虑编码的问题了。StructLayout中的CharSet属性可以控制结构体中字符串是封送为LPSTR (Ansi)还是LPWSTR (Unicode)。
同时在结构字段中也可以显示的通过MarshalAs来设置。举例说明:
[StructLayout(LayoutKind.Explicit,Pack = 8,CharSet=CharSet.Ansi)]
public struct Person
{
[FieldOffset(0)]
[MarshalAs(UnmanagedType.LPWStr)]
public string Name;
[FieldOffset(8)]
public double Weight;
[FieldOffset(16)]
public int Age;
}
public struct Person
{
[FieldOffset(0)]
[MarshalAs(UnmanagedType.LPWStr)]
public string Name;
[FieldOffset(8)]
public double Weight;
[FieldOffset(16)]
public int Age;
}
- 在非托管代码中修改结构体(封送结构体指针)
有时候我们需要传送一个结构体到非托管代码中,然后由非托管代码填充值,最后由托管代码读取结构体中的值。其实这和在托管代码中修改以引用方式传递的值类型是一个道理。
Example:
C++ 代码:
#pragma pack(push)
#pragma pack(8)
struct Person
{
public:
LPCWSTR name;
double weight;
int age;
};
#pragma pack(pop)
_declspec(dllexport) void _stdcall ModifyPerson(Person* person)
{
person->age = 25;
person->name = L"Updated";
person->weight = 60;
}
C#包装类:
[StructLayout(LayoutKind.Explicit,Pack = 8,CharSet=CharSet.Ansi)]
public struct Person
{
[FieldOffset(0)]
[MarshalAs(UnmanagedType.LPWStr)]
public string Name;
[FieldOffset(8)]
public double Weight;
[FieldOffset(16)]
public int Age;
}
[DllImport("native.dll", EntryPoint = "ModifyPerson", CharSet = CharSet.Ansi)]
public static extern void ModifyPerson(ref Person person);
C#调用代码:
Person person = new Person() { Name = "汪熊", Age = 27, Weight = 0 };
NativeWrapper.ModifyPerson(ref person);
Console.WriteLine(string.Format("Name:{0},Age:{1},Weight:{2}", person.Name, person.Age, person.Weight));