SWIG包装器使用指南——(一)基本概念

SWIG系列:http://t.csdn.cn/cIAcr

一、前言

SWIG 版本:4.1.1
官方文档:https://www.swig.org/doc.html

二、简介

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 会解析这个文件来生成对应的包装代码。此文件的文件名与文件后缀无特殊要求,可随意更改。

文件大致分为四部分:

  1. 模块名:%module开头,必须有但意义不大,就是个名字而已。
  2. 需要include的头文件:用%{ %}括起来且内部用#include开头,这些include的头文件会原样不动的复制粘贴到包装代码中。此项必须有。
  3. swig配置项及指令:如%typemap %apply等,不同的指令有不同的功能。此项可选。
  4. 要解析的头文件:%include后面跟待解析的头文件。SWIG会解析这些头文件中都有哪些类,哪些成员,并对解析到的这些东西进行包装。

当.i文件编写完成之后,我们就需要使用命令行来让swig生成包装代码:
swig.exe -c++ -outdir ../../ConsoleTest/lib -namespace ModuleNameSharp -csharp swig.i

  • -c++:表示启用c++语法解析,一般都要有这项。
  • -outdir:表示自动生成的c#类文件应该放到哪里。
  • -namespace:生成的C#类还可以指定命名空间。
  • -csharp:生成的目前语言是c#。
  • swig.i:.i文件的名称

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 整型数据

int
short
long
unsigned short
unsigned long
unsigned char
signed char
bool
以上都属于C++里的的整型,大多数目标语言都只有32位整型。但是C++的整型位数不确定,对于C#来说上述的整型都会被映射为C#的int类型(32位)。而反向从C#到C++映射时可能就会有数据丢失的问题,所以需要特别注意。

3.3.2 浮点类型

float、double 完美支持

3.3.3 字符类型

char会映射为只含单个字符的字符串(对于部分脚本语言),但对于c#来说会映射为c#的char
char* 会映射为字符串,但也可以通过后续介绍的typemap指令将其映射为byte

3.3.4 注意项

  1. long long类型需要谨慎使用:因为long long并不是所有的目标语言都支持,常常是超出了脚本语言的整型精度,而且在Tcl和Perl里会将其转为字符串。
  2. 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指令(设置忽略、重命名)

适用场景:

  1. 命名冲突
  2. 关键字冲突,如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指令或者指定段的指令来插入:

  1. 指定段指令:
%begin %{
 //插入到wrapper文件最开始的位置,常定义一些宏
%}

%runtime %{
//在SWIG生成的包装代码之前,可以定义一些帮助函数
%}

%header %{
//在SWIG生成的包装代码之前,添加头文件。等同于%{ … %}
%}

%wrapper %{
//在SWIG生成的包装代码里(末尾)
%}

%init %{
//在wrapper文件的末尾,可以添加初始化的代码
%}
  1. 使用insert:
// begin段
%insert(“begin”) “somecode”;

//header段
%insert(“header”) %{ //somecode %}

3.10 %inline指令

该指令使用方式如下,功能也是进行代码插入:

%inline{
  void inlineFunc()
  {
     // 插入的位置在header之前,runtime之后
  }
%}

但是与%insert差别有如下两点:

  1. %insert 只是将文本复制粘贴到wrapper.cpp中,目标语言无法调用
  2. %inline 复制粘贴之后还会解析和包装,供目标语言调用

3.11 .i 文件的编写策略

  1. 首先识别要包装的C++类都有哪些,不要包装所有的C++类。
  2. 一种目标语言一个.i文件
  3. 使用%include包含合适的头文件声明(注意依赖的头文件)
  4. 注意SWIG指令的顺序,在使用之前先定义
  5. 重命名原有的main()函数
  6. Run swig.exe and Compile

猜你喜欢

转载自blog.csdn.net/catshitone/article/details/129838746
今日推荐