c++與c#間的溝通與資料傳遞

目前的專案server端與client端間的共用邏輯是用C++寫的, 而Unity的主要使用程式碼又是c#, 所以有需要從c#呼叫c++端的方法, 以及從c++取資料, 這裡主要整理一下專案用的方法以及遇到的一些問題。

  1. Windows是編成dll, android是編成.so, ios主要是編成.a

  2. 溝通方式有分兩種:

  3. 簡單的呼叫範例
    c++用的巨集

    1
    2
    3
    4
    5
    #ifdef _MSC_VER 
    #define PLUGIN_DLL_API __declspec(dllexport)
    #else
    #define PLUGIN_DLL_API
    #endif

    c++端的方法

    1
    2
    3
    4
    5
    6
    extern "C" {
    PLUGIN_DLL_API void Test_Func(int StartIdx, int Count, int* OutArray)
    {
    ...
    }
    };

    c#的lib名稱宣告

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #if (UNITY_IOS && !UNITY_EDITOR_OSX) || UNITY_XBOX360

    // On iOS and Xbox 360 plugins are statically linked into
    // the executable, so we have to use **Internal as the
    // library name.

    public const string PlatformDllName = "__Internal";
    #else
    public const string PlatformDllName = "Name"; // 不用加lib.
    #endif

    c#端方法宣告

    1
    2
    [DllImport(SysPluginBase.PlatformDllName)]   
    private static extern void Test_Func(int StartIdx, int Count, System.IntPtr OutArray);

    要注意的有兩點:

    • 如果c++要傳指標給c#, c#需要用System.IntPtr來處理。
    • 兩邊的名稱必須一樣, 不然會有EntryPointNotFoundException
  4. 簡單的結構範例
    c++端

    1
    2
    3
    4
    5
    6
    7
    class Data
    {
    public:
    int m_Int; // int
    char m_Str[32]; // 字串
    int m_IntAry[10]; // int陣列
    };

    c#端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    [StructLayout(LayoutKind.Sequential, Pack = 4)]
    [Serializable]
    public class Data
    {
    public int m_Int; // int
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
    public string m_Str; // 字串
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
    public int[] m_IntAry; // int陣列
    };

    LayoutKind.Sequential主要是告知這個結構的記憶體是照順序排下來, 也可以宣告成 Explicit 來自己指定, 不過用 Sequential 是比較簡單的作法, 只要注意順序不要有錯誤就行了
    最基本的作法, 是傳該結構的指標到c#(IntPtr), 再使用Marshal.PtrToStructure來轉換成c#用的結構

    1
    2
    3
    extern "C" {    
    PLUGIN_DLL_API Data* Test_Func(){ ... }
    };

    在取IntPtr時需要檢查是否為null, 可以檢查該值是否為IntPtr.Zero

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    [DllImport(SysPluginBase.PlatformDllName)]   
    private static extern System.IntPtr Test_Func();
    public Data GetData()
    {
    System.IntPtr DataPtr = Test_Func();

    if (DataPtr == System.IntPtr.Zero)
    return null;

    Data Dta = (Data)Marshal.PtrToStructure(DataPtr, typeof(Data));
    return Dta;
    }
  5. 除了最基本的Marshal.PtrToStructure方法以外, 還有幾種其他的方法可以取得結構的值, 由於實在太多程式碼用gist來存

  6. iOS要使用CallBack C++Plugin, MonoPInvokeCallback 是一定要加的
    https://developer.xamarin.com/guides/ios/advanced_topics/limitations/#Reverse_Callbacks
    原文節錄如下:

    When using the ahead-of-time compiler required by the iPhone there are two important limitations at this point:
    You must flag all of your callback methods with the MonoPInvokeCallbackAttribute
    The methods have to be static methods, there is no support for instance methods.

    除了要在方法上方加上[AOT.MonoPInvokeCallback(typeof(對應該方法的delegate))]外, 該方法還需要是static方法.
    PC端跟android不需要

  7. 在android上如果結構使用pack = 1 , 在加上該struct有float的話, c#在使用Marshal.PtrToStructure就會造成crash, 只有android會有這個問題, 該問題找不到確切造成的原因。

  8. 結構中不要使用bool, 因為無法保證他的位元長度, 最好的方法還是用byte之類的代替。

ref: http://www.mono-project.com/docs/advanced/pinvoke/