大家好,欢迎来到IT知识分享网。
探索SWIG的Demo请参考:SWIG-Playground
一、前言
二、简介
2.1 SWIG是什么?
- SWIG = Simplified Wrapper and Interface Generator
- 是一个exe小工具
- 主要用来包装已有的 C/C++ 代码并生成目标语言(C#、Lua、Python等)代码(本系列文章将以C#如何调用C++函数为例)。
2.2 为什么要使用SWIG?
- 可以极大简化目标语言(C#、Python、Lua)调用C/C++接口的难度
- 解耦,原生的开发方式
- 更易于测试
三、基本概念
3.1 .i 文件简介
.i
文件是SWIG规则描述文件,这个文件需要我们自己手动编写。SWIG 会解析这个文件来生成对应的包装代码。此文件的文件名与文件后缀无特殊要求,可随意更改。
文件大致分为四部分:
- 模块名:
%module
开头,必须有但意义不大,就是个名字而已。 - 需要include的头文件:用
%{ %}
括起来且内部用#include
开头,这些include的头文件会原样不动的复制粘贴到包装代码中。此项必须有。 - swig配置项及指令:如
%typemap
,%apply
等,不同的指令有不同的功能。此项可选。 - 要解析的头文件:
%include
后面跟待解析的头文件。SWIG会解析这些头文件中都有哪些类,哪些成员,并对解析到的这些东西进行包装。
当.i
文件编写完成之后,我们就需要使用命令行来让swig生成包装代码:
swig.exe -c++ -outdir ../../ConsoleTest/lib -namespace ModuleNameSharp -csharp swig.i
-c++
:表示启用c++语法解析。如果你的是C++项目,就要有这项。-outdir
:生成的C#类文件的输出目录。-namespace
:指定生成的C#类的命名空间。-csharp
:表示生成的目前语言是C#。swig.i
:.i
文件的路径-debug-tmsearch
:显示typemap查找信息
3.2 接入SWIG之后项目上的变化
接入前:
我们的C#项目一般都是通过P/Invoke
的方式或者通过添加一个C++/CLI
中间层项目的方式来调用C++代码。这些方式都需要我们自己进行中间层代码维护,不同语言间数据结构的转换。
接入后:
中间层代码就由SWIG代劳了,上图的绿色部分完全由SWIG自动生成。可以将SWIG生成的C#类全部放到一个单独的项目中,这里称之为C# Module。此模块中的C#代码称之为代理类代码。
SWIG生成的C++函数的Wrapper.cpp
,需要放到原有的C++项目中。wrapper.cpp称之为包装代码。
调用链路:c#调用到c# module上,然后调用到wrapper.cpp上,然后从wrapper.cpp再调用到具体的某个c++目标函数上。
3.3 简单数据类型的处理
这里的简单数据类型是指:int、 long、 short、 unsigned int、float
等(不是类、结构体)。
3.3.1 整型数据
3.3.2 浮点类型
float、double
完美支持
3.3.3 字符类型
char
会映射为只含单个字符的字符串(对于部分脚本语言),但对于c#来说会映射为c#的char
。
char*
会映射为字符串,但也可以通过后续介绍的typemap指令将其映射为byte
。
3.3.4 注意项
long long
类型需要谨慎使用:因为long long
并不是所有的目标语言都支持,常常是超出了脚本语言的整型精度,而且在Tcl和Perl里会将其转为字符串。long double
类型不被swig支持。
3.4 指针与复杂类型
int *
double *
char
指针、指针的指针等的包装swig完美全支持。
类、结构体、数组等复杂数据类型,也是指针。
☝ 在这里你将会遇到SWIG的第一个核心知识点:除了基础类型之外一切皆是指针。
3.5 理解一切皆是指针
假设我们有如下的C++代码需要包装:
//test.h FILE* open(char* path); void display(FILE* file);
编写swig.i文件:
%module XXX %{ #include "test.h" %} %include "test.h"
然后观察生成的包装文件:
typedef struct _iobuf {
void* _Placeholder; } FILE; void * CSharp_DemoNamespace_open(char * jarg1) {
void * jresult ; char *arg1 = (char *) 0 ; FILE *result = 0 ; arg1 = (char *)jarg1; result = (FILE *)open(arg1); jresult = (void *)result; return jresult; } void CSharp_DemoNamespace_display(void * jarg1) {
FILE *arg1 = (FILE *) 0 ; arg1 = (FILE *)jarg1; display(arg1); }
我们在swig.i里并没有告诉swig FILE
是个什么类型,所以它就自行定义了一个typedef
,而这个FILE
又对应的生成了一个C#的SWIGTYPE_p_FILE
类型。如下:
namespace DemoNamespace {
public class demoModule {
public static SWIGTYPE_p_FILE open(string path) {
global::System.IntPtr cPtr = demoModulePINVOKE.open(path); SWIGTYPE_p_FILE ret = (cPtr == global::System.IntPtr.Zero) ? null : new SWIGTYPE_p_FILE(cPtr, false); return ret; } public static void display(SWIGTYPE_p_FILE file) {
demoModulePINVOKE.display(SWIGTYPE_p_FILE.getCPtr(file)); } } }
我们在使用C#调用时只需要调用到open和display上,而不需要考虑SWIGTYPE_p_FILE
的真实类型是什么,我们不使用它,它只是一个载体:
SWIGTYPE_p_FILE f=demoModule.open("xxxx"); demoModule.display(f);
而这个载体载的是什么呢?答案为:C++的指针,一个指向FILE
类型对象的指针。
观察一下生成的SWIGTYPE_p_FILE
类:
public class SWIGTYPE_p_FILE {
private global::System.Runtime.InteropServices.HandleRef swigCPtr;// 注意此成员 internal SWIGTYPE_p_FILE(global::System.IntPtr cPtr, bool futureUse) {
swigCPtr = new global::System.Runtime.InteropServices.HandleRef(this, cPtr); } protected SWIGTYPE_p_FILE() {
swigCPtr = new global::System.Runtime.InteropServices.HandleRef(null, global::System.IntPtr.Zero); } internal static global::System.Runtime.InteropServices.HandleRef getCPtr(SWIGTYPE_p_FILE obj) {
return (obj == null) ? new global::System.Runtime.InteropServices.HandleRef(null, global::System.IntPtr.Zero) : obj.swigCPtr; } internal static global::System.Runtime.InteropServices.HandleRef swigRelease(SWIGTYPE_p_FILE obj) {
return (obj == null) ? new global::System.Runtime.InteropServices.HandleRef(null, global::System.IntPtr.Zero) : obj.swigCPtr; } }
注意成员swigCPtr
,它就是C++对象的指针,这里的SWIGTYPE_p_FILE
就是C++里的FILE
的代理类,可以将其理解为一个包含了C++指针的容器。
3.5.1 一切皆是指针–值传递
假如我们有如下的C++函数:
Vector cross_product(Vector v1,Vector v2);
对应的包装代码为:
void * CSharp_DemoNamespace_cross_product(void * jarg1, void * jarg2) {
Vector * argp1 = (Vector *)jarg1; Vector arg1 = *argp1; Vector * argp2 = (Vector *)jarg2; Vector arg2 = *argp2; Vector result = cross_product(arg1,arg2); void * jresult = new Vector(result); return jresult; }
可见即使从C#调用过来时传的是值,包装函数里接收的也是指针,然后根据指针再取值,传到cross_product
上。
3.5.2 一切皆是指针–数组
上述两种C++里表示数组的方式,对于SWIG来说没有什么区别,不管是一维还是多维数组,SWIG都将其包装为指针。如下:
int CSharp_DemoNamespace_foobar(void * jarg1) {
int * arg1 = (int *)jarg1; int result = (int)foobar(arg1); int jresult = result; return jresult; } void CSharp_DemoNamespace_grok(void * jarg1) {
// 二维数组强转为指针的指针 char ** arg1 = (char **)jarg1; grok(arg1); } void CSharp_DemoNamespace_transpose(void * jarg1) {
double (*arg1)[20] ; // 强转 arg1 = (double (*)[20])jarg1; transpose(arg1); }
3.6 %ignore与%rename指令(设置忽略、重命名)
适用场景:
- 命名冲突
- 关键字冲突,如c++里的某个变量名是C#里的关键字,或者是SWIG自身的一个关键字。
考虑有如下的C++代码:
void internal(const char *);
internal
是c#的关键字,如果不加修改则生成C#代理类时肯定会报错。我们可以配置如下的规则:
// 将internal 重命名为my_print %rename(my_print) internal; // 或者忽略这个internal函数,不让SWIG去包装它 %ignore internal;
%rename
很强大,可以将一个很长很长的名字缩为较短的一个名字,或者根据字符串匹配规则替换名字里的某些字符:
%rename("myprefix_%s") ""; // print 重命名为 myprefix_print %rename("%(lowercamelcase)s") ""; // foo_bar 重命名为 fooBar; FooBar -> fooBar %rename("%(strip:[wx])s") ""; // wxHello -> Hello; FooBar -> FooBar %rename("%(regex:/wx(?!EVT)(.*)/\\1/)s") ""; // 支持正则 wxSomeWidget -> SomeWidget wxEVT_PAINT -> wxEVT_PAINT
3.7 函数指针的处理
int binary_op(int a, int b, int (*op)(int, int));
上述函数的第三个指针是一个函数指针,SWIG处理时与其他类型的指针包装方式没有什么不同,就是个指针而已。来看下生成的包装代码(无非也是指针强转):
int CSharp_DemoNamespace_binary_op(int jarg1, int jarg2, void * jarg3) {
int arg1 = (int)jarg1; int arg2 = (int)jarg2; int (*arg3)(int,int) = (int (*)(int,int))jarg3; int result = (int)binary_op(arg1,arg2,arg3); int jresult = result; return jresult; }
此时你可能会想生成的C#代理是什么样的?C#能否直接调用?来看一下:
public class demoModule {
public static int binary_op(int a, int b, SWIGTYPE_p_f_int_int__int op) {
int ret = demoModulePINVOKE.binary_op(a, b, SWIGTYPE_p_f_int_int__int.getCPtr(op)); return ret; } }
注意:这里SWIG生成函数指针op
的代理类为SWIGTYPE_p_f_int_int__int
,名字的_p
就是pointer的意思,_f
就是function的意思,后面的_int_int__int
则分别为入参类型与返回值类型。此种代理类我们之前已有介绍,它就是一个包含指针的容器,它的成员只有一个swigCPtr
而已。我们无法只用c#直接new
,也无法直接调用。
此时我们可以暂时先忽略它,不妨碍我们对SWIG机制的理解。在后续章节中,我会介绍如何将其映射为C#里直接使用的委托。
3.8 %extend:扩展数据结构、替换函数
考虑有如下的c++代码:
class Vector {
public: double x,y,z; };
如何我们想给Vector
这个数据结构添加一个额外的成员,用来打印出x,y,z的值。其中一种方式你可以直接修改现有代码,另外也可以通过%extend
指令,告诉SWIG在生成包装代码时,额外添加成员。如下:
%{ #include "vector.h" %} %include "vector.h" %extend Vector { void print() { printf("Vector [%g, %g, %g]\n", $self->x, $self->y, $self->z); } }
这样就可以使用如下的C#代码直接调用print
:
Vector v = new Vector(); v.x = 1; v.y = 2; v.print();
%extend
另外的一个功能是可以用来替换原数据结构中的某个成员:
class Foo {
public: void bar(int a); };
如果我们觉得现有的bar
函数不符合我们的期望,可以结合使用%ignore
进行替换:
%{ #include "Foo.h" %} %extend Foo { void bar(int a){ // 在这里编写自己的逻辑 } } // 忽略原有的bar %ignore Foo::bar; %include "Foo.h"
注意:%extend和%ignore的顺序不能反。
3.9 如何在wrapper.cpp中插入自定义代码
可以通过%insert
指令或者指定段的指令来插入:
- 指定段指令:
%begin %{ //插入到wrapper文件最开始的位置,常定义一些宏 %} %runtime %{ //在SWIG生成的包装代码之前,可以定义一些帮助函数 %} %header %{ //在SWIG生成的包装代码之前,添加头文件。等同于%{ … %} %} %wrapper %{ //在SWIG生成的包装代码里(末尾) %} %init %{ //在wrapper文件的末尾,可以添加初始化的代码 %}
- 使用insert:
// begin段 %insert(“begin”) “somecode”; //header段 %insert(“header”) %{ //somecode %}
3.10 %inline指令
该指令使用方式如下,功能也是进行代码插入:
%inline{ void inlineFunc() { // 插入的位置在header之前,runtime之后 } %}
但是与%insert
差别有如下两点:
%insert
只是将文本复制粘贴到wrapper.cpp中,目标语言无法调用%inline
复制粘贴之后还会解析和包装,供目标语言调用
3.11 .i 文件的编写策略
- 首先识别要包装的C++类都有哪些,不要包装所有的C++类。
- 一种目标语言一个
.i
文件 - 使用
%include
包含合适的头文件声明(注意依赖的头文件) - 注意SWIG指令的顺序,在使用之前先定义
- 重命名原有的
main()
函数 - Run swig.exe and Compile
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/125823.html