p/invoke碎片,对结构体的处理

结构体的一些相关知识

可直接转换类类型,比如int类型,在托管代码和非托管代码中占据内存大小 和意义都是一个样的。

  结构体封送的关键是:在托管代码和非托管代码中定义的一致性。什么是定义的一致性?包括结构体的对齐粒度;结构体成员的顺序和每个结构体成员在托管代码和非托管代码中的占据内存的大小。本来想着是只是类型一样就行了,但是像布尔类型,在托管代码中占据1个字节,但是在非托管代码中是4个字节,也就是非可直接转换类型。

   对齐粒度。这个东西感觉和内存布局有关,以前有一个错误的理念:在一个结构体中定义了一此成员,那个这个结构体的大小是和每个成员占据内存大小之和相等。其实,根本不是这么回事,这和分布有关,在内存中结构体的成员并不是一个字节一个字节那样挨着排列的。而是内存对齐的。内存对齐的表现是,一个变量在定义时,分配的内存地址是n的倍数,n通常是4或者8.内存对齐的原因有两个方面,一是处理器的要求,另一个是提高性能;内存对齐的规则在不同的编译器和运行平台上是不同的,Win32平台下的微软C编译器(cl.exe for 80x86)在默认情况下采用如下的对齐规则: 任何基本数据类型T的对齐模数就是T的大小,即sizeof(T)。比如对于double类型(8字节),就要求该类型数据的地址总是8的倍数,而char类型数据(1字节)则可以从任何一个地址开始。对于结构体,比较特殊的类型,它的对齐规则和分配地址的原则:地址起始位置是由结构体成员中对齐要求最严格的那个成员来决定;然后每个成员按照自己的对齐原则来确定自己在结构体中的位置和占据的大小。 有了这两个原则,结构体分布就好理解了。找出一个给定结构体的内存对齐方式,可以确定结构体占据的内存大小。

  分以下步骤:

  第一、在所有成员中,找出对内存对齐要求最严格的那个成员。也就是找出占据内存大小最大的那个成员。从而确定结构体开始地址是哪个数的倍数。,比如此成员占据内存大小是8,那么结构体开始地址肯定是8*n ,n>=0 的正整数。

  第二、从第一个成员开始,按各个成员的内存对齐规则来安置成员。该留间隙的留间隙。

  第三、容易忽略的一个地方,完成前两个步骤后,最后要看结构体的大小是不是8的整数倍。这就要求对最后一个成员补 作为整个结构体的间隙,而不是作为成员的间隙。

比如结构体:

struct Test
{
    char c;//假如c的地址是3,那么a可以分配到4,正好是a大小的倍数。这种说法是错误的。因为在c地址为2时,a的地址就不成立了。不满足任意值
    int a;
}
    Test tt;
    printf("%d\n",sizeof(tt));//输出8

   第一步:找最严格成员,这里是a,大小是4,4就是本结构体的对齐粒度。这时可以知道结构体位置从4*n开始。

  第二步:安置成员 ,第一个c ,1个字节,第一个成员位置肯定是结构体的开始位置,是4*n;第二个成员a,占据4个字节,注意这里,不能从4*n+1开始紧紧挨着c放置a,因为a的对齐粒度是4,所以要留3个间隙,从4*n+4开始放置a.这时地址使用到4*n+8了。

  第三步:检查结构体大小,4n+8-4n=8,正好是4的倍数,不用再留间隙了。

对齐

 结构体分布内存三原则。第三个原则的意思是,有一个成员对内存要求大,那么本结构体就按它的要求来。对于要求严格 不严格,应该有一种顺序,但目测是和大小有关,成员占据内存大,就严格 。
  另一个例子:
1 struct Layout
2 {
3     int a;
4     double d;
5     char c;
6 };

   仍然按三步走:

  第一步:找出最对齐要求最严格的成员是double d,是8字节;这时确定结构体开始位置是8*n  ; 大小是8的倍数。

  第二步:安排成员。a, 从8n地址开始,占据4字节,这时地址使用到8n+4;不能紧接着在8n+5处安排d,因为d的对齐要求是8,地址要从8的整数倍开始,所以空出4字节间隙,从8n+8开始放d,这时地址使用到8n+17; 开始放c,可以从8n+17开始放c,因为c的对齐粒度是1,这时地址使用到8n+17.

  第三步:检查,结构体地址是8n+17-8n=17;不是8的整数倍,需要在最后补充7个字节间隙。

  最终,结构体成员分配完成,大小是24.

  内存结构:
但是修改一下:
1 struct Layout
2 {
3     double d;
4     int a;
5     
6     char c;
7 };

 这下就变成16了。8+4+1+3(空隙)。

或者是这样理解的,结构体的大小有一个要求,必然是要求最严格的成员占据内存大小的位数。就像例子中的double 大小是8,最后的间隙7或者4就是为了补充到8的位数。

  好像是这样一种分布方式:先得到最大成员double,占8字节;取第一成员,开始分配内存,原则为 大小*k,第一种情况下是4*k;取第二个double d,4*k+1地址不能放置了,一直到4*k+4地址才能放,这时空隙有4个了;取第三个成员,大小为1,地址为4*k+4+8,原则3,最后的这个7算是结构体的。不能是4+4+8+1,所以加上了最后7个空隙,而不是5个或者6个,加上7个正好struct大小是double 大小的倍数。

   结构体的对齐和大小大概如上,但是,还有一种情况,也就是可以在程序代码中对编译器处理,告诉编译器怎么处理结构体的对齐粒度。在C语言中,指令#pragma pack(n) 指定对齐为n,也就是说每个成员的起始位置都要是n的倍数,并且大小也是n的倍数。对于Layout结构体,对应的变量大小会变成多少呢?果然,结果是8+4+1+1(最后这个1是填充的空隙)。 另外,这个n不是无限的,对于VC6.0,编译器只认1,2,4,8,16  如果定义是5的话,还当没有设置来处理。

  来一个处理后的结构体情况:

  处理前

typedef struct
{
    char c;
    int a;
    double d;
}PackStruct;

    PackStruct ps;
    ps.c='k';
    ps.a=98;
    ps.d=6.8;

 

  结果:pack=8;结构体的大小 是16。看看内存吧,

结构体的内存    6B是'k'的ASCII码;后面的CC CC CC是空隙;62 00 00 00 是98;再后面的8个字节是6.8.

  处理后

#pragma pack(1)
typedef struct
{
    char c;
    int a;
    double d;
}PackStruct;
#pragma pack()

 

  结果:pack=1;结构体有大小是13,是因为空隙没有了。

pack=1时的内存分布 这时可以看到内存的不同了:6B是指'k';62 00 00 00 是98;后面的是6.8 。大小是13.

 在托管内存中定义与在非托管代码中等价的结构体

   这里的等价应该从三个方向来考虑:

第一是,结构体成员的顺序;

第二个是,每个结构体成员的等价性;

第三个是结构体的对齐。另外,成员的等价性包括占据内存的大小和类型。这里的成员主要注意“可直接在托管内存和非托管内存转换”的类型,和“不可直接在托管内存和非托管内存转换”的类型,比如bool,在两种内存都是一种意义 ,但是占据的大小却不相同。

  结构体成员的顺序和对齐可以用托管代码中的特性StructLayout来调整; 结构体成员的封送调整可以使用MarshalAs特性来调整。

  来看例子吧。

  非托管函数和结构体如下:

 1 typedef struct 
 2 {
 3     int a;
 4     char b;
 5 }MyStruct;
 6 extern "C" __declspec(dllexport) void GoStruct(MyStruct ms)
 7 {
 8     printf("%d\n",ms.a);
 9     printf("%c\n",ms.b);
10 }

 

   托管代码如下:

 1         const string dllpath = @"C:\Documents and Settings\Administrator\桌面\pInvoke\CPPDLL\Debug\CPPDLL.dll";
 2         [DllImport(@"C:\Users\Administrator\Desktop\pInvoke\CPPDLL\Debug\CPPDLL.dll")]
 3         private static extern void GoStruct(MyStruct ms);
 4         static void Main(string[] args)
 5         {
 6             MyStruct ms;
 7             ms.a = 101;
 8             ms.b = 'e';
 9             GoStruct(ms);
10             Console.Read();
11         }
12 
13         struct MyStruct
14         {
15             public int a;
16             public char b;
17         }

 

   这里好像什么都没有做,没有做一些说明性的东西来迎合上面的三个条件。其实,封送处理器已经做了一些事情,做了一些它力所能及的事情,默认对结构体的对齐和顺序处理使用了[StructLayout(LayoutKind.Sequential)]‎ 来进行处理。结构体的成员,也是按默认的方式进行封送,就像下面的处理那样:

1         [StructLayout(LayoutKind.Sequential)]
2         struct MyStruct
3         {
4           [MarshalAs(UnmanagedType.I4)]  public int a;
5           [MarshalAs(UnmanagedType.U1)]  public char b;
6         }

 

  只是没有写出来,其实在封送到非托管代码的时候,就是采用上面的规则来做的。事实上也是这样的。

  到这里,就需要了解UnmanagedType和MarshalAs。前者是对数据类型的处理,把某个变量以什么形式来传送到非托管代码,或者是从非托管代码传递过来的数据要怎么安排成托管代码中的形式。后者主要是修饰结构体和类,作用到对齐大小和成员顺序,其实它是靠构造函数的成员来实现的。LayoutKind 是构造函数传递的参数。LqyoutKind.Explicit枚举需要注意一下,这是一种更详细的对结构体的布局,它和FieldOffset的组合更精确地来处理结构体成员的间隙和分布,特别是对非托管代码中指定#pragma pack(n) 对齐的情况,使用LqyoutKind.Explicit和FieldOffset能很好的满足情况。

  结构体是以值传递的,数据在内存中的变化过程是这样的:先从托管内存中复制一份,分配在非托管代码的栈上,也就是压栈处理,再调用相应的函数。可以进行以下测试:

  在调用GoStruct方法处断下,看一下ms的地址:位置 是 0x0030ed5c

0x0030ed5c  65 00 00 00 65 00 00 00 50 bf e6 01 00 00 00 00  e...e

 

   解释:第一个65是101,可以看到占据4个字节,第二个65表示字符'e',也占据4个字节?是的,对齐后的补充间隙是3个字节。

  接着执行---到非托管函数中,断点在print前,结果再也找不到ms了!!!并且回到托管代码也没有看到两个e 在内存中的变化,也就是没有看到被释放。接下来只好移动到另一方法中了:

 1         static void Main(string[] args)
 2         {
 3             CallStruct();
 4             Console.Read();
 5         }
 6         static void CallStruct()
 7         {
 8             MyStruct ms;
 9             ms.a = 101;
10             ms.b = 'e';
11             GoStruct(ms);
12         }

 

不知道以int这样简单的类型作为参数时,内存会是什么样的变化呢?另外,用out替换ref也可以把结果反映到托管代码,但托管代码中结构体的内容不能传递到非托管内存。

  断点后执行到Console时,发现内存中的e占据的内存释放了,局部变量分布到栈上,等方法结束时,局部变量也被释放,这个结论是证明了。但非托管代码中的ms,我只能理解是一个复本,调用结束也被释放吧。

   在上面的测试中能看到一个现象,那就是,如果在非托管函数中对Ms进行修改的话,当从非托管函数返回时,托管代码中的ms结构体变化。由于一些需要,在非托管代码中对参数的修改能反映到托管代码怎么做呢?那就需要传递指针了。一
个例子:
  对上面的方法的参数可以使用ref 修饰,声明像下面这样:
 1 private static extern void GoStruct(ref MyStruct ms); 
  调用时,像下面:
 1             MyStruct ms;
 2             ms.a = 101;
 3             ms.b = 'e';
 4             GoStruct(ref ms);
 5 ///////////////////////////////////////////////非托管函数
 6 extern "C" __declspec(dllexport) void GoStruct(MyStruct* mms)
 7 {
 8     printf("%d\n",mms->a);
 9     mms->a=90;
10     printf("%c\n",mms->b);
11 }

   通过跟踪发现,传递的是ms的地址,所以非托管函数的修改反映到托管代码了,因为不论是托管代码还是非托管代码操作的都是一个地方。

  以上是第一种传递指针的方式 ,还有一种通用的方式传递指针:使用IntPtr。看一下内存情况。

  

        static void CallStruct()
        {
            MyStruct ms;
            ms.a = 101;
            ms.b = 'e';
            IntPtr pms = Marshal.AllocHGlobal(Marshal.SizeOf(ms));
            Marshal.StructureToPtr(ms, pms, true);
            GoStruct(pms);
            MyStruct mss = (MyStruct)Marshal.PtrToStructure(pms,typeof(MyStruct));
            Marshal.FreeHGlobal(pms);
        }
////////////////////////////////////////非托管函数
extern "C" __declspec(dllexport) void GoStruct(MyStruct* mms)
{
    printf("%d\n",mms->a);
    mms->a=90;
    printf("%c\n",mms->b);
}

 

这里涉及到对IntPtr的使用: IntPtr类型实际上是一个指针,使用时先分配内存,注意这个内存在非托管区域;然后把某个结构体或者类的内容复制到这个内存;当指针来传递;再释放这个内存。主要在类Marshal中进行操作。申请内存---填充数据---当指针传递---填充结构体或者类---释放内存。
  解释:
  1、Marshal.AllocHGlobal是在非托管内存分配一个大小为SizeOf(ms)的内存,在这里大小是8. pms保存这个地址(0x0017c520);
  2、Marshal.StructureToPtr是把ms中的成员复制到pms保存的地址(0x0017c520)中。
  3、把地址0x0017c520传递到非托管函数中。非托管函数对成员的修改都反映到0x0017c520处。
  4、最后(MyStruct)Marshal.PtrToStructure 再把修改后的结果得到到一个新的结构mss中,这样就完成一次传递过程。
  5、最后Marshal.FreeHGlobal释放0x0017c520处的内存。
  这个过程显然比上一个过程复杂。
   以上的情况只是结构体或者其指针作为函数参数的情况,如果是作为函数返回值的情况 ,不能再使用ref了,只好使用IntPtr接收。
   小结:传递结构体地址两种方式,ref 修饰参数和IntPtr。 IntPtr这种方式比ref多了两个复制过程。
 
  上面所举例的结构体是简单的结构体,如果是复杂的结构体,有时需要严格来处理每个成员了。

 结构体成员中有复杂成员时,如何等价定义和处理封送

  第一种复杂成员:成员是字符串。

  这时,就要注意使用CharSet 类型来约束字符的宽度,使与非托管代码中对应。

 

 1 //////////////////////////////////////////非托管代码:结构体定义和使用
 2 typedef struct
 3 {
 4     char *pFirstName;//结构体中有字符串
 5     char *pNextName;
 6 }StrStruct;
 7 extern "C" __declspec(dllexport) void DoStrStruct(StrStruct ss)
 8 {
 9     printf("第一个名字是:%s\n",ss.pFirstName);
10     printf("第二个名字是:%s\n",ss.pNextName);
11 }
12 ///////////////////////////////////////托管代码:等价结构体定义和使用
13            
14        {
15            StrStruct ss;
16             ss.firstname = "this is firstname";
17             ss.nextname = "this is nextname";
18             DoStrStruct(ss);
19         }
20 
21         struct StrStruct
22         {
23             public string firstname;
24             public string nextname;
25         }

 

  在内存中的情况和分配过程:

  ★、把托管代码中结构体内的字符串复制一份,放到非托管内存中的某个地址。在本例中两个字符串,对应两个地址;非托管内存分别是0x003A0920和0x003A0960。

  在托管代码中的地址如下:

0x01B0BF60  48 0d c5 64 12 00 00 00 11 00 00 00 74 00 68 00  H.?d........t.h.
0x01B0BF70  69 00 73 00 20 00 69 00 73 00 20 00 66 00 69 00  i.s. .i.s. .f.i.
0x01B0BF80  72 00 73 00 74 00 6e 00 61 00 6d 00 65 00 00 00  r.s.t.n.a.m.e...
0x01B0BF90  00 00 00 80 48 0d c5 64 11 00 00 00 10 00 00 00  ...€H.?d........
0x01B0BFA0  74 00 68 00 69 00 73 00 20 00 69 00 73 00 20 00  t.h.i.s. .i.s. .
0x01B0BFB0  6e 00 65 00 78 00 74 00 6e 00 61 00 6d 00 65 00  n.e.x.t.n.a.m.e.

  在非托管代码中的地址如下:

0x003A0920  74 68 69 73 20 69 73 20 66 69 72 73 74 6e 61 6d  this is firstnam
0x003A0930  65 00 ad ba 0d f0 ad ba 0d f0 ad ba 0d f0 ad ba  e.??.???.???.???
0x003A0940  0d f0 ad ba ab ab ab ab ab ab ab ab ee fe ee fe  .???????????????
0x003A0950  00 00 00 00 00 00 00 00 46 d3 98 3c b2 f6 00 1e  ........F??<??..
0x003A0960  74 68 69 73 20 69 73 20 6e 65 78 74 6e 61 6d 65  this is nextname

  非托管函数对字符串的处理,都是基于地址0x003A0920这里。

  ★、把两个地址包装到非托管代码中结构体中,当作两个指针来使用。

  

1 ss
2 {
3     0x003A0920;
4     0x003A0960;
5 }

 

  ★、最后把ss作为参数传递给非托管函数DoStrStruct

在这里地址被释放的原因是:在非托管代码中分配的两个字符串的内存是用CoMemAlloc方式分配的,封送处理器有能力自动处理掉。

  ★、调用结束,执行返回到托管代码时。刚才分配的非托管内存会释放掉,地址0x003A0920和0x003A0960处的数据变得面目全非了。

   扩展1:结构体中的多个字符串宽度不相同的情况。

  上面的例子中, 第一个字符串和第二个字符串字符类型都是ANSI的,如果有一个是UNICODE怎么办呢?这时对结构体字段的封送处理就不能使用默认情况了,需要各个说明。如下:

 

 1 /////////////////////////////////////////////////非托管函数
 2 
 3 typedef struct
 4 {
 5     char *pFirstName;//结构体中有字符串
 6     wchar_t *pNextName;
 7 }StrStruct;
 8 extern "C" __declspec(dllexport) void DoStrStruct(StrStruct ss)
 9 {
10     printf("%s\n",ss.pFirstName);
11     wprintf(L"%s\n",ss.pNextName);//以宽字符打印出
12 }
13 //////////////////////////////////////////////////托管代码   
14      [StructLayout(LayoutKind.Sequential)]
15         struct StrStruct
16         {
17             [MarshalAs(UnmanagedType.LPStr)] public string firstname;
18             [MarshalAs(UnmanagedType.LPWStr)] public string nextname;
19 //如果第二个字段使用MarshalAs(UnmanagedType.LPStr)方式封送的话,不会打印出东西来的
20         }

 

   过程和上面是一样的,但是封送的方式不一样。某个字段需要显式的说明,因为和默认的不同。这种方式和单一操作字符串的方式差不多,也是涉及到字符串的复制和内存分配,并且内存分配的方式是CoTaskMemAlloc,不是malloc 和new 方式(第一种方式封送处理器能检测到并把它释放掉)。

   扩展2: 为了能非托管代码中对字符串的操作结果返回到托管代码中

   为了这个目的,可以使用ref 来显式说明对结构体变量的封送。代码如下:

 

///////////////////////////////////////////////////////非托管函数
typedef struct
{
    char *pFirstName;//结构体中有字符串
    wchar_t *pNextName;
}StrStruct;
extern "C" __declspec(dllexport) void DoStrStruct(StrStruct* pss)
{
    printf("%s\n",pss->pFirstName);
    wprintf(L"%s\n",pss->pNextName);
    strcpy(pss->pFirstName,"new ansi string ");//修改
    wcscpy(pss->pNextName,L"new unicode string");//修改
}
////////////////////////////////////////////////////////托管代码      
  [DllImport(@"C:\Users\Administrator\Desktop\pInvoke\CPPDLL\Debug\CPPDLL.dll")]
        private static extern void DoStrStruct(ref StrStruct ss);

            StrStruct ss;
            ss.firstname = "this is firstname";
            ss.nextname = "this is nextname";
            DoStrStruct(ref ss);
            Console.WriteLine("新的字符串分别为:");
            Console.WriteLine(ss.firstname);
            Console.WriteLine(ss.nextname);

        struct StrStruct
        {
            [MarshalAs(UnmanagedType.LPStr)] public string firstname;
            [MarshalAs(UnmanagedType.LPWStr)] public string nextname;
        }

 

 

 

   结果显而易见:在非托管代码中的修改反映到托管代码中了。通过跟踪发现,传递过去的的确是一个结构体指针,这个结构体指针指向两个指向字符串的指针,或者说是指向两个字符串的地址,非托管代码中的操作都是对这两个地址操作的,所以向这两个地址复制后,托管代码是看得见的。

   PS:对于传递地址,还可以使用IntPtr类型来完成。

   第二种复杂成员:非托管代码中的结构体使用pack约束对齐粒度,内存对齐发生变化。

  在这种情况下,主要是让结构体在托管内存封送到非托管内存时,保存一致性。一致性上面说了,完全一样:每个成员一致性,结构体内存对齐。看一个例子,

 

 1         [DllImport(@"C:\Users\Administrator\Desktop\pInvoke\CPPDLL\Debug\CPPDLL.dll")]
 2         private static extern void DoPackStruct(PackStruct ps);     
 3 
 4             PackStruct ps;
 5             ps.a = 34;
 6             ps.c = 'h';
 7             ps.d = 2.3;
 8             DoPackStruct(ps); 
 9 
10  
11        [StructLayout(LayoutKind.Sequential)]
12         struct PackStruct
13         {
14             public int a;
15             public char c;
16             public double d;
17         }
//非托管结构体
typedef struct
{
    int a;
    char c;
    double d;
}PackStruct;

  在非托管代码中的布局如下,需要注意的一点是68 00 指字符'h',它后面的两个00 00 是内存间隙。

  0x0031F17C  22 00 00 00 68 00 00 00 66 66 66 66 66 66 02 40 00  "...h...ffffff.@.

  现在进行修改:

1 #pragma pack(1)
2 typedef struct
3 {
4     int a;
5     char c;
6     double d;
7 }PackStruct;
8 #pragma pack()

   托管代码中的处理:

        [StructLayout(LayoutKind.Explicit,Pack=1)]
        struct PackStruct
        {
            [FieldOffset(0)]public int a;
            [FieldOffset(4)] [MarshalAs(UnmanagedType.U1)]public char c;
            [FieldOffset(6)] public double d;
        }

 

  第三种复杂成员:结构体的成员中存在“非直接复制到本机结构中的类型”。

  一个典型类型是布尔类型-bool,虽然含义是一样的,但是在托管代码中占据1个字节,在非托管代码中占据1个字节(VS6),要保证二者在两种平台上占据内存大小一样,在封送时就要显式的进行说明。

 

 1 ////////////////////////////////////////非托管函数
 2 typedef struct
 3 {
 4     bool b;
 5     bool bb;
 6     int a;
 7 }BoolStruct;
 8 
 9 extern "C" __declspec(dllexport) void DoBoolStruct(BoolStruct bs)
10 {
11     if(bs.bb==true)
12     {
13         printf("传递的是true");
14     }
15     else
16     {
17         printf("false");printf("%d\n",bs.a);
18     }
19 }
20 /////////////////////////////////////////托管代码
21         [StructLayout(LayoutKind.Sequential)]
22         struct BoolStruct
23         {
24             public bool b;
25             public bool bb;//使用默认的封送方式,以4个字节传递
26             public int a;
27         }
28             BoolStruct bs;
29             bs.b = true;
30             bs.bb = true;
31             bs.a = 32;
32             DoBoolStruct(bs);
33             Console.Read();

   在托管代码中的分布情况:

托管代码中分布

   封送后在非托管代码中情况 :

结果

在很多地方,说是bool类型在非托管内存中占据4字节,但是在托管内存中是1字节,不知道是什么用意呢?单独使用vc6一个程序测试,bool仍然占据1字节呢?平台调用时,只是按4字节处理了。

   默认情况 下是按4字节封送的,那么非托管代码对布尔类型怎么处理的呢?

  通过跟踪发现,如果是bs.b==true这个判定的话,程序会从0x002cf2bc处取4个字节,与0xff进行and操作,也相当于取1个字节了。那如果是bs.bb==true这个判定的话,就会取0x2cf2bd开始的4个字节与0xff进行and操作,也是相当于取1个字节了,但是,这时的结果就出现了意外!接着执行,printf("%d\n",bs.a);会怎么样呢?程序会取0x002cf2c0开始的4个字节当一个int打印出来,这时错误的结果就明显了。

  为什么会这样呢?原来 ,非托管代码还是以为封送后,内存像在托管代码中布局那样,如果像托管代码中布局那的话,结果是完成正确。printf("%d\n",bs.a);这里取的a是按内存对齐来取的,如果内存像这样:

0x002cf35c 01 01 00 00 20 00 00 00 ,那取bs.a时就会从0x002cf360处取4字节,结果就正确了。

  结论:对bool类型的封送要做显式处理

1         [StructLayout(LayoutKind.Sequential)]
2         struct BoolStruct
3         {
4             [MarshalAs(UnmanagedType.I1)]public bool b;
5             [MarshalAs(UnmanagedType.I1)]public bool bb;
6             public int a;
7         }

   第四种复杂成员:结构体中的联合体。

   联合体中的一个特点是 :所有成员都占据同一个偏移位置,大小等于最大的那个成员的大小 。

typedef union 
{
    char c;
    int a;
    double d;
}MyUnion;
typedef struct 
{
    int a;
    char c;
    MyUnion mu;
}UnionStruct;

extern "C" __declspec(dllexport) void DoUnionStruct(UnionStruct us)
{
    printf("%d\n",us.a);
    printf("%lf\n",us.mu.d);
}

 

////////////////////////////////////////////////////托管代码
        [DllImport(@"C:\Users\Administrator\Desktop\pInvoke\CPPDLL\Debug\CPPDLL.dll")]
        private static extern void DoUnionStruct(UnionStruct ps);

            UnionStruct us;
            us.c = 'd';
            us.a = 99;
            us.d = 9.8;
            DoUnionStruct(us);

        [StructLayout(LayoutKind.Sequential)]
        struct UnionStruct
        {
            public int a;
            public char c;
            public myunion mu;
        }

        [StructLayout(LayoutKind.Explicit)]
        struct myunion
        {
           [FieldOffset(0)] public char c;
            [FieldOffset(0)] public int a;
            [FieldOffset(0)] public double d;
        }

 

  结果能正常显示,只要定义同种类型就行了。这是第一种方法

  另外,如果知道非托管代码中对联合体中哪个成员处理的话,可以直接把联合体当那个类型处理。例如,上面的例子中非托管函数是对联合体MyUnion中的double成员处理,所以完全可以在托管代码中如下定义等效结构体:

        [StructLayout(LayoutKind.Sequential)]
        struct UnionStruct
        {
            public int a;
            public char c;
            public double mu;
        }

 

会达到相同的效果。这是第二种方法

不能第一种方法的原因是在托管代码中,值类型和引用类型不能重叠,为什么呢?个人感觉是在托管内存中,值类型和引用类型内存位置不同。

  以上联合体中的成员有一个特点,那就是两个成员在托管代码中都是值类型的,能用以上两种方式处理。但是,当联合体中的成员在托管代码中属于不同的类型,比如一个值类型,一个引用类型时,就只能用第二种方法了。如下:

 1 typedef union 
 2 {
 3     char c;
 4     int a[5];
 5 }MyUnion;
 6 typedef struct 
 7 {
 8     int a;
 9     char c;
10     MyUnion mu;
11 }UnionStruct;
12 
13 extern "C" __declspec(dllexport) void DoUnionStruct(UnionStruct us)
14 {
15     printf("%s\n",us.mu.a);
16 }

 

  明显看到,非托管函数对联合体的处理,其实处理的是第二个成员,是个数组a[5],大小5个int的大小。这时使用上面的第二种方法来处理,定义等价结构体为:

1         [StructLayout(LayoutKind.Sequential)]
2         struct UnionStruct
3         {
4             public int a;
5             public char c;
6             [MarshalAs(UnmanagedType.ByValArray,SizeConst=5)]public int []muint;
7         }

 

   前两成员不用显式说明封送方式了。默认的就可以正确处理。

   验证代码:

 

            UnionStruct us;
            us.c = 'd';
            us.a = 99;
            us.muint = new int[5];
            us.muint[0] = 11;
            us.muint[1] = 22;
            DoUnionStruct(us);

 

   结果是正确的,打印出11 和 22.

   那如果非托管函数中处理的联合体中的第一上成员呢?这时就需要在托管代码中新定义一个结构体,按上面的方式来定义:

1        [StructLayout(LayoutKind.Sequential)]
2         struct UnionStruct2
3         {
4             public int a;
5             public char c;
6             public char cc;
7         }

 

   声明的函数参数也跟着变化,相当于函数的重载了。

        [DllImport(@"C:\Users\Administrator\Desktop\pInvoke\CPPDLL\Debug\CPPDLL.dll")]
        private static extern void DoUnionStruct(UnionStruct2 us2);

 

   这样,就完成了结构体中有联合体情况的处理。

   第五种复杂成员:结构体中存在子结构体

  有两种情况:父结构体中含有子结构体类型的实例;父结构体中含有子结构体的指针。

  1、实例的情况

  非托管代码:

 1 typedef struct
 2 {
 3     char * firstname;
 4     char * lastname;
 5 }NAMES;
 6 typedef struct
 7 {
 8     NAMES names;
 9     int score;
10 }STUDENT;
11 
12 extern "C" __declspec(dllexport) void DoStructStruct(STUDENT student)
13 {
14     printf("%s\n",student.names.firstname);
15     printf("%s\n",student.names.lastname);
16     printf("%d\n",student.score);
17 }

 

   托管代码:

      [DllImport(@"C:\Users\Administrator\Desktop\pInvoke\CPPDLL\Debug\CPPDLL.dll")]
        private static extern void DoStructStruct(Student stu);

        static void Main(string[] args)
        {
            CallStruct();
            Console.Read();
        }
        static void CallStruct()
        {
            Student stu;
            stu.names.firstname = "chen";
            stu.names.lastname = "zhi";
            stu.score = 99;
            DoStructStruct(stu);

            Console.Read();
        }

        struct Student//默认封送
        {
            public Names names;
            public int score;
        }
        struct Names//默认封送
        {
            public string firstname;
            public string lastname;
        }

 

 结果能正常显示:chen zhi 99  .那么看一下内存是怎么变化的。

为什么顺序是反的呢?

  首先、断点在DoStructStruct,取地址&stu 得到结果:0x0014EF48  63 00 00 00 60 bf e2 01 7c bf e2 01   地址0x0014ef48 ,可以看到63 00 00 00 明显是99,后面的01 e2 bf 60 和 01 e2 bf 7c 是两上地址,分别指向两个字符串。其效果和下面的声明是一样的:

        struct Student
        {
            public string firstname;
            public string lastname;
            public int score;
        }

   其次、断点在非托管函数中,找到student的地址:0x00640C60  63 68 65 6e 00 f0 ad ba ee fe ab ab ab ab ab ab ab  chen.????????????  其实是第一个成员的地址,第二个地址 0x00640C88  7a 68 69 00 0d f0 ad ba ab ab ab ab ab ab ab ab 00  zhi..???????????. 最后压入栈的是63,十进制的就是99.

 执行回到托管代码时,上面两个地址处的数据被清除掉了。封送处理器做了许多工作。

  2、指针的情况

  非托管代码:

typedef struct
{
    char * firstname;
    char * lastname;
}NAMES;
typedef struct
{
    NAMES* names;
    int score;
}STUDENT;

 

   托管代码:

            Student stu;
            Names ns;
            ns.firstname = "aaa";
            ns.lastname = "bbb";
            IntPtr iptr = new IntPtr();
            iptr = Marshal.AllocCoTaskMem(Marshal.SizeOf(ns));//分配内存
            Marshal.StructureToPtr(ns, iptr, true);//
            stu.names = iptr;
            stu.score = 99;
            DoStructStruct(stu);

            Console.Read();
        }
        [StructLayout(LayoutKind.Sequential)]
        struct Student
        {
            public IntPtr names;
            public int score;
        }
        struct Names
        {
            public string firstname;
            public string lastname;
        }

 

  主要是对IntPtr这个类型的操作。

 

 

 

 

 

posted @ 2015-01-29 07:49  deeeeeed  阅读(688)  评论(0编辑  收藏  举报

pppppppppppppp