SWIG包装器使用指南——(二)C++代码的包装

时间:2021-06-27 01:26:07


文章目录

  • 一、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回收代理类时,其流程如下:

  1. var v=new Vector(),此时构造出来了两个对象,占用两块内存区域。其中一块内存就是传统C#对象v占用的内存。另外通过demoModulePINVOKE.new_Vector()也分配一块C++内存,并返回其指针赋值到swigCPtr,此内存藏在了对象v背后。
  2. 构造完成之后,swigCPtr就有了值,存放c++内存的地址。swigCMemOwn为true,表示对象v拥有这块c++内存。
  3. 当对象v不在被需要,GC进行回收时,就调用到了Dispose方法上。此时检查swigCMemOwn若为true,则表示v背后的那块C++内存归它所有,需要调用demoMudulePINVOKE.delete_Vector()先将其释放掉,然后再释放v自身内存。

注:

  1. 当我们手动new一个对象时,其swigCMemOwn标识为true。
  2. 当我们接收一个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

调用过程及内存分配如下:

SWIG包装器使用指南——(二)C++代码的包装


当第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++ intlong, SWIG都会将其映射为c#的int。所以生成的C#代理类就会出现二义性问题。在我们使用swig命令行进行生成时也会有对应提示:

SWIG包装器使用指南——(二)C++代码的包装


告知我们第二个函数被忽略了。

如果这种默认行为不符合我们的期望,则可以使用%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
	{
	} 
}