實踐: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++一樣,記的清除申請的內存空間.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM