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

结构体的一些相关知识数组

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

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

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

  分如下步骤:性能

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

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

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

好比结构体:spa

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开始。3d

  第二步:安置成员 ,第一个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中进行操做。申请内存---填充数据---当指针传递---填充结构体或者类---释放内存。
  解释:
  一、 Marshal.AllocHGlobal是在 非托管内存分配一个大小为SizeOf(ms)的内存,在这里大小是8. pms保存这个地址(0x0017c520);
  二、 Marshal.StructureToPtr是把ms中的成员复制到pms保存的地址(0x0017c520)中。
  三、把地址0x0017c520传递到非托管函数中。非托管函数对成员的修改都反映到0x0017c520处。
  四、最后(MyStruct)Marshal.PtrToStructure 再把修改后的结果获得到一个新的结构mss中,这样就完成一次传递过程。
  五、最后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 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.

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

  二、指针的状况

  非托管代码:

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这个类型的操做。

相关文章
相关标签/搜索