自己比較懶,有的時候想寫點東西,但由於文筆不行、技術不行也就沒有怎么寫。經常是用到什么、學習什么的時候,簡單寫點,權當是個學習筆記。上博客的次數也很少,有人給我留言也是沒有怎么及時的回復,深感抱歉!
在一些特殊的行業,比如我從事的GIS、地質行業,大部分軟件還是以C/S形式存在,軟件大多是產品來銷售。這些程序大部分是Cpp語言來編寫,一方面是考慮到效率問題,另一方面可能是因為歷史原因,創建者使用Cpp,后面接班人也就繼續使用。
但是使用Cpp去做項目的時候,又會倍感cpp的笨拙,做個界面非常費勁。所以如果能夠使用C#語言來研發,使用WinForm、WPF來做界面,世界就會美好很多。可是軟件產品生成的很多成果想要利用起來就比較困難,用C#重新寫一遍系統是一條很好的路,技術難度低,但是工作量大,后期維護也比較困難,最主要的是在項目實施過程中,時間不夠。另外一種思路就是對現有的Cpp系統進行包裝,直接用C#調用,這幾天比較了幾種方法,最后使用CLR對C++進行封裝了,可行性比較高。
一). PInvoke
不需要修改C++的DLL,直接在C#程序中把需要的接口引進進來即可。開始的時候感覺比較順暢,但是后面越搞越麻煩,在CSharp和Cpp之間傳遞的個數組、傳遞個類,需要編寫很多,並且MS上的文檔也看得暈乎乎的。最后就放棄了。
1. 首先創建一個C++的普通DLL,從歷史的DLL提取出自己想用的幾個接口,暴露出來。(例子來自網上查詢)
#define SOFTWRAPPER_API extern "C" __declspec(dllexport)
// 簡單的接口調用 SOFTWRAPPER_API int fnCppDll(int a, int b); //帶傳入數組: SOFTWRAPPER_API void testArray1(const int N, const int n[], int& Z); //帶傳出數組:C++不能直接傳出數組,只傳出數組指針, SOFTWRAPPER_API void testArray2(const int M, const int n[], int *N) ;
2. 對應的實現
// 1. 把數據底層封裝成一個全局函數,並導出
// 2. 編譯好之后拷貝到CSharp運行路徑
SOFTWRAPPER_API int fnCppDll(int a, int b)
{ return a+b; } SOFTWRAPPER_API void testArray1(const int N, const int n[], int& Z) { for (int i=0; i<N; i++) { Z+=n[i]; } } SOFTWRAPPER_API void testArray2(const int M, const int n[], int *N) { for (int i=0; i<M; i++) { N[i]=n[i]+10; } }
編譯成DLL之后,程序會把這幾個接口暴露出去,可以使用depends.exe查看導出情況。
3. C#中對其調用
在C#中也建立一個類,專門用來管理這些接口
namespace CSharp {
// 3. 定義一個CSharp類,wrap c++的接口,方便CSharp使用 class CppDll { [DllImport("CppDll.dll", CallingConvention = CallingConvention.Cdecl)] public static extern int fnCppDll(int x, int y); [DllImport("CppDll.dll", EntryPoint = "#2", CallingConvention = CallingConvention.Cdecl)] public static extern double testArray(int N, int[] n, ref int Z); [DllImport("CppDll.dll", EntryPoint = "#3", CallingConvention = CallingConvention.Cdecl)] public static extern void testOutArray(int N, int[] n, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] int[] Z); } }
這樣定義之后在其他的C#程序中即可直接調用了。其中"CppDll.dll"是DLL文件名,保證在C#的輸出目錄下。聲明的函數名稱要么和CPP中的一致,要么不通過名字而通過EntryPoint = "#3"這種方式指定。確保編譯器能找到接口
此種方法也可以實現類(C#中使用struct和IntPtr實現)的傳輸,但是需要在CSharp中重新編寫類,並且變量的順序、內存對齊方式可能都需要自己操心。尤其是我們類很多、類之間有嵌套等情況
MS網站有更詳細的解釋PInvoke和Marshal技術,可以根據這兩個關鍵字在網絡上好好查找下。
!!如何跟蹤調試:在C#工程屬性的Debug下面,check上Enable unmanaged code debugging即可。
二)SWIG
以前使用GDAL的時候,了解到GDAL可以在多中語言和多種平台下訪問,底層使用C++編寫,感覺很好。前幾天也順帶看了下http://www.swig.org/
他的包裝可以把類、接口很好的封裝起來,但是使用SWIG封裝的時候難度很大,要學習很多東西,網上有人評價,感覺是在學習一門新的語言,所以也沒有進行一步深入下去。
三)D-BUS
本是LINUX等系統上的技術,“dbus的是一個低延遲,低開銷,高可用性的ipc機制。是desktop-bus的簡稱”,因為我封裝的DLL主要是數據服務,所以當時考慮這條技術路線,使用DBus提供服務器端的數據服務,然后C#做為客戶端直接訪問服務獲取數據,感覺也是一條很好的路線。但是,感覺不是很正規,也放棄了。
關於SWIG和DBUS網上文檔比較多,感興趣的可以看看,多一種思路說不定什么時候可以用到:)
四)C++/CLI
在看Mashalling的時候,MS網站上到處都是托管代碼這樣的概念,於是深入了下,看了這個視頻之后感覺這個很不錯。
http://www.microsoft.com/uk/msdn/nuggets/nugget/184/Wrapping-Windows-APIs-with-CCLI.aspx
通過這視頻了解了如何封裝C++接口,同時也能看到高手是怎么編程的,受益匪淺
托管代碼簡單來說,就是在C++的基礎上進行擴展,使得可以調用.Net里面的類庫等東西。既然他是C++,那么他訪問C++的DLL或者其他C++庫,肯定是沒有問題的了。另一方面,他支持.Net類庫,那么就是說可以直接調用.Net里面的各種庫了,同時提供了C++類型和.Net類型之間的各種轉換。進一步,我們C#工程使用托管代碼組成的DLL便能很方便的訪問Native代碼了。托管代碼模塊起到了橋梁的作用,連接了Native C++和CSharp
1. 新建CLR的類庫:New Project下面選擇VC++里面的CLR,下面的Class Libary。創建好之后,確保General屬性頁的Common Language Runtime Support 屬性設置成了Common Language Runtime Support(/clr)
2. 編寫類
#pragma once using namespace System; using namespace System::Data; namespace CppSoftBridge { public ref class DataManger { public : bool Open(String^ path); property bool IsOpened{ bool get(){return m_isOpened;} }; property array<String^>^ AllData{ array<String^>^ get(); }; } }
其中class前面的ref說明這個類是托管的,要在C#中調用。同樣調用.Net類庫的類也要有所區分,就是這里的符號 ^,相當於一個托管類的引用。
cpp中的實現
using namespace System::Collections::Generic;
using namespace System::Runtime::InteropServices;
using namespace GPTSoftBridge;
array<String^>^ DataManger::AllData::get()
{
List<String^>^ list = gcnew List<String^>();
// TODO, 這里可以訪問Native C++里面的接口
list->Add("1");
list->Add("2");
return list->ToArray();
}
bool DataManger::Open(String^ path)
{
return true; // 根據自己需要進行實現即可
}
- 這里需要注意的是,這部分使用了.Net的東西,但是 還是C++的語法,
- 比如導入庫不是import,還是using語句。
- 包的組織不是System.Collections.Generic而是System::Collections::Generic;
- 申請對象托管代碼需要使用gcnew,而不是new。
- 申請之后如何判斷是否為空呢?使用 if(p == nullptr)。
- pin_ptr關鍵字能把托管引用轉換為原生指針。 如: pin_ptr<BYTE> pBytes = & byteArray[0];
其余的.Net的類庫就直接使用,C++的老代碼也照常用就好了。
3. C#中使用
C#使用托管代碼,和使用C#編寫的類庫方式一樣。直接添加工程引用以后就可以直接使用了。
DataManger dm = new DataManger(); if (!dm.Open("D:\\datal")) return; String[] wells = dm.AllData;
就這么多了,通過上面的步驟,我們能很方便、快捷的把原來的DLL封裝起來,供C#調用。以后再做項目,我們可以輕松的選擇C#,並且可以同時使用原有的C++代碼了。