文章目录
- 一、SWIG支持的C++特性
- 1.1 支持的特性
- 1.2 不支持的特性
- 二、内存管理
- 2.1 管理机制
- 2.2 该机制的潜在问题
- 三、包装引用和指针
- 四、包装带有参数默认值的函数
- 五、函数重载引起的二义性问题
- 六、C++模板
- 七、命名空间
一、SWIG支持的C++特性
1.1 支持的特性
- 类
- 构造函数与析构函数
- 虚函数
- 继承(包括多继承)
- 静态函数
- 函数重载与标准运算符重载
- 引用、指针
- 模板
- 命名空间
- 参数默认值
- 智能指针
1.2 不支持的特性
- 特殊运算符的重载(如 new\delete)
二、内存管理
2.1 管理机制
这里通过分析生成的C#代理类的方式来理解,C++与C#互操作时的内存管理。
假如我们有如下的C++函数:
class Vector
{
public:
double x,y,z;
void print();
};
生成的C#代理类缩写如下:
public class Vector : IDisposable {
private HandleRef swigCPtr;
protected bool swigCMemOwn;
public Vector() : this(demoModulePINVOKE.new_Vector(), true) {
}
internal Vector(IntPtr cPtr, bool cMemoryOwn) {
swigCMemOwn = cMemoryOwn;
swigCPtr = new HandleRef(this, cPtr);
}
protected virtual void Dispose(bool disposing) {
lock(this) {
if (swigCMemOwn) {
swigCMemOwn = false;
demoModulePINVOKE.delete_Vector(swigCPtr);
}
}
}
public double x {
set {
demoModulePINVOKE.Vector_x_set(swigCPtr, value);
}
get {
return demoModulePINVOKE.Vector_x_get(swigCPtr); }
}
public void print() {
demoModulePINVOKE.Vector_print(swigCPtr);
}
}
可以看到Vector
代理类里除了原有的swigCPtr
外,还多出了两个成员:x
属性和print()
方法,这两个分别对应的是原C++代码里的同名成员。除此之外还有一个最重要的Dispose
方法,用来回收分配的C++内存。
当我们手动new
一个代理类和GC回收代理类时,其流程如下:
-
var v=new Vector()
,此时构造出来了两个对象,占用两块内存区域。其中一块内存就是传统C#对象v
占用的内存。另外通过demoModulePINVOKE.new_Vector()
也分配一块C++内存,并返回其指针赋值到swigCPtr
,此内存藏在了对象v
背后。 - 构造完成之后,
swigCPtr
就有了值,存放c++内存的地址。swigCMemOwn
为true,表示对象v
拥有这块c++内存。 - 当对象
v
不在被需要,GC进行回收时,就调用到了Dispose
方法上。此时检查swigCMemOwn
若为true,则表示v
背后的那块C++内存归它所有,需要调用demoMudulePINVOKE.delete_Vector()
先将其释放掉,然后再释放v
自身内存。
注:
- 当我们手动new一个对象时,其swigCMemOwn标识为true。
- 当我们接收一个C++函数返回的对象时,其swigCMemOwn标识为false。
2.2 该机制的潜在问题
考虑以下的C++代码:
class Foo
{
public:
int bar(int x);
int x;
};
class Spam
{
public:
Foo* value;
};
包装之后,我们使用如下的C#代码进行调用:
var f = new Foo(); // 1
var s = new Spam(); // 2
s.value = f; // 3
var g = s.value; // 4
g = null; // 5
f.Dispose(); // 6
调用过程及内存分配如下:
当第6步执行完之后,s.value
就变成了一个野指针,其指向的内存其实已被释放。所以SWIG自带的内存管理机制过于简单有一定的不足,不一定是我们调用时所期待的结果。这一点需要特别注意。
三、包装引用和指针
SWIG支持C++的引用类型,正如完美支持指针类型一样。SWIG将引用也包装成指针调用。
对于如下的C++代码:
class Foo
{
public:
int bar(int &x);
void bar2(const int& x);
int x;
};
生成的包装代码为:
int CSharp_DemoNamespace_Foo_bar(void * jarg1, void * jarg2) {
Foo * arg1 = (Foo *)jarg1;
int * arg2 = (int *)jarg2;
int result = (int)(arg1)->bar(*arg2); //取指针指向的值
int jresult = result;
return jresult;
}
void CSharp_DemoNamespace_Foo_bar2(void * jarg1, int jarg2) {
Foo * arg1 = (Foo *)jarg1;
int temp2 = (int)jarg2;
int * arg2 = &temp2;
(arg1)->bar2((int const &)*arg2);
}
对于const int &
来说,SWIG知道我们不会更改其值,所以可以将其直接映射为c#的int
,这二者调用方式区别如下:
var f = new Foo();
f.bar(new DemoNamespace.SWIGTYPE_p_int()); // 被包了一层,无法直接调用,需做tyepmap处理
f.bar2(20); //直接传20
四、包装带有参数默认值的函数
如下的c++参数默认值函数:
class Foo
{
public:
void bar(int a,int b=2,int c=3);
};
SWIG其实会将其包装为三个不同名的函数:
void CSharp_DemoNamespace_Foo_bar__SWIG_0(void * jarg1, int jarg2, int jarg3, int jarg4) {
Foo* arg1 = (Foo *)jarg1;
int arg2 = (int)jarg2;
int arg3 = (int)jarg3;
int arg4 = (int)jarg4;
(arg1)->bar(arg2,arg3,arg4);
}
void CSharp_DemoNamespace_Foo_bar__SWIG_1(void * jarg1, int jarg2, int jarg3) {
Foo *arg1 = (Foo*)jarg1;
int arg2 = (int)jarg2;
int arg3 = (int)jarg3;
(arg1)->bar(arg2,arg3);
}
void CSharp_DemoNamespace_Foo_bar__SWIG_2(void * jarg1, int jarg2) {
Foo *arg1 = (Foo*)jarg1;
int arg2 = (int)jarg2;
(arg1)->bar(arg2);
}
而对应的C#代理类则为三个重载,调用到包装的三个函数上:
public class Foo {
public void bar(int a, int b, int c) {
demoModulePINVOKE.Foo_bar__SWIG_0(swigCPtr, a, b, c);
}
public void bar(int a, int b) {
demoModulePINVOKE.Foo_bar__SWIG_1(swigCPtr, a, b);
}
public void bar(int a) {
demoModulePINVOKE.Foo_bar__SWIG_2(swigCPtr, a);
}
}
五、函数重载引起的二义性问题
class Foo
{
public:
void bar(int a);
void bar(long a);
};
包装上述的C++代码时,因C++ int
和long
, SWIG都会将其映射为c#的int
。所以生成的C#代理类就会出现二义性问题。在我们使用swig命令行进行生成时也会有对应提示:
告知我们第二个函数被忽略了。
如果这种默认行为不符合我们的期望,则可以使用%ignore指令忽略第一个函数,或者使用%rename:
%ignore bar(int); // 忽略第一个
%rename(“longBar”) bar(long); // 重命名第二个
其它也会引起二义性的函数类型有:
- 整型,如int , long , short
- 浮点数,如double , float
- 指针、引用和实例,如Foo* , Foo& , Foo
- 指针和数组,如Foo* , Foo [4]
- 参数默认值,如foo(int a,int b) , foo(int a,int b=1)
六、C++模板
SWIG也完整支持,不过使用时需要指定一种模板类型. 无法直接映射为C#的泛型。
如以下的c++代码:
template <typename T>
class BaseDAL
{
public:
virtual bool Add(T t) = 0;
};
class OperateLogDAL:public BaseDAL<OperateLog>
{
public:
bool Add(OperateLog t) override;
};
在编写.i
文件时,我们要手动指定一个具体的模板实例:
%include "Include/BaseDAL.h";
%include "Include/OperateLog.h“
// 模板指定为OperateLog类型,生成的C#代理类名字设置为BaseOperateLogDAL
%template(BaseOperateLogDAL) BaseDAL<OperateLog>;
%include "Include/OperateLogDAL.h";
就可以生成如下的C#代理类结构:
public class BaseOperateLogDAL
{
}
public class OperateLogDAL : BaseOperateLogDAL
{
}
对于不包含继承关系的模板来说更简单:
template <typename T>
class List
{
public:
virtual bool Add(T t) = 0;
};
// 定义.i
%template(IntList) List<int>;
%template(LonglongList) List<long long>;
%template(ListInt) List<int>; //错误,重复定义,虽然名字不一样。
函数模板与类模板的使用方法一致,不做赘述。
七、命名空间
SWIG支持C++的命名空间,但是默认会忽略。可通过%nspace指令开启。
namespace FooSpace
{
class Foo
{
public:
void bar(int a);
};
}
class Spam
{
public:
FooSpace::Foo* value;
FooSpace::Foo* getFoo();
};
.i
文件:
%{
#include "Foo.h"
%}
%nspace FooSpace::Foo;
%include "Foo.h"
生成了带有命名空间的C#代理类:
namespace DemoNamespace.FooSpace
{
public class Foo
{
}
}