本篇文章為linux下開發c#,內文與windows開發的會有些不同
雖然會盡量寫上對應在windows上的名稱,但若有疏忽請見諒
mono下c#使用so(dll)檔進行外部呼叫時
string(字串)的傳遞及接收。
正文開始
在微軟官網上列出了當我們使用c#進行外部呼叫so(dll)時,參照範例
基本上依照官網設定就可以完成大部分的呼叫,int long struct...等
但實際上使用時我在string卻遇到了大量的問題
於是進行了一連串的測試。
先把要外部呼叫的so檔寫好
CharTest 使用strcpy將字串改寫
ConstCharTest 使用string將指標位置改寫
DoubleCharTest 使用雙指標,讓原指標指向的字串指標改寫
ReturnStrTest 直接回傳字串
值得注意的是ReturnStrTest回傳在C/C++標準中,是不允許陣列回傳的
需產生一個記憶體空間後把資料帶入回傳。
int CharTest (char *MyWord)
{
printf ("\nSO get string:%s\n", MyWord);
strcpy (MyWord, "newStringReturn");
printf ("SO replace string:%s\n\n", MyWord);
return 0;
}
int ConstCharTest (const char *MyWord)
{
printf ("\nSO get string:%s\n", MyWord);
MyWord = "newStringReturn\x00";
printf ("SO replace string:%s\n\n", MyWord);
return 0;
}
int DoubleCharTest (const char **MyWord)
{
printf ("\nSO get string:%s\n", *MyWord);
*MyWord = "newStringReturn\x00";
printf ("SO replace string:%s\n\n", *MyWord);
return 0;
}
char* ReturnStrTest ()
{
char MyStr[] = "newStringReturn\x00";
int length = strlen(MyStr);
length++;
char *StrPtr = (char*)malloc(length);
for (int i =0;i<length;i++)
StrPtr[i] = MyStr[i];
return StrPtr;
}
so檔準備好,換c#
1.測試 char *
public MainWindow() : base(Gtk.WindowType.Toplevel)
{
Build();
CharTest();
}
void CharTest ()
{
int Ans;
string Str = "helloworldmycsharpcode\x00";
Ans = Test_API.CharTest(Str);
Console.WriteLine("string after:" + Str);
byte[] ByteAry = System.Text.Encoding.Default.GetBytes(Str);
Ans = Test_API.CharTest(ByteAry);
Console.WriteLine("byteary after:" + System.Text.Encoding.Default.GetString(ByteAry));
IntPtr MyPtr = Marshal.StringToHGlobalAnsi(Str);
Ans = Test_API.CharTest(MyPtr);
Console.WriteLine("IntPtr after:" + Marshal.PtrToStringAnsi(MyPtr));
Marshal.FreeHGlobal(MyPtr);
}
public static class Test_API
{
const string SO_FILE_NAME = "libstrtest.so";
[DllImport(SO_FILE_NAME, EntryPoint = "CharTest")]
public static extern int CharTest(string MyWord);
[DllImport(SO_FILE_NAME, EntryPoint = "CharTest")]
public static extern int CharTest(byte[] MyWord);
[DllImport(SO_FILE_NAME, EntryPoint = "CharTest")]
public static extern int CharTest(IntPtr MyWord);
[DllImport(SO_FILE_NAME, EntryPoint = "ConstCharTest")]
public static extern int ConstCharTest(string MyWord);
[DllImport(SO_FILE_NAME, EntryPoint = "ConstCharTest")]
public static extern int ConstCharTest(byte[] MyWord);
[DllImport(SO_FILE_NAME, EntryPoint = "ConstCharTest")]
public static extern int ConstCharTest(IntPtr MyWord);
[DllImport(SO_FILE_NAME, EntryPoint = "DoubleCharTest")]
public static extern int DoubleCharTest(ref string MyWord);
[DllImport(SO_FILE_NAME, EntryPoint = "DoubleCharTest")]
public static extern int DoubleCharTest(ref byte[] MyWord);
[DllImport(SO_FILE_NAME, EntryPoint = "DoubleCharTest")]
public static extern int DoubleCharTest(ref IntPtr MyWord);
[DllImport(SO_FILE_NAME, EntryPoint = "ReturnStrTest")]
public static extern string ReturnStrTest1();
[DllImport(SO_FILE_NAME, EntryPoint = "ReturnStrTest")]
public static extern byte[] ReturnStrTest2();
[DllImport(SO_FILE_NAME, EntryPoint = "ReturnStrTest")]
public static extern IntPtr ReturnStrTest3();
}
在官網說明中只有string,但我測試後其實可以使用byte[]及IntPtr將string帶入(其實也蠻合理的)
結果:
string沒有改變
byte[]雖然改變了,但送出去的值也殘存了,這樣在處理上會變的很麻煩
(除非你在C/C++上先將資料完整清除後在strcpy)
IntPtr就全正確了
2.再來是const char *
void ConstCharTest()
{
int Ans;
string Str = "helloworldmycsharpcode\x00";
Ans = Test_API.ConstCharTest(Str);
Console.WriteLine("string after:" + Str);
byte[] ByteAry = System.Text.Encoding.Default.GetBytes(Str);
Ans = Test_API.ConstCharTest(ByteAry);
Console.WriteLine("byteary after:" + System.Text.Encoding.Default.GetString(ByteAry));
IntPtr MyPtr = Marshal.StringToHGlobalAnsi(Str);
Ans = Test_API.ConstCharTest(MyPtr);
Console.WriteLine("IntPtr after:" + Marshal.PtrToStringAnsi(MyPtr));
Marshal.FreeHGlobal(MyPtr);
}
結果:
通通沒有變XD
3.測試const char **
void DoubleCharTest()
{
int Ans;
string Str = "helloworldmycsharpcode\x00";
Ans = Test_API.DoubleCharTest(ref Str);
Console.WriteLine("string after:" + Str);
byte[] ByteAry = System.Text.Encoding.Default.GetBytes(Str);
Ans = Test_API.DoubleCharTest(ref ByteAry);
Console.WriteLine("byteary after:" + System.Text.Encoding.Default.GetString(ByteAry));
IntPtr MyPtr = Marshal.StringToHGlobalAnsi(Str);
Ans = Test_API.DoubleCharTest(ref MyPtr);
Console.WriteLine("IntPtr after:" + Marshal.PtrToStringAnsi(MyPtr));
//Marshal.FreeHGlobal(MyPtr);
}
結果:
string crush惹
byte[] 沒變化
IntPtr有變化,但無法釋放該指標
會造成這問題是因為原始的intptr在so檔內被改成so檔產生的指標
所以回到c#手上,雖然能讀到該資料,但已非原始c#的位址,
因先釋放是會出錯的,需讓so檔進行釋放,
這方式會造成大量的unmanaged pointer在c#和so檔內,三思。
4.最後一個return char*
void ReturnStrTest()
{
string Str = Test_API.ReturnStrTest1();
Console.WriteLine("string after:" + Str);
byte[] ByteAry = Test_API.ReturnStrTest2();
Console.WriteLine("byteary after:" + System.Text.Encoding.Default.GetString(ByteAry));
IntPtr MyPtr = Test_API.ReturnStrTest3();
Console.WriteLine("IntPtr after:" + Marshal.PtrToStringAnsi(MyPtr));
Marshal.FreeHGlobal(MyPtr);
}
byte[]失敗,其它都正常。
這邊做一張表格處理一下
| char * | const char* | const char ** | return char * | |
| string | ✖ | ✖ | ✖(會crush) | ✔ |
| byte[] | ✔(資料未清) | ✖ | ✖ | ✖ |
| InpPtr | ✔ | ✖ | ✔(無法free) | ✔ |
這結果表示string完全沒有用處嗎?
其實大部分的情況下,送資料過去,這三種都可以(官網的stringbuilder與string相同,就不額外說明了)
但若是要接收回傳的資料時,送出和接收同一個時,可以IntPtr,除非回傳時,會帶長度資訊給你,否則別用byte[]
int Length = ReturnLength;
System.Text.Encoding.Default.GetString(ByteAry, 0, Length);
若只是單純的接收,可以使用IntPtr和byte[]
System.Text.Encoding.Default.GetString(ByteAry).Replace('\0', ' ').Trim();
這裡有一篇我針對表格第二個為什麼沒有作用的詳細探討,也算修正吧。
沒什麼好說的,就是被廠商雷到才會寫這一篇…
若有任何錯誤,請留言告知,會立即修正。