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這個類型的操做。

相關文章
相關標籤/搜索