• 实践:C++平台迁移以及如何用C#做C++包装层


         在前面,我们看过OpenTK与MOgre,这二个项目都是C#项目,但是他的实现都是C++.他们简单来说就是一个包装层.常见的包装方式有二种,一种就是我们熟知的显式P/Invoke(DllImport),上面所说的OpenTK就是这种,还有一种就是C++ -> C++/CRL -> C#,这种也叫隐式P/Invoke,也有称C++ Interop,MOgre就是采用的这种方式.在这篇文章主要讲的就是隐式P/Invoke,具体相关操作请见http://msdn.microsoft.com/zh-cn/library/2x8kf7zx.aspx .

       先说下我的开发环境,用的是VS2013,如下问题有些版本可能不一样.

      从IOS环境下的代码,移到vs2013中,首先是如sprintf这种都会报错,因为vs2013会默认启用安全处理,sprintf的安全版本是sprintf_s,会加上缓冲区检查,还有一些如strcpy,strcpy_s,localtime,localtime_s.一般提示你启用安全版本,在对应的函数后添加_s,参数个数可能也会变,不过根据定义都容易改.

      然后就是socket里的一些原型参数的区别了,在IOS或是LINUX里,如关闭socket直接是close,在vs2013中是closesocket,ioctl与ioctlsocket也是如此,如果编译出错,又知道是socket里的函数,可以尝试在原函数后加上socket,然后是查询广播这个地方二个位置的写法差别比较大,在原IOS环境下ioctl传入SIOCGIFCONF命令,得到对应的ifconf数组,然后把ifconf数组每个值通过ioctl传入SIOCGIFBRDADDR命令得到对应的广播地址,我开始尝试把其中的一些ifconf结构引用进来,后来发现关联的比较多,现在想了一想,就是全添加进来,应该也是不行的.而在window环境下,直接使用WSAIoctl,传入命令SIO_GET_INTERFACE_LIST得到INTERFACE_INFO的结构体,里面就包含了所需要的广播地址,别的区别就不大了,相关的很多API是通用的.  

      到这里,差不多原来的IOS环境下的代码就转到VS2013中了,然后就是把这部分代码整合到原来的实时模式下,因为二个项目有些头文件本来就有重合的,或者重合的结构体,但是有些属性名不一样,二个相同功能的类,但是部分API与成员不一样的问题,在这我的办法是先把原IOS环境下的代码大致看一下,确定引用关系前后,就是没引用项目内别的文件与引用项目内别的文件最多的文件的.h与.cpp文件,先把引用关系最小的.h与点.cpp文件转移过去,然后确保能正确编译,然后再按顺序,一个个移过来,对于高手不清楚,但是对于我们这种小白,能保证不会因大堆错误而烦心,每转移一个文件成功后都会感觉到高兴.

      在这,实时模式和IOS下的黑盒模式代码都成功整合到VS2013中了,这里遇到一个问题.如下代码 所示.

      std::thread detectThread(&PApi::DetectProc, this);
      detectThread.detach();

      在原IOS环境下用的线程都是用的STL下的线程类,在主线程下,初始化一个类中包含如上STL线程类的初始化,会卡在初始化那里,根本进入不了detach这个方法,最开始是单独重建一个函数,包含上面的代码,在初始化后调用这个函数没有问题.但是这个功能本来应该是在初始化就启用的,再初始化后再加一函数,感觉不好,故把相关线程改成window api里的线程启用.问题解决,代码如下,需要把成员函数DetectProc改成静态成员函数.   
      DWORD threadId = 0;
      HANDLE threadHandle = CreateThread(0, 0, DetectProc, this, 0, &threadId);
      CloseHandle(threadHandle);
      threadHandle = INVALID_HANDLE_VALUE;  
      这是一个小插曲,问题解决后,我们按要求转移到C#平台.如最前面所说,采用的是隐式P/Invoke,我们来看看是如何实现的.  我们用C++实现全局钩子的同学们都知道,非托管C++DLL里的函数不同C#托管DLL,你在里面写个Public,引用这个DLL的项目就能看到,非托管C++动态链接库想让外面知道内部的API,需要添加关键字__declspec(dllexport).而不管是显示P/Invoke还是隐式P/Invoke引用非托管C++里的API,都需要相关API声明成__declspec(dllexport).  在这里,说下P/Invoke显示调用与隐式调用的用法的不同,显示调用一般是在C#中使用DLLImport特性包装非托管语言用关键字__declspec(dllexport)公开的API.而隐式调用一般会新建一个托管C++项目,也就是C++/CRL,托管C++和非托管C++共同点是.h头文件.cpp源文件分类与头文件引用这些,别的部分可能让我感觉更像是C#,通过托管C++生成的动态链接库就和我们C#生成DLL一样,直接能被托管项目引用调用类啥的,而托管C++也能像非托管C++一样,引用头文件直接调用相应API,不需要用DLLImport,但其实总的来说更麻烦些,不过能实现的功能也多些,有些C++好用的方法在C#里没有,如memcyp,一些针对指针的操作也和C++一样方便.  __declspec(dllexport)是我们暴露给外部的DLL生成API所需的,对应我们用P/Invoke引用这些公开的API时,最好在对应的方法上给出关键字__declspec(dllimport),虽然导出函数不是必要的,但是如果是DLL中的变量,这个是必需的,并且编译器也能针对关键字做特定优化.下面是一段常见代码.
     1 #ifdef TRADITIONALDLL_EXPORTS
     2    #define TRADITIONALDLL_API __declspec(dllexport)
     3 #else
     4    #define TRADITIONALDLL_API __declspec(dllimport)
     5 #endif
     6 
     7 extern "C" {
     8    TRADITIONALDLL_API double GetDistance(Location, Location);
     9    TRADITIONALDLL_API void InitLocation(Location*);
    10 }
    View Code

      如上定义一个宏定义,在导出的C++动态链接库中,可以选择项目属性里添加预处理器定义TRADITIONALDLL_EXPORTS,也或者是在引用这个文件加上.而在引用这个动态链接库不做处理.

      先看一个普通的函数从非托管C++到C++/CRL到C#相应流程,这个函数是传入一个设备ID,得到设备的所有工程,以及默认的工程ID.

     1  // C++
     2 TRADITIONALDLL_API int GetTestList(const unsigned long deviceId, char** confs, int& count, int& defaultID);
     3 TRADITIONALDLL_API  int GetTestList(const unsigned long deviceId, char** confs, int& count, int& defaultID)
     4 {
     5      auto tests = PApi->Spider_GetTestList();
     6      count = tests->count;
     7      if (count > 0)
     8      {
     9           auto testNames = new char[32 * count];
    10           memset(testNames, 0, 32 * count);
    11           for (int i = 0; i < count; i++)
    12           {
    13                memcpy(testNames + i * 32, tests->driveArray[i].TestName, 32);
    14           }
    15           *confs = testNames;
    16           defaultID = PApi->curModule->nDefaultID;
    17           return FUNC_SUCCESS;
    18      }
    19      spiderAPI->errorStr = NotConnecteDev;
    20      return 0;
    21 }
    22 // managed C++
    23 bool GetTestList(const unsigned long deviceId, [Out]List <String^>^% testList, [Out]int% defalutID);
    24 bool DeviceController::GetTestList(const unsigned long deviceId, [Out]List <String^>^% testList, [Out]int% defalutID)
    25 {
    26      testList = gcnew List<String^>();
    27      char *nameBuffer = NULL;
    28      int testCount = 0;
    29      int dID = 0;
    30      ::GetTestList(deviceId, &nameBuffer, testCount, dID);
    31      defalutID = dID;
    32      if (nameBuffer != NULL && testCount > 0)
    33      {
    34           char testName[32];
    35           memset(testName, 0, 32);
    36           for (int index = 0; index < testCount; ++index)
    37           {
    38                memcpy(testName, nameBuffer + index * 32, 32);
    39                String^ str = gcnew String(testName);
    40                testList->Add(str);
    41           }
    42           return true;
    43      }
    44      return false;
    45 }
    46 //C#
    47 bool result = DeviceController.Instance.GetTestList(Device.Id, out testNames, out defaultID);
    View Code

         基本的传递如上,但是现在要求C#实时刷新设备转过来的数据,简单来说,就是C++里socket接收线程收到设备发送的数据,需要通知C#界面刷新.看需求,C#里的事件就能满足,但是是C++发送的消息,在这我们根据C++里的回调函数与托管代码里的事件结合来完成,去掉一些不必要的代码,主要过程如下.

     1 // C++
     2 typedef void (__stdcall *OnDataMessageRev)(const unsigned long deviceId,  char* data, const int eventId,const int p0, const int p1,const int p2);
     3 
     4 class Module
     5 {
     6      OnDataMessageRev onDataRev;
     7      void didDataReceived();
     8      void SetDataMessageCallback(OnDataMessageRev callback);
     9 }
    10 void Module::SetDataMessageCallback(OnDataMessageRev callback)
    11 {
    12      onDataRev = callback;
    13 }
    14 void Module::didDataReceived()
    15 {    
    16     switch (dataMsg.Msg.nEventID)
    17     {    
    18         case DSP_DISPNEXT_OK:    
    19         {
    20              if (onDataRev)    
    21                   onDataRev(this->deviceId, dataMsg.Data, dataMsg.Msg.nEventID, dataMsg.Msg.nParameters0, dataMsg.Msg.nParameters1, dataMsg.Msg.nParameters2);
    22          }
    23         break;
    24         //...
    25     }  
    26 }
    27 DEVICEAPI_API void SetDataMessageCallback(OnDataMessageRev callback);
    28 DEVICEAPI_API void SetDataMessageCallback(OnDataMessageRev callback)
    29 {
    30      model.SetDataMessageCallback(callback);
    31 }
    32 // managed C++
    33 public delegate void DeviceDataMessageHandler(const unsigned long deviceId, const array<Byte>^ data, const int eventId, const int p0, const int p1, const int p2);
    34 public delegate void DeviceDataCallback(const unsigned long deviceId, char* data, const int eventId, const int p0, const int p1, const int p2);
    35 public ref class DeviceController
    36 {
    37     DeviceDataCallback^ dataCallback;
    38     DeviceDataMessageHandler^ onDeviceDataReceived;
    39     event DeviceDataMessageHandler^ DeviceDataReceived
    40      {
    41           void add(DeviceDataMessageHandler^ h)
    42           {
    43                onDeviceDataReceived += h;
    44           }
    45           void remove(DeviceDataMessageHandler^ h)
    46           {
    47                onDeviceDataReceived -= h;
    48           }
    49      }
    50 
    51     DeviceController::DeviceController()
    52      {
    53           dataCallback = gcnew DeviceDataCallback(&(DeviceController::DataReceivedCallback));
    54           IntPtr ptrData = Marshal::GetFunctionPointerForDelegate(dataCallback);
    55 
    56           ::SetDataMessageCallback(static_cast<OnDataMessageRev>(ptrData.ToPointer()));
    57           GC::KeepAlive(dataCallback);
    58      }
    59 
    60 void OnDeviceDataReceived(const unsigned long deviceId, const array<Byte>^ data, const int eventId, const int p0, const int p1, const int p2)
    61 {
    62      DeviceDataMessageHandler^ handler = onDeviceDataReceived;
    63      if (handler != nullptr)
    64      {
    65           handler(deviceId, data, eventId, p0, p1, p2);
    66      }
    67 }
    68 }
    69 
    70 //C#
    71 
    72 DeviceController.Instance.DeviceDataReceived += Instance_DeviceDataReceived;
    73 
    74 T ByteArrayToStructure<T>(byte[] bytes, IntPtr pin, int offset) where T : struct
    75 {
    76     try
    77     {
    78         return (T)Marshal.PtrToStructure(pin + offset, typeof(T));
    79     }
    80     catch (Exception e)
    81     {
    82         return default(T);
    83     }
    84 }
    85 private void Instance_DeviceDataReceived(uint deviceId, byte[] data, int eventId, int p0, int p1, int p2)
    86 {
    87      GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned);
    88         IntPtr pin = handle.AddrOfPinnedObject();
    89      int nCheckNum = ByteArrayToStructure<int>(data, pin, offset);
    90      DISPLAYPARAMS displayParams = ByteArrayToStructure<DISPLAYPARAMS>(data, pin, offset);
    91      VCSParamsDSP vcsPar = ByteArrayToStructure<VCSParamsDSP>(data, pin, offset);
    92      handle.Free();
    93 
    94 }

    View Code

      C++里的memcyp确实很好用,上段代码中,ByteArrayToStructure也能实现如memcyp一样的功能,先用GCHandle.Alloc选择Pinned生成CG不能回改的内存区域,就和C++申请内存一样,然后根据偏移量offset,把对应的字节转成我们需要的数据.C++里的char和C#里的byte是一样的,都是一个字节,这里不要搞错了,也和C++一样,记的清除申请的内存空间.

  • 相关阅读:
    Asp.net读取AD域信息的方法(一)
    登录时记住用户名和密码及cookie案例应用
    Web项目,要求:保存用户名和密码在Cookie中,下次登录不再重新输入
    ASP.net 学习路线(详细)
    vs调试技巧(二)
    vs断点调试技巧(一)
    flexPaper +swftools实现文档在线阅读
    OC 类 的声明
    OC extern和函数
    OC extern和变量
  • 原文地址:https://www.cnblogs.com/zhouxin/p/3738708.html
Copyright © 2020-2023  润新知