mono - C#外部呼叫so檔,字串傳遞和接收

本篇文章為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)
※char* 和const char*測試是用strcpy 和 pointer resigned的方式,如果都是用strcpy、memcpy的話,其實結果是一樣的。

這結果表示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();

 

這裡有一篇我針對表格第二個為什麼沒有作用的詳細探討,也算修正吧。

 

沒什麼好說的,就是被廠商雷到才會寫這一篇…

若有任何錯誤,請留言告知,會立即修正。