p/invoke 碎片-- 對字符串的處理

字符串在內存中的的幾種風格編程

字符串做爲參數和返回值數組

參考編程語言

 字符串在內存中的幾種風格函數

  所謂的風格,也就是字符串在內存中的存在形式。如何存放的,佔據內存的大小,還有存放順序等。在不一樣的編程語言和不一樣的平臺上字符串風格通常不相同。ui

  一、.net中字符串的風格 spa

  .net中的託管代碼:.net

1             string strin = "in string";
2             Console.WriteLine(strin);//斷點下在這裏
3             Console.Read();

   調試時查找字符串strin的地址發現內存中的狀況:線程

1 0x01363360  b8 97 0f 79 0a 00 00 00 09 00 00 00 69 00 6e 00  ...y........i.n.
2 0x01363370  20 00 73 00 74 00 72 00 69 00 6e 00 67 00 00 00   .s.t.r.i.n.g...

 個人一個疑問,這是.net中的字符串存放風格嗎?3d

  二、C風格指針

  託管代碼:

1         [DllImport(@"C:\Documents and Settings\Administrator\桌面\pInvoke\CPPDLL\Debug\CPPDLL.dll")]
2         private static extern void Putstring(string s);
3         static void Main(string[] args)
4         {
5             string strin = "in string";
6             Console.WriteLine(strin);
7             Putstring(strin);//調用非託管函數
8             Console.Read();
9         }

 

  非託管函數:

1 extern "C" __declspec(dllexport) void Putstring(char *str)
2 {
3     printf("%s\n",str);//斷點
4 }

 

   斷點外,在內存中能夠看到str的狀況:

1 0x0012F2EC  69 6e 20 73 74 72 69 6e 67 00 00 00 01 00 00 00  in string.......

 

  這就是C風格在內存中保存字符串,字符串以00結束,字符串的長度比字符個數多1,也就是後面的'\0'.C風格字符串的表示是以0結束的ASCII或Unicode字符數組。

三、Visual Basic 和 Java風格

  ●Visual Basic字符串是一個ASCII字符數組加上表示長度的前綴。
  ●Java字符串是以0結束的Unicode字符數組。
四、BSTR風格
  BSTR是「Basic STRing」的簡稱,微軟在COM/OLE中定義的標準字符串數據類型;須要定義一種通用的字符串類型,能夠很容易的匹配到不一樣編程語言。 C++ 中,就是 BSTR標準BSTR是一個有長度前綴和null結束符的OLECHAR數組。
1 0x0012F2EC  12 00 00 00 69 00 6e 00 20 00 73 00 74 00 72 00  ....i.n. .s.t.r.
2 0x0012F2FC  69 00 6e 00 67 00 00 00 22 00 00 00 00 00 00 00  i.n.g...".......

 封送字符串

   在封送字符串時,須要考慮字符的寬度、風格、是傳入仍是傳出以及內存釋放的問題。

  字符串做爲傳入參數的狀況

  非託管函數:

1 //字符串做爲傳入參數的狀況
2 extern "C" __declspec(dllexport) void Putstring(char* str)
3 {
4     printf("%s\n",str);
5 }

  一個簡單功能:把字符串在控制檯打印出來。

  託管代碼:

1         [DllImport(@"C:\Documents and Settings\Administrator\桌面\pInvoke\CPPDLL\Debug\CPPDLL.dll")]
2         private static extern void Putstring(string s);
3         static void Main(string[] args)
4         {
5             string strin = "in string";
6             Putstring(strin);
7             Console.Read();
8         }

  因爲CLR會自動採用默認的方式來進行封送,至關於下面的顯式操做:

1 private static extern void Putstring([In] [MarshalAs(UnmanagedType.AnsiBstr)]string s);
2 //默認是輸入並以(AnsiBstr)平臺相關的字符串指針進行封送

疑問1:不知道爲何不是和MSDN上指定的默認處理同樣(LPTStr)

   

字符串被做爲對平臺調用進行的調用的方法參數封送時的封送處理選項以下表:

字符串做爲參數或者返回值的處理方式

  執行過程以下:第一步string strin = "in string";在託管內存分配內存,保存字符串"in string",在內存中的位置和分佈以下:

0x01DEBF60  48 0d 87 61 0a 00 00 00 09 00 00 00 69 00 6e 00  H.?a........i.n.
0x01DEBF70  20 00 73 00 74 00 72 00 69 00 6e 00 67 00 00 00   .s.t.r.i.n.g...

 能夠看到在內存中的風格和上面說的是一個模型的。48 0d 87 61 不清楚這是什麼意思,其餘字符串的開始也是這個數據,0a 00 00 00 在其餘字符串中也是如此,第三個DWORD 09 00 00 00表示字符的個數。這哪是一個字符串呀,這分明是一個對象在內存中的形式吧!

  第二步Putstring(strin);從這裏進入非託管函數中。執行到函數中printf("%s\n",str);,這時能夠看到str表示一個內存指針,指向的內存0x01debf6c保存了傳遞的字符串,這個過程會在非託管內存分配一個地址,並在這個地址寫入處理後的字符串,這個地方的處理是按照非託管函數參數類型進行的,轉變成ANSI 的C風格樣式。以下:

69 6e 20 73 74 72 69 6e 67 00 00 00 00 00 00 00  in string.......

  第三步:Console.Read();從託管代碼到非託管代碼。這裏會有一個關鍵的動做,就是非託管地址0x01debf6c處的內存會被釋放掉,原來保存字符的地方會變的面目全非。

  在.net中,字符都是佔據兩個字節,也就是寬的,在這裏非託管函數中使用是的窄字符,ANSI,封送處理器會進行默認的轉變。若是非託管函數使用參數是wchar_t *類型,會出現什麼狀況呢?

  非託管函數簽名變化以下:

 1 void Putstring(wchar_t * str) 

   在即時窗口中看到的狀況:

  str
  0x002ff04c "湩猠牴湩g"  

  其緣由是當窄字符傳遞的,複製到非託管內存中每一個字符是緊密排列的,可是函數wprintf當寬的處理"in"的ASC碼當一個漢字來處理了,,結果什麼也沒有輸出到控制檯。此次的輸出只是一個特例,有一些狀況是輸出認不出的字符.若是把函數在託管代碼中描述修改成以下,就能正常輸出字符串了。而且內存也釋放。

 1 private static extern void Putstring([MarshalAs(UnmanagedType.BStr)] string s);//UnmanagedType.BStr

  封送處理器對輸入型的字符串的默認封送處理是UnmanagedType.AnsiBStr,在執行過程當中會在非託管內存先分配一塊內存,把字符串複製到這個內存中,進行適當的處理,最後從非託管內存返回後會自動釋放掉這個內存區域,而且沒有把字符串從非託管內存複製回託管內存,這就是以值的方式傳遞引用。其實,這個內存是使用CoTaskMemAlloc函數分配的,封送處理器會在最後返回進調用CoTaskMemFree函數來釋放掉佔用的內存,否則的話封送處理器沒有能力來釋放這塊內存

  爲了證實這種猜測,把非託管函數修改以下:

其餘分配內存的方法有malloc,new等方式。可是封送處理器沒有能力自動釋放使用這些方式在非託管內存中的分配的內存。

 //字符串做爲傳入參數的狀況
extern "C" __declspec(dllexport) void Putstring(char * str)
{
    char *pNew = (char *)malloc(10);//修改
    printf(str);
}

 

最終會發現,pNew指向的內存不會被釋放掉,而且在託管代碼中沒有能力釋放,惟一的方法就是再調用一個非託管方法專門釋放這個內存。若是是使用CoTaskMemAlloc來申請內存的話,能夠在託管代碼中使用Marshal.FreeCoTaskMem方法釋放此內存。

  字符串做爲傳出參數

  爲了達到一個目標,把字符串傳遞到非託管函數,非託管函數對字符串處理後可以把結果反映到託管代碼中而且不以返回值的方式完成這個目標,這就須要把字符串做爲傳出參數。在默認狀況下,對字符串的操做是做爲傳入[In]處理的,而且是經過複製字符串到一個非託管內存中,非託管函數對字符串的操做實際是對非託管內存中的那個字符串進行的。那怎麼來完成修改,使對字符串的修改能反映到託管代碼中呢?

  一、使用stringbuilder。有許多地方說是向非託管函數傳遞的是stringbuilder的緩衝區,我也就理解爲是指向字符串的指針,認爲非託管代碼和託管代碼操做的是同一塊內存區域,因此最終非託管代碼操做的結果也反映到了託管代碼中。可是在內存中的狀況並非我想的那樣。

  在使用stringbuilder做爲參數時,默認使用的方向屬性是[In,Out],而且有一些嚴格的要求才會作到「非託管函數的修改能反映到託管代碼中」。

  第一個要求就是顯式地肯定封送類型爲CharSet=CharSet.Unicode。不然的話,不會完整的複製。

  第二個要求就是非託管函數參數是Unicode。

  整個內存操做的過程仍是和上面說的同樣,來回的複製字符串到內存。不論過程如何,結果仍是能夠達到目標的。

  二、使用IntPtr。

1         private static extern void Putstring(IntPtr ps);
2         static void Main(string[] args)
3         {
4             IntPtr  ipstr = Marshal.StringToHGlobalAnsi("123456");
5             Putstring(ipstr);
6             Console.Read();
7         }

 

  非託管函數:

1 //字符串做爲傳入參數的狀況
2 extern "C" __declspec(dllexport) void Putstring(char * str)
3 {
4     printf(str);
5     StrCpy(str,"abc");
6 }

  經過跟蹤能夠看到,傳遞的的確是一個地址,是一個非託管地址,不管在託管代碼仍是非託管函數中,這個地址始終是0x00657ee0。所作的修改也都在這裏。惟一感受不太好的是這塊內存CLR不能主動釋放掉。   

  三、顯式以Unicode方式封送

  在託管代碼中聲明以下:

1         [DllImport(@"C:\Users\Administrator\Desktop\pInvoke\CPPDLL\Debug\CPPDLL.dll")]
2         //[return: MarshalAs(UnmanagedType.LPWStr)]
3         private static extern void Instring([MarshalAs(UnmanagedType.LPWStr)] string refstr);
4         static void Main(string[] args)
5         {
6             string refstr = "321";
7             Instring( refstr);
8         }

  非託管函數:

1 extern "C" __declspec(dllexport) void Instring(wchar_t *pStr)
2 {
3     wcscpy(pStr,L"abc");
4 }

 

主要的問題在於使用IntPtr的方法,是一個值得考慮的問題

  這時封送處理器會傳遞到非託管函數refstr="321"的地址,也就是pStr指向"321",這裏對pStr任何修改都能反映到refstr中。這樣真是

完美實現了做爲傳出參數的傳遞。這就是傳說中的固定,之前作的不少狀況都是複製數據。但這是有要求的:

  第一,託管代碼必須調用本機代碼,而不是本機代碼調用託管代碼。第二,該類型必須可直接複製或者必須能夠在某些狀況下變得可直接複製。第三,您不是經過引用傳遞(使用 out 或 ref)。第四,調用方和被調用方位於同一線程上下文或單元中。

可直接複製類型是指在託管和非託管內存中具備共同表示方法的類型。CLR中字符串是Unicode的,這時也是以[MarshalAsAttribute(UnmanagedType.LPWSTR)]指定顯式封送,就變成從託管代碼到非託管代碼可複製了。

  返回值是字符串

  非託管函數:

1 extern "C" __declspec(dllexport) char* Outstring()
2 {
3     char *pStr = (char*)malloc(8);
4     StrCpy(pStr,"abc");
5     return pStr;
6 }

   託管代碼:

1         private static extern int Outstring();
2         static void Main(string[] args)
3         {
4             int i = Outstring();
5             Console.Read();
6         }

 

  最終i接收的是一個DWORD值,是一個地址,指向非託管內存中的字符串"abc"。若是使用IntPtr接收,最後再Marshal.PtrToStringAnsi轉換能獲得正常結果。須要注意的是若是使用string類型進行接收的話,會出現異常提示訪問不可訪問的內存。

 參考

 http://www.codeproject.com/Articles/66243/Marshaling-with-Csharp-Chapter-3-Marshaling-Compou.aspx

MSDN雜誌上一篇文章

複製和鎖定內存管理、方向屬性等

《平臺互操做》

相關文章
相關標籤/搜索