有的时候想写点东西,但由于文笔不行、技术不行也就没有怎么写。经常是用到什么、学习什么的时候,简单写点,权当是个学习笔记。上博客的次数也很少,有人给我留言也是没有怎么及时的回复,深感抱歉!
在一些特殊的行业,比如我从事的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提取出自己想用的几个接口,暴露出来。(例子来自网上查询)
1
2
3
4
5
6
|
#define SOFTWRAPPER_API extern "C" __declspec(dllexport) <BR><BR>// 简单的接口调用
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运行路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<SPAN style=
"LINE-HEIGHT: 1.5"
>SOFTWRAPPER_API
int
fnCppDll(
int
a,
int
b)</SPAN><BR>{
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#中也建立一个类,专门用来管理这些接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
namespace
CSharp
{<BR>
// 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. 编写类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
#pragma once
using
namespace
System;
<SPAN style=
"COLOR: #0000ff"
>
using
namespace
System::Data;</SPAN>
namespace
CppSoftBridge {
public
ref
class
DataManger
{
public
:
bool
Open(<SPAN style=
"COLOR: #0000ff"
>String^</SPAN> path);
property
bool
IsOpened{
bool
get(){
return
m_isOpened;}
};
property array<String^>^ AllData{
array<String^>^ get();
};
}
}
|
其中class前面的ref说明这个类是托管的,要在C#中调用。同样调用.Net类库的类也要有所区分,就是这里的符号 ^,相当于一个托管类的引用。
cpp中的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
using
namespace
System::Collections::Generic;
using
namespace
System::Runtime::InteropServices;
using
namespace
GPTSoftBridge;
array<String^>^ DataManger::AllData::get()
{
List<String^>^ list = <SPAN style=
"COLOR: #0000ff"
>gcnew </SPAN>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#编写的类库方式一样。直接添加工程引用以后就可以直接使用了。
1
2
3
4
|
DataManger dm =
new
DataManger();
if
(!dm.Open(
"D:\\datal"
))
return
;
String[] wells = dm.AllData;
|
就这么多了,通过上面的步骤,我们能很方便、快捷的把原来的DLL封装起来,供C#调用。以后再做项目,我们可以轻松的选择C#,并且可以同时使用原有的C++代码了。