SWIG包装器使用指南——(四)C#使用SWIG简介与实践
标签: SWIG包装器使用指南——(四)C#使用SWIG简介与实践 JavaScript博客 51CTO博客
2023-04-09 18:23:51 161浏览
一、简介
- 二、全局函数、变量、常量
- 三、继承
- 四、传递指针、引用、数组与值
- 五、基本类型的指针与引用
- 六、基本类型的数组
- 七、基本类型的默认map规则
- 八、常用的typemap方法
- 九、代码插入
- 十、实践
- 10.1 如何映射Foo*&到ref Foo
- 10.2 映射Foo* array数组
- 10.3 如何将char**映射为string[]
- 10.4 如何映射C++的容器(vector\pair)
- 10.5 如何映射函数指针
- 十一、要点总结
一、简介
SWIG如何实现让C#方便的调用C++函数的?
其实原理并不负责,仍然使用C#的互操作技术P/Invoke实现,只不过SWIG对C++代码进行的包装,使开发者更易于调用。
生成C#代码时注意可选添加几个额外的命令行选项:
-
-dllimport
:指定P/Invoke时要调用的dll名称 -
-namespace
:设置C#的命名空间 -
-outfile
:将所有生成的C#代码放到一个cs文件中
二、全局函数、变量、常量
int a = 0;
void funA();
#define PI 3.14
class Foo
{
public:
int * bar(int* items);
};
因C#里没有全局成员的概念,所以SWIG都把它们生产到了与%module同名的类里,且都是静态成员,可直接访问。
三、继承
class Foo
{
};
class Bar : public Foo
{
};
以上的c++代码SWIG可以完整的生成为C#代码,并且保留继承关系:
public class Foo
{
};
pulic class Bar : Foo
{
};
注意:因C#里没有多继承,所以如果C++里的代码是多继承的,则生成的C#代码只会继承第一个父类。
四、传递指针、引用、数组与值
class Foo
{
public:
void spam1(Foo* x); // 传指针
void spam2(Foo& x); // 传引用
void spam3(Foo x); // 传值
void spam4(Foo x[]); // 传数组
};
由于SWIG认为一切皆是指针,所以上面的Foo* Foo& Foo Foo[]
四种类型并没有什么区别,生成C#代理类后调用方式都一样:
var f = new Foo();
f.spam1(f);
f.spam2(f);
f.spam3(f);
f.spam4(f); //接收是数组,但是我们只传了一个值进去。不符合期望,后续要做处理。
同理,对于函数的返回值类型来说,以下也没有什么区别:
class Foo
{
public:
Foo* spam1();
Foo& spam2();
Foo spam3();
};
五、基本类型的指针与引用
如果未做特殊说明,这里的基本类型指的就是int、long、bool、char、float
等这些类型,非自己定义的类。
class Foo
{
public:
void add(int* a, int* b, int* result) {
*result = *a + *b;
}
};
默认情况下指针和引用会被生成为以SWIGTYPE_
开头的C#类型:
public void add(SWIGTYPE_p_int a, SWIGTYPE_p_int b, SWIGTYPE_p_int result)
{
}
对于这些不太友好的类型,需要编写.i
文件进行映射,将其映射为C#的int
类型:
%include <typemaps.i>
%apply int *OUTPUT {int * result};
%apply int *INPUT {int *a,int *b};
生成如下易于调用到C#代码:
public void add(int a, int b, out int result)
{
}
同理,引用类型的处理也是类似,可以将其映射为c#的ref
:
%include <typemaps.i> //添加SWIG自带的库
%apply bool& INOUT {bool&};
注:在上述过程中,我们并没有自己定义typemap。因为以上的操作SWIG已经帮我们实现了只是默认没有这么做而已,其实现的代码就是写在了
<typemaps.i>
库,我们只需要将其%apply
一下。
六、基本类型的数组
C++里的int*
即可以表示单个对象,也可以表示一个数组。这里演示如何将其映射为C#的数组。
class Foo
{
public:
void copy(int* source, int* target, int count){
}
};
可以通过如下的定义将上述数组转为C#的数组:
%include <arrays_csharp.i> // 添加SWIG自带的库
// apply一下
%apply int INPUT[] {int * source}
%apply int OUTPUT[] {int * target}
然后就生成了如下的C#代码:
public void copy(int[] source, int[] target, int count) {
demoModulePINVOKE.Foo_copy(swigCPtr, source, target, count);
}
调用起来非常之方便,我们观察下demoModulePINVOKE.Foo_copy
方法的实现:
原来是通过Marshal
将C#数组元素的首指针封送了过去。
七、基本类型的默认map规则
部分C++类型映射到C#类型的规则如下:
更多默认规则,下载完SWIG之后,使用Everything软件搜索csharp.swg
文件即可。
八、常用的typemap方法
是否还记得前几篇文章介绍typemap时的哪些in、out、freeargs方法? 上图里的方法也属于同等范畴,只不过是特定于C#语言映射时使用,后续你将会频繁使用。
九、代码插入
指令:%pragma(csharp) method={ … my code …}
。
常用method有:
- imclassbase
- imclasscode
- imclassclassmodifiers
- imclassimports
- imclassinterfaces
- modulebase
- modulecode
- moduleimports
- … 等等
分别可以在不同的代理类的不同位置插入自己定义的代码。
如:
%pragma(csharp) imclasscode={ … my code …}`
就是在moduleInvoke.cs
文件里插入代码。
十、实践
10.1 如何映射Foo*&到ref Foo
class Spam
{
public:
void changeFoo(Foo*& f);
};
对于上述的C++代码,我们期望的调用方式为:
var s = new Spam();
var f = new Foo();
s.changeFoo(ref f);
需要进行如下typemap定义,一步一步的映射过去:
// 告诉SWIG,C++的Foo*&就是C#的ref Foo
%typemap(cstype) Foo*& "ref Foo";
// 因为一切皆是指针,所以我们需要获取ref Foo的指针,供后续P/Invoke时传递。pre和post就是用来处理Foo与IntPtr的生成。
%typemap(csin,
pre="System.IntPtr temp_$csinput=Foo.getCPtr($csinput).Handle; System.IntPtr back_$csinput=temp_$csinput;",
post="if(temp_$csinput!=back_$csinput) $csinput=new Foo(temp_$csinput,false);") Foo*& "ref temp_$csinput";
// 因为一切皆是指针,所以我们就把指针传过去,并加上ref关键字,因为值会被修改。
%typemap(imtype) Foo*& "ref System.IntPtr";
// &是指针,*是指针,*&就是指针的指针,所以为void**
%typemap(ctype) Foo*& "void**";
其中cstype
和csin
具体指代如下图:
imtype
指代如下:
ctype
指代如下:
此时就可以正常调用了。
10.2 映射Foo* array数组
简单类型的数组上面已经介绍过了,那么如何映射复杂类型的数组呢?Foo*
可以直接映射为C#的Foo[]
吗?
考虑有如下的C++代码:
class Spam
{
public:
void setFoo(Foo* array,int count)
{
}
};
在这里我们可以编写.i
文件将Foo*
映射为一个数组类,但无法直接映射为C#的数组。
%include "carrays.i" // 添加库
%array_class(Foo,FooArray); // 定义一个数组类,其元素类型为Foo
然后我们就可以通过FooArray
这个数组类,进行调用:
var s = new Spam();
FooArray fooArray = new FooArray(2);
for(int i = 0; i < 2; i++)
{
fooArray.setitem(i, new Foo());
}
//.cast()返回的类型正好就是Foo,数组的第一个元素
s.setFoo(fooArray.cast(), 2);
但是你仔细查看setitem
的代码会发现:
public void setitem(int index, Foo value) {
demoModulePINVOKE.FooArray_setitem(swigCPtr, index, Foo.getCPtr(value)); }
Foo.getCPtr(value)
是获取到了value
对象的指针传到C++层,setitem
并未持有value
对象自身的引用。这就会导致一个问题,当你的应用程序面临很大的内存使用压力时,value
对象完全有可能被GC掉,导致C++调用出现异常。
如何解决过早被GC的问题?
思路也很简单:让setitem
所在的对象继续持有value
对象的引用即可,当该对象被GC时,才会顺便GCvalue
,而此时对C++调用也一定已经结束了。
所以我们对FooArray
类做如下的扩展,添加了一个自定义方法SetItemAndHold
:
%typemap(cscode) FooArray %{
private List<object> _ref = new List<object>();
public void SetItemAndHold(int index,Foo value)
{
_ref.Add(value); //将其添加一个列表里,使其不被GC
setitem(index,value);
}
%}
然后将原有对setitem
的调用重新调用到SetItemAndHold
上即可。
FooArray
除了cast()
之外另一个有用的方法就是frompointer()
,它可以根据首元素读取一个数组:
//f是C++层返回的一个值,是数组的首个元素
FooArray array=FooArray.formpointer(f);
// 后续对array操作,遍历
10.3 如何将char**映射为string[]
使用如下的规则即可:
CSHARP_ARRAYS(char *, string)
%typemap(imtype, inattributes="[System.Runtime.InteropServices.In, System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPArray, SizeParamIndex=0, ArraySubType=System.Runtime.InteropServices.UnmanagedType.LPStr)]") char *INPUT[] "string[]";
%apply char *INPUT[] { char ** };
10.4 如何映射C++的容器(vector\pair)
class Spam
{
public:
void setFoo(std::vector<Foo> foos)
{
}
};
与数组的映射基本一样:
%include <std_vector.i>
%template(FooVector) std::vector<Foo>;
使用时:
var s = new Spam();
FooVector vec = new FooVector();
vec.Add(new Foo());
s.setFoo(vec);
同数组类一样,setFoo
也有内存过早被GC的问题,解决方式也是一样,不在赘述。
10.5 如何映射函数指针
记住一点:函数指针与普通对象指针没有什么不同。
typedef void(*act)();
class Spam
{
public:
void setAction(act a)
{
}
};
对于act
的包装,我们可以编写以下规则:
%typemap(csin) void(*)() "System.Runtime.InteropServices.Marshal.GetFunctionPointerForDelegate($csinput)";
%typemap(imtype,out="IntPtr") void(*)() "System.IntPtr";
// 将void(*)()映射为C#名为OperationFunction的委托
%typemap(cstype) void(*)() "OperationFunction";
//定义一个名为OperationFunction的委托
%pragma(csharp) moduleimports=%{
public delegate void OperationFunction();
%}
然后就可以使用原生的C#方式调用:
var s = new Spam();
OperationFunction action = new OperationFunction(() => { });
s.setAction(action);
十一、要点总结
- 理解一切皆是指针
- 避免在
interface.i
里写业务逻辑 - 注意SWIG指令顺序(大多
%xxx
在前,%include
在后) - 关注内存回收的节点,避免被过早GC
好博客就要一起分享哦!分享海报
此处可发布评论
评论(0)展开评论
展开评论