SWIG高级应用之智能指针
SWIG
作为C++到其他语言包装的万能胶水语言,无疑是非常强大的,对于C++研发人员来说, SWIG
的使用做是作为必备技能来的,关于SWIG的基础知识我在前面的文章《SWIG与JAVA的交互指南》也详细的讲过了,这里不在叙述(如果还不清楚的自己去官网查资料)。在今天的这篇文章中,我们将学习一些SWIG的高级应用,这些应用可以让基于C++底层的其他高级语言二次开发变的与原生语言交互一致的用户体验。
ps:本文例子是基于C++包装C#,其他语言基本类似,更多的还是掌握其语言的特性。
在本专题中我们将会学到以下内容
- 如何改写swig模板实现自定义智能指针的封装问题(确保在C#只有智能指针和普通指针是一个类对象)
- 将自定义的字符串类转为平台默认的字符串类如(QString 转C#的String对象)
- 对于自己写的迭代类(如
vector
等)如何在C#中可以用如、List foreach(var item:collections)
的使用方式来遍历对象 - 解决类的向下转换问题(默认不支持类的向下转换,如C#的 as功能)
- 解决C++类在C#中继承的对象的生命周期问题
本章要解决自定义智能指针的封装问题。
改写SWIG智能指针
开始使用swig的时候很多人可能会有个疑惑,C++ new出来的对象是在堆上的,需要自己的手动调用释放才能将内存释放。但C#的生命周期管理是由C#的垃圾回收机制决定的。对于写C#的人而言,肯定不希望自己手动的去管理内存,太过麻烦,而且与C#的代码风格也是不一致的。
//以下是开始写代码经常遇到的问题,NEW的对象要手动去释放内存,对于C++来可说,可能还好一些,但是一旦封装到C#就成了一个大问题。
//一个加法类;
class AddCls
{
public:
int add(int x, int y)
{
return x + y;
}
};
//计算类;
class Calculate
{
public:
AddCls* addCls()
{
return _add_class;
}
void setAddCls(AddCls* cls)
{
__add_class.reset(cls);
}
private:
AddCls* _add_class;
};
//C#类
Calculate cal=new Calculate();
cal.addCls().add(3,4);
//?? 什么时候释放,不释放就内存泄露,我是不是该加一个函数去释放呢?
方案一:使用template模板化智能指针
使用c++11的 std::shared_ptr
智能指针类来解决,在引用计数为0的时候就可以自动释放内存了。具体分为以下四步
- 在代码的所需要的类的头文件中加入以下内容如:
#ifdef SWIG
%template(AddClsPtr)std::shared_ptr;
#endif
- 改写需要使用这个类的地方的代码
class Calculate
{
public:
std::shared_ptr addCls()
{
return _add_class;
}
void setAddCls(AddCls* cls)
{
_add_class.reset(cls);
}
private:
std::shared_ptr _add_class;//使用智能指针
};
- 使用SWIG生成C#代码
发现有两个类似的C#文件(AddCls.cs 和AddClsPtr.cs) 一个是普通类生成的,一个是模板化指针类生成的。在C#使用是这样
Calculate cal=new Calculate();
AddClsPtr cls=new AddClsPtr();
//setAddCls 使用的是AddCls类,要使用智能指针的 get函数获取另一个类.
cal.setAddCls(cls.get());
看这个简单的例子好像还行,不是什么大的问题,但是对于一个大的系统而言,就算是有了代码规范,但是每个人的使用风格还是有一点差异的。如果使用方案1的方案,那这个友好度太差了,瞬间不想用了。
方案二 使用智能指针的SWIG模板
- 在SWIG的i文件中,添加shared_ptr的swig头文件如
%include std::shared_ptr.i
- 在代码的所需要的类的头文件中加入以下内容如:
#ifdef SWIG
%std::shared_ptr(AddCls); //注意与上面的不同,可以识别这个%std::shared_ptr关键字是因为加了头文件%include std::shared_ptr.i
#endif
- Calculate这个类还是方案一的,通用SWIG生成C#文件
这个使用生成的文件只有一个文件AddCls.cs ,这个其实是智能指针类的封装(不是普通类的封装),原理是swig在对接口进行封装时都使用智能指针封装了一层出去。有兴趣的可以看看生成的cxx代码,可以发现所有关于这个AddCls作为参数输出都是std::shared_ptr(AddCls)。(相当于new了一个std::shared_ptr(AddCls)类,但是将ADDCls的指针传了进去。
- C#中的使用
Calculate cal=new Calculate();
AddCls cls=new AddCls();
cal.setAddCls(cls);
方案二算是比较完美了,看起来C#的已经没有什么特别的地方了,不需要调用释放,也不需要在两个类中切换。
方案三 自己写智能指针模板
方案二使用C++11自带 std::shared_ptr
,但是有些场景还是希望使用自定义的智能指针。
- std::shared_ptr 是非侵入式指针,对于很多应用场景,还是使用侵入式的智能指针比较方便(可以普通指针和智能指针互用,主要是在某些地方可以确定智能指针的生命周期肯定存在时)
- 很多现成的框架时有自己的智能指针类的,改用shared_ptr就不太现实了.
- C++很喜欢自己造轮子,金窝银窝都不如自己的好。
编写SWIG的智能指针模板(假设定义的智能指针类为 Auto_Object)
主要还是参考
std::shared_ptr.i
这个头文件(可以在swig的安装包的LIB目录中找到,代码太长不贴出来了,有需要的可以联系我),这里做一个改造说明,以下是AutoObject.i文件的内容的部分说明
std::shared_ptr.i文件的代码使用说明,以下的example代码是基于std::shared_ptr.i头文件改造的
## 这个是我们在swig的i文件声明为某个类为智能指针的宏如 %Auto_Object(AddCls)
examples:
%define %Auto_Object(TYPE...)
%feature("autoptr", noblock=1) TYPE { Auto_Object< TYPE > }
SWIG_AUTOOBJECT_PTR_TYPEMAPS(SWIGEMPTYHACK, TYPE)
SWIG_AUTOOBJECT_PTR_TYPEMAPS(const, TYPE)
%enddef
## 关于typemaps的使用说明
//使用 %typemap 指令来指示转换代码行为,in表示从目标语言到C/C++,out表示从C/C++到目标语言
//主要作用是用来定义C/C++层包装代码的生成行为,可以定义各类类型在C++与C#的转换行为。
%typemap(in) 输入参数转换
%typemap(typecheck) 输入参数类型检查重载方法中使用的类型
%typemap(argout) 输出参数处理
%typemap(check) 输入参数值检查
%typemap(arginit) 输入参数初始化
%typemap(out) 函数返回值转换
%typemap(ret) 返回值资源管理(“ret”类型映射)
%typemap(newfree) 新分配对象的资源管理(“newfree”typemap)
examples:
//代表的是封装C++的代码到C的代码的使用,$input代表的是当前输入参数,$1当前参数的转换结果 TYPE是指类的类型如AutoCls
%typemap(in, canthrow=1) CONST TYPE & %{
$1 = ($1_ltype)(((Auto_Object< CONST TYPE > *)$input) ? ((Auto_Object< CONST TYPE > *)$input)->get() : 0);
if (!$1) {
SWIG_CSharpSetPendingExceptionArgument(SWIG_CSharpArgumentNullException, "$1_type reference is null", 0);
return $null;
} %}
//csin是C#这边封装的代码,下面代码是指参数如果为Auto_Object,Auto_Object*,Auto_Object*,Auto_Object*&
//翻译出来的C#代码是 AutoCls.getCPtr(类的参数名);
%typemap(csin) Auto_Object< CONST TYPE >,
Auto_Object< CONST TYPE > &,
Auto_Object< CONST TYPE > *,
Auto_Object< CONST TYPE > *& "$typemap(cstype, TYPE).getCPtr($csinput)"
## TypesMap的参数说明,可参考官方文档
* csdirctorin csdirctorout directorout dierectorin 回调类参数处理 cs为C#这边代码,没有这个为C++这边处理代码
* csvarout csvarin varout,varin 全局参数
* in out csin csout 函数参数
## 智能指针类在C#的代理类
// 智能指针在C#中的代理类,这个是基类
%typemap(csbody) TYPE %{
private global::System.Runtime.InteropServices.HandleRef swigCPtr;
protected bool swigCMemOwnBase;
public $csclassname(global::System.IntPtr cPtr, bool cMemoryOwn) {
swigCMemOwnBase = cMemoryOwn;
swigCPtr = new global::System.Runtime.InteropServices.HandleRef(this, cPtr);
}
public static global::System.Runtime.InteropServices.HandleRef getCPtr($csclassname obj) {
return (obj == null) ? new global::System.Runtime.InteropServices.HandleRef(null, global::System.IntPtr.Zero) : obj.swigCPtr;
}
%}
// 智能指针在C#中的代理类,这个是继承的类
%typemap(csbody_derived) TYPE %{
//这个里面可以写一些C#的函数或者代码;
private global::System.Runtime.InteropServices.HandleRef swigCPtr;
public $csclassname(global::System.IntPtr cPtr, bool cMemoryOwn) : base($imclassname.$csclazznameSWIGSmartPtrUpcast(cPtr), true) {
swigCMemOwnDerived = cMemoryOwn;
swigCPtr = new global::System.Runtime.InteropServices.HandleRef(this, cPtr);
}
public static global::System.Runtime.InteropServices.HandleRef getCPtr($csclassname obj) {
return (obj == null) ? new global::System.Runtime.InteropServices.HandleRef(null, global::System.IntPtr.Zero) : obj.swigCPtr;
}
%}
侵入式智能指针模板
SWIG模板的使用对智能指针的实现没有特殊的要求,大家使用自己的智能指针类即可,类的名字必须与自己编写的i文件里的智能指针类一致即可,也可参考网上的实现 https://blog.csdn.net/jiange_zh/article/details/52512337
notes:SWIG3.10的版本在处理回调类的智能指针的时候是有问题,需要使用SWIG4.0以上的版本。
使用自定义智能指针模板
- swig的文件中添加包含文件如
%Auto_Object(ADDCls)
- 在需要使用的头文件写上宏
#ifdef SWIG
%Auto_Object;
#endif
- C#使用与方案二一样的方式即可