13 1月 2015

[C#] Allocate structure buffer array from DLL

C# 透過 DllImport 的方式讓你可以直接使用 C/C++ DLL 的 function,C/C++ 的程式常使用 struct 來儲存 data ,C# 也提供了讓你在 C# 中 define struct layout,這個轉換方式可以在 C# 中,直接宣告一個 struct 變數,丟入 DLL 中使用。

這點跟 Python 其實滿像的,Python 有 ctypes 可以做到類似的行為。script 中的 memory 跟 C/C++ 的 memory 無法互通,都必須透過轉換的方式讓 interpreter 可以順利看得懂 memory buffer。

C# 單一個 struct buffer 使用上都沒什麼問題,但是當你試著想要從 DLL 中撈回一串 struct array buffer時,就無法用直覺的方法完成這件事。因為 C# 在 DllImport 時的參數傳遞動了點手腳,所以如果直接將 C# 的 struct array 加 ref  傳至 DLL 可能會導致 crash 的產生。



1. 測試 C# dump struct buffer

測試一下 C# 在傳遞 struct buffer 的行為,將傳入的 struct address print 出來看看
printf() 可以替換為 OutputDebugString() 至 DbgView 中看結果會比較方便

TestDLL.cpp

#define DLL_API __declspec(dllexport)

typedef struct _Data
{
 bool bData;
 int nData;
 char strData[256];
} Data;

#ifdef __cplusplus
extern "C" {
#endif
 DLL_API bool Test_DumpAddress(Data* p)
 {
  printf("Test_DumpAddress() address=0x%p\n", p);
  return true;
 }

 DLL_API bool Test_DumpData(Data* d)
 {
  printf("Test_DumpData() address=0x%p bool=%d, int=%d, str=%s\n", d, d->bData, d->nData, d->strData);
  return true;
 }
#ifdef __cplusplus
}
#endif


TestDLL.cs

class Program
{
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public struct Data
    {
        public bool bData;
        public int nData;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string strData;
    }

    [DllImport("TestDLL.dll", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
    public static extern void Test_DumpAddress(ref Data p);

    [DllImport("TestDLL.dll", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
    public static extern bool Test_DumpData(ref Data d);

    static void Main(string[] args)
    {
        // testing for dump single struct buffer
        Data sd = new Data();
        sd.bData = false;
        sd.nData = 99;
        sd.strData = "Falldog";
        Test_DumpData(ref sd);
        Test_DumpAddress(ref sd);

        // testing for dump struct buffer array
        Data[] sd_array = new Data[5];
        for (int i = 0; i < 5; i++ )
        {
            sd_array[i].bData = false;
            sd_array[i].nData = 100+i;
            sd_array[i].strData = "Falldog";
            Test_DumpData(ref sd_array[i]);
        }

        Console.ReadLine();  // enter to end the process
    }
}

Result :
[4660] Test_DumpData() address=0x0035EB74 bool=0, int=99, str=Falldog
[4660] Test_DumpAddress() address=0x0035EB74
[4660] Test_DumpData() address=0x0035EB74 bool=0, int=100, str=Falldog
[4660] Test_DumpData() address=0x0035EB74 bool=0, int=101, str=Falldog
[4660] Test_DumpData() address=0x0035EB74 bool=0, int=102, str=Falldog
[4660] Test_DumpData() address=0x0035EB74 bool=0, int=103, str=Falldog
[4660] Test_DumpData() address=0x0035EB74 bool=0, int=104, str=Falldog

明顯看到 address 都是一樣的!在 C# 中明明是不一樣的變數,在傳入 DLL 後,address 都變一樣了,應該就是 C# 中間會產生一個 temp 的 buffer 來做轉換。

2. 將 DLL allocate 的 buffer load 至 C# 的 struct memory 中

以下為 C# 透過 DLL allocate 一塊 buffer,讓 DLL 填完後,在 C# 轉換為 C# 看得懂的 struct array buffer,所以傳下去的 buffer pointer type 為 IntPtr ,取出來後 再用 Marshal.PtrToStructure() 將 buffer 轉為 struct buffer,最後記得要將 DLL allocate 出來的 buffer free 掉,否則會 memory leak。

TestDLL.cpp

#define DLL_API __declspec(dllexport)

typedef struct _Data
{
 bool bData;
 int nData;
 char strData[256];
} Data;

#ifdef __cplusplus
extern "C" {
#endif
 DLL_API void Test_AllocData(int count, Data*& res)
 {
  res = new Data[count];
  for(int i=0 ; i<count ; i++){
   res[i].bData = true;
   res[i].nData = 500 + i;
   sprintf_s(res[i].strData, "Test_AllocData(%d)", i);
  }
 }

 DLL_API void Test_FreeData(Data* d)
 {
  if(d)
   delete [] d;
 }

#ifdef __cplusplus
}
#endif


TestDLL.cs
class Program
{
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public struct Data
    {
        public bool bData;
        public int nData;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string strData;
    }

    [DllImport("TestDLL.dll", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
    public static extern void Test_AllocData(int size, ref IntPtr ptr);

    [DllImport("TestDLL.dll", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
    public static extern void Test_FreeData(IntPtr ptr);

    static void Main(string[] args)
    {
        // testing for retrive struct array buffer from DLL
        IntPtr binary = new IntPtr();
        {
            Data[] ad = new Data[3];
            Test_AllocData(3, ref binary);
            ad[0] = (Data)Marshal.PtrToStructure(binary + Marshal.SizeOf(typeof(Data)) * 0, typeof(Data));
            ad[1] = (Data)Marshal.PtrToStructure(binary + Marshal.SizeOf(typeof(Data)) * 1, typeof(Data));
            ad[2] = (Data)Marshal.PtrToStructure(binary + Marshal.SizeOf(typeof(Data)) * 2, typeof(Data));
            Test_FreeData(binary);

            for (int i = 0; i < 3; i++)
            {
                Console.WriteLine(string.Format("@AllocData from DLL i={0} n={1} str={2}", i, ad[i].nData, ad[i].strData));
            }
        }
        Console.ReadLine();  // enter to end the process
    }
}


C# 的 DLL import 還有不少有趣的東西,只是因為接觸不多,沒時間好好來研究

Reference

沒有留言: