本文介绍Unity和iOS和Windows通信

1. Unity与C++交互

Windows平台的SDK是C++编写的DLL库,所以我们需要Unity和C++进行通信

1.1 基本概念

  • 托管(Managed)和非托管(Unmanaged):.Net的运行环境是CLR(Common Language Runtime),运行在CLR上的代码成为托管代码(Managed Code),CLR提供了自动的垃圾回收机制(GC)。而C++是编译后直接由操作系统执行的代码,不运行在CLR上,所以C++属于非托管代码(Unmanaged Code)。
  • P/Invoke:P/Invoke(Platform Invoke,平台调用)使得我们可以在托管代码中调用非托管函数,Unity与C++的交互都是通过P/Invoke实现。([DllImport])

1.2 创建Wrapper层C++ DLL

在VS中新建DLL项目,把引用的IM和RTC库拖入项目,在项目属性中设置好*.dll.lib链接地址以及头文件链接地址(如下图)




新建一个类RTCInterface.h和RTCInterface.cpp

//RTCInterface.h

extern "C"
{
    __declspec(dllexport) RCRTCEngine *rcrtc_create_engine(void* im_client);
    //__declspec(dllexport)必须添加
}
//RTCInterface.cpp
#include "RTCInterface.h"

extern "C"
{
    RCRTCEngine *rcrtc_create_engine(void* im_client)
    {
        //code...
    }
}

1.3 C#调用

右键项目生成DLL后,将生成的DLL以及依赖的DLL拷贝到Unity项目对应架构的目录中,设置好对应的架构

在Unity中新建NativeWin.cs脚本

//NativeWin.cs
namespace cn_rongcloud_rtc_unity
{
    internal class NativeWin
    {
        [DllImport("RTCWinWapper", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
        internal static extern IntPtr rcrtc_create_engine(IntPtr client);
        //DllImport后面的名称需要和引入的DLL名称一致
    }
}

这样在C#中调用NativeWin的create方法就可以调用到C++的代码了

1.4 其他情况

  • 参数中有类

需要在C#和Wrapper中声明对应的结构体,C#端将C#类转为结构体,Wrapper中将结构体转为C++类

//声明结构体
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
internal struct rtc_engine_setup
{
    [MarshalAs(UnmanagedType.U1)] 
    public bool reconnectable;

    public int statsReportInterval;

    [MarshalAs(UnmanagedType.U1)] 
    public bool enableSRTP;
    public IntPtr audioSetup;
    public IntPtr videoSetup;
    public string mediaUrl;
    public string logPath;
}

//声明方法
[DllImport("RTCWinWapper", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
internal static extern IntPtr rcrtc_create_engine_with_setup(IntPtr client, ref rtc_engine_setup setup);

//调用方法
internal RCRTCEngineWin(RCRTCEngineSetup setup)
{
    //转换成结构体
    rtc_engine_setup cobject;
    cobject.reconnectable = setup.IsReconnectable();
    if (setup.GetAudioSetup() != null)
    {
        rtc_audio_setup audio;
        audio.audioCodecType = (int)setup.GetAudioSetup().GetAudioCodecType();
        IntPtr ptr = Marshal.AllocHGlobal(Marshal.SizeOf(audio));
        Marshal.StructureToPtr(audio, ptr, true);
        cobject.audioSetup = ptr;
     }
     else
     {
         cobject.audioSetup = IntPtr.Zero;
     }
     //other paramers...
     
     //调用方法
     rtc_engine = NativeWin.rcrtc_create_engine_with_setup(im_client, ref cobject);
     if (cobject.audioSetup != IntPtr.Zero)
     {
        Marshal.FreeHGlobal(cobject.audioSetup);
     }
}

注意:
结构体中有Bool需要指定非托管类型为UnmanagedType.U1,可以告知运行时将字段作为 1 字节本机 bool 类型进行封送
结构体中有其他的类需要使用Marshal.AllocHGlobal在非托管内存中分配内存,使用结束后需手动调用Marshal.FreeHGlobal释放

//RTCInterface.cpp
#include "RTCInterface.h"

extern "C"
{
    //声明结构体
    typedef struct rtc_engine_setup
    {
        bool reconnectable;
        int statsReportInterval;
        bool enableSRTP;
        rtc_audio_setup* audioSetup;
        rtc_video_setup* videoSetup;
        const char* mediaUrl;
        const char* logPath;
    } rtc_engine_setup;

    RCRTCEngine *rcrtc_create_engine_with_setup(void *im_client, rtc_engine_setup *csetup)
    {
        RCRTCEngineSetup *setup = RCRTCEngineSetup::create();
        setup->setEnableSRTP(csetup->enableSRTP);
        //other params...
        RCRTCEngine *engine = RCRTCEngine::create(im_client, setup);
        return engine;
    }
}
  • 回调Unity方法
//C#中声明回调
public delegate void OnRoomJoinedDelegate(int code, String errMsg);

//实现回调方法
[MonoPInvokeCallback(typeof(OnRoomJoinedDelegate))]
private static void on_rtc_room_joined(int code, string message)
{
    //code...
}

//声明调用方法
[DllImport("RTCWinWapper", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
internal static extern void rcrtc_set_engine_listeners(OnRoomJoinedDelegate on_rtc_room_joined);

//设置回调 
NativeWin.rcrtc_set_engine_listeners(on_rtc_room_joined);
extern "C"
{
    //wrapper中声明回调
    typedef void (*on_rtc_room_joined)(int code, const char* message);
    
    //声明调用方法
    void rcrtc_set_engine_listeners(on_rtc_room_joined _on_rtc_room_joined)
    {
        //直接调用
        _on_rtc_room_joined(0, "error msg");
    }
}

2. Unity和iOS交互

Unity和iOS的交互和跟C++的交互过程基本一致,也是通过DllImport调用非托管函数

2.1 创建Wrapper层framework

新建Xcode framework项目,将使用的库拖入项目

新建一个交互类RongUnityRTC.mm

//RongUnityRTC.mm

extern "C" {
    //声明结构体
    typedef struct rtc_engine_setup {
      bool reconnectable;
      int statsReportInterval;
      bool enableSRTP;
      rtc_audio_setup *audioSetup;
      rtc_video_setup *videoSetup;
      const char *mediaUrl;
    } rtc_engine_setup;
    
    //声明创建方法
    CFTypeRef rtc_create_engine_with_setup(rtc_engine_setup *csetup) {
      RCRTCIWEngineSetup *setup = [[RCRTCIWEngineSetup alloc] init];
      setup.enableSRTP = csetup->enableSRTP;
      if (csetup->audioSetup != NULL) {
        RCRTCIWAudioSetup *audioSetup = [[RCRTCIWAudioSetup alloc] init];
        audioSetup.type = (RCRTCIWAudioCodecType)csetup->audioSetup->audioCodecType;
        audioSetup.mixOtherAppsAudio = csetup->audioSetup->mixOtherAppsAudio;
        setup.audioSetup = audioSetup;
      }
      //other params...
      engine = [RCRTCIWEngine create:setup];
      return CFBridgingRetain(engine);
    }
    
    //声明回调
    typedef void (*on_rtc_room_joined)(int code, const char* message);
    
    //声明调用方法
    void rcrtc_set_engine_listeners(on_rtc_room_joined _on_rtc_room_joined)
    {
        //直接调用
        _on_rtc_room_joined(0, "error msg");
    }
}

2.2 C#调用

Build wrapper项目,拷贝生成的framework以及依赖的frawork到Unity中,并设置好对应的架构

在Unity中新建NativeIOS.cs脚本

//NativeIOS.cs
namespace cn_rongcloud_rtc_unity
{
    //声明结构体
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    internal struct rtc_engine_setup
    {
        [MarshalAs(UnmanagedType.U1)] 
        public bool reconnectable;

        public int statsReportInterval;

        [MarshalAs(UnmanagedType.U1)] 
        public bool enableSRTP;
        public IntPtr audioSetup;
        public IntPtr videoSetup;
        public string mediaUrl;
        public string logPath;
    }

    internal class NativeIOS
    {
        [DllImport("__Internal", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
        internal static extern IntPtr rtc_create_engine_with_setup(ref rtc_engine_setup setup);
        //DllImport后面的名称必须为__Internal

        [DllImport("__Internal", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
        internal static extern void rcrtc_set_engine_listeners(OnRoomJoinedDelegate on_rtc_room_joined)
    }
}

//C#中声明回调
public delegate void OnRoomJoinedDelegate(int code, String errMsg);

//实现回调方法
[MonoPInvokeCallback(typeof(OnRoomJoinedDelegate))]
private static void on_rtc_room_joined(int code, string message)
{
    //code...
}

调用跟C++一致

//调用方法
internal RCRTCEngineIOS(RCRTCEngineSetup setup)
{
    //设置回调 
    NativeIOS.rcrtc_set_engine_listeners(on_rtc_room_joined);
    //转换成结构体
    rtc_engine_setup cobject;
    cobject.reconnectable = setup.IsReconnectable();
    if (setup.GetAudioSetup() != null)
    {
        rtc_audio_setup audio;
        audio.audioCodecType = (int)setup.GetAudioSetup().GetAudioCodecType();
        IntPtr ptr = Marshal.AllocHGlobal(Marshal.SizeOf(audio));
        Marshal.StructureToPtr(audio, ptr, true);
        cobject.audioSetup = ptr;
     }
     else
     {
         cobject.audioSetup = IntPtr.Zero;
     }
     //other paramers...
     
     //调用方法
     rtc_engine = NativeIOS.rcrtc_create_engine_with_setup(im_client, ref cobject);
     if (cobject.audioSetup != IntPtr.Zero)
     {
        Marshal.FreeHGlobal(cobject.audioSetup);
     }
}

2.3 其他情况

  • 对象有继承关系

c#为ios_class_warpper对象 , type 保存类名, obj 存真正的对象指针

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
internal struct ios_class_warpper
{
    public IntPtr obj;
    public string type;
}

internal static ios_class_warpper toMessageWapper(RCIMMessage message)
{
    ios_class_warpper message_wapper;
    if (message is RCIMUnknownMessage)
    {
        im_unknown_message cmessage = toUnknownMessage((RCIMUnknownMessage)message);
        IntPtr message_ptr = Marshal.AllocHGlobal(Marshal.SizeOf(cmessage));
        Marshal.StructureToPtr(cmessage, message_ptr, false);
        message_wapper.obj = message_ptr;
        message_wapper.type = "RCIMIWUnknownMessage";
    }
    else if (message is RCIMCustomMessage)
    {
        //...
    }
}

oc转换则是根据type初始化对应的对象

+ (RCIMIWMessage *)fromMessageWapper:(struct ios_class_warpper *)cmessage {
  if (cmessage == NULL || cmessage->obj == NULL || cmessage->type == NULL) {
    return nil;
  }
  RCIMIWMessage *message_wapper = nil;
  NSString *type = [NSString stringWithUTF8String:cmessage->type];
  if ([type isEqualToString:@"RCIMIWUnknownMessage"]) {
    message_wapper =
        [self fromUnknownMessage:(im_unknown_message *)cmessage->obj];
  }else if //...
 }

转换回来的过程则是相反的

  • 数组

c#转为 ios_c_list对象,list存数组指针,count存数组数量

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
internal struct ios_c_list
{
    public IntPtr list;
    public long count;
}

//转到ios
internal static IntPtr GetStructMapPointer<StructType>(ref List<StructType> clist)
{
     int size = Marshal.SizeOf(typeof(StructType));
     IntPtr list_ptr = Marshal.AllocHGlobal(clist.Count * size);
     for (int i = 0; i < clist.Count; i++)
     {
          Marshal.StructureToPtr(clist[i], list_ptr+size*i, false);
     }
     ios_c_list map;
     map.list = list_ptr;
     map.count = clist.Count;
     IntPtr map_ptr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(ios_c_list)));
     Marshal.StructureToPtr(map, map_ptr, false);
     return map_ptr;
}
//从ios转回
internal static List<StructType> GetObjectListByPtr<StructType>(IntPtr ptr)
{
    if (ptr == IntPtr.Zero)
    {
        return new List<StructType>();
    }
    ios_c_list clist = Marshal.PtrToStructure<ios_c_list>(ptr);
    StructType[] cobjects = new StructType[clist.count];
    for (uint i = 0; i < clist.count; ++i)
    {
       IntPtr item_ptr = new IntPtr(ptr.ToInt64() + Marshal.SizeOf(typeof(StructType)) * i);

       if ((item_ptr != null))
       {
           cobjects.Add((StructType)Marshal.PtrToStructure(item_ptr, typeof(StructType)));
       }
    }
    return new List<StructType>(cobjects);
}

oc根据count遍历

typedef struct ios_c_list {
  void *list;
  unsigned long count;
} ios_c_list;
//从c#
ios_class_warpper *messages_iter = c_list->list;
for (int i = 0; i < c_list->count; i++) {
    //   messages_iter ... 
    messages_iter++;
}
//转到c#
//NSArray<RCIMIWMessage *> *messages = ...
 ios_c_list *clist = (ios_c_list *)malloc(sizeof(ios_c_list));
 memset(clist, 0, sizeof(ios_c_list));
 ios_class_warpper *cmessages = (ios_class_warpper *)malloc(
          sizeof(ios_class_warpper) * messages.count);
 memset(cmessages, 0, sizeof(ios_class_warpper) * messages.count);
 ios_class_warpper *messages_iter = cmessages;
 for (int i = 0; i < messages.count; i++) {
     [RongUnityConvert makeMessageWapper:messages[i] to:messages_iter];
     messages_iter++;
 }
 clist->list = cmessages;
 clist->count = messages.count;
 return clist;
  • 字符串数组

和数组不同的点是字符串需要转换为指针,则结构就是指针数组

//转到ios
internal static IntPtr GetStringListPointer(List<string> clist)
{
      IntPtr[] ptrArray = new IntPtr[clist.Count];
      for (int i = 0; i < clist.Count; i++)
      {
          ptrArray[i] = Marshal.StringToHGlobalAnsi(clist[i]);
      }
      IntPtr list_ptr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(IntPtr)) * clist.Count);
      for (int j = 0; j < clist.Count; j++)
      {
          IntPtr item_ptr = new IntPtr(list_ptr.ToInt64() + Marshal.SizeOf(typeof(IntPtr)) * j);
          Marshal.StructureToPtr(ptrArray[j], item_ptr, false);
      }
      ios_c_list map;
      map.list = list_ptr;
      map.count = clist.Count;
      IntPtr result_ptr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(ios_c_list)));
      Marshal.StructureToPtr(map, result_ptr, false);
      return result_ptr;
}
//从ios转回      
internal static List<string> GetStringListByPtr(IntPtr ptr)
{
    if (ptr == IntPtr.Zero)
    {
        return new List<string>();
    }
    ios_c_list clist = Marshal.PtrToStructure<ios_c_list>(ptr);
    List<string> cobjects = new List<string>();
    for (int i = 0; i < clist.count; i++)
    {
        IntPtr item_ptr = new IntPtr(clist.list.ToInt64() + Marshal.SizeOf(typeof(IntPtr)) * i);
        IntPtr str_ptr = Marshal.PtrToStructure<IntPtr>(item_ptr);
        cobjects.Add(PtrToString(str_ptr));
    }
    return cobjects;
}
        
//需要注意释放
internal static void FreeStringListByPtr(IntPtr ptr)
{
     if (ptr == IntPtr.Zero)
     {
          return;
     }
     ios_c_list clist = Marshal.PtrToStructure<ios_c_list>(ptr);
     if (clist.list != IntPtr.Zero)
     {
         for (int i = 0; i < clist.count; i++)
         {
             IntPtr item_ptr = new IntPtr(clist.list.ToInt64() + Marshal.SizeOf(typeof(IntPtr)) * i);
             IntPtr str_ptr = Marshal.PtrToStructure<IntPtr>(item_ptr);
             Marshal.FreeHGlobal(str_ptr);
         }
         Marshal.FreeHGlobal(clist.list);
     }
}

iOS接收采用char*数组接收

//从c#
ios_c_list *clist = (ios_c_list *)cinfo->userIdList;
NSMutableArray<NSString *> *list =
        [[NSMutableArray alloc] initWithCapacity:clist->count];
char **iter = (char **)clist->list;
for (int i = 0; i < clist->count; i++) {
  if (*iter != NULL) {
     [list addObject:[NSString stringWithUTF8String:*iter]];
  }
  iter++;
}
//从ios转回
//NSArray<NSString *> *keys = ...
ios_c_list *clist = (ios_c_list *)malloc(sizeof(ios_c_list));
memset(clist, 0, sizeof(ios_c_list));
char **cstrings = (char **)malloc(keys.count * sizeof(char *));
char **keys_iter = cstrings;
for (NSUInteger i = 0; i < keys.count; i++) {
     NSString *string = keys[i];
     char *cstring = (char *)malloc(
            [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1);
     strncpy(cstring, [string UTF8String],
                [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1);
     *keys_iter = cstring;
     keys_iter++;
}
 clist->list = cstrings;
 clist->count = keys.count;
 return clist;
  • 字典

转换为以ios_c_map_item为元素的数组

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
internal struct ios_c_map_item
{
    public string key;
    public string value;
}

2.4 内存管理

调用完成后
c# 非托管的需要手动释放
C malloc的内存需要free