因为项目需要使用python来调用C/C++函数,经过调研发现swig是合适的方案,为了记录下学习的过程,所以有了这个文档。
此资料是将SWIG的官方文档[2]截取重要的部分,以此来入门,章节序号对应原文档。
原文档地址在文末。
简介:
众所周知,SWIG这个项目创建的缘由,是为你们提供简洁而又天然的脚本语言接口。什么是简洁而天然呢?它的意思就是C/C++的函数就直接被封装为python的函数,class就被封装成python的class。 这样你们用起来就不会变扭 [1] 。
用官话来说,SWIG是让脚本语言(Python,java等)可以调用C/C++代码的工具。
SWIG是一个用来把C/C++程序和Perl,Python,Ruby,Tcl等脚本语言连接起来的接口编译器。它的工作原理是获取C/C++头文件的声明,并使用它们生成可以访问底层C/C++代码的脚本语言。此外,SWIG提供了大量的定制特性,让你可以定制包装过程以使用你的应用。
John Outsterhout(Tcl的创始人)曾经写了一遍描述脚本语言的好处的论文。SWIG使脚本语言与C/C++代码连接起来相当容易。
SWIG有大量的用途:
构建更强大的C/C++程序。使用SWIG,你可以用脚本解释器替换C程序中的main()函数,通过脚本解释器控制应用。这增加了相当的灵活性并使程序“可编程”。也就是说,脚本接口允许用户和开发人员轻松地修改程序的行为,而不必修改低级的C/C++代码。这样做的好处有很多。实际上,考虑你每天使用的所有大型软件包,几乎所有的软件包都包括特殊的宏语言、配置语言、甚至是允许用户定制的脚本引擎。
快速原型化和调试。SWIG允许C/C++程序放置在用于测试和调试的脚本环境中。比如,你可以使用一组脚本来测试一个库或者使用脚本解释器作为交互式调试器。因为SWIG不需要修改底C/C++代码,所以即使最终产品不依赖脚本,也可以使用它。
系统集成。脚本语言在控制和粘接松耦合的软件组件方面做工作地相当好。使用SWIG,不同的C/C++程序可以转换为脚本语言的扩展模块。这些模块可以被组合成新的有趣的应用。
脚本语言扩展模块的构建。SWIG可用于将常见的C/C++库转换为用在流行的脚本语言中的模块。。当然,在此操作之前,你需要确认没有其他人已经创建了这种模块。
SWIG有时会被用来比较其他接口定义编译器(IDL)进行比较,比如在CORBA和COM等系统中可以找到的IDL编译器。尽管有一些相似之处,但SWIG的目的是让你不必向应用程序添加额外的IDL规范层。如果实在有的话,它更多的是一个快速应用程序开发和原型工具。具体来说:
最后,值得注意的是,尽管有时会将SWIG与其他更专用的脚本语言扩展构建工具(如Perl XS Python bgen)进行比较,当它的主要受众是希望在应用程序中添加脚本语言组件的C/C++程序员。正因为如此,SWIG的关注点往往与那些在脚本语言发行版中广泛使用而构建小模块的工具略有不同。
SWIG(Simplified Wrapper and Interface Generator),是一个软件开发工具,用于将C/C++程序构建为脚本语言接口。最初开发于1995年。SWIG通过脚本语言自动化来大大简化了开发,也使得开发人员和用户可以放手去处理更重要的事情。
SWIG是用C和C++语言实现的,并以源代码的形式公开。你需要一个可以工作的C++编译器(比如g++)和至少一个SWIG支持的脚本语言来运行。
ps:我的目标语言是Python,因为CentOS自带的有,所以为了方便没有加其他例子。
下载地址:
https://sourceforge.net/projects/swig/
解压
tar -zxvf swig-3.0.12.tar.gz
然后编译
cd swig-3.0.12
./configure
make
make install
在安装之后,如果想测试SWIG,那么键入以下命令。
make -k check #此命令消耗时间久,应该是一个全面的检查,我大概运行了20分钟
即使此测试失败,SWIG仍有很大几率可以正常运行。其中的一些测试也有可能会失败,因为缺少依赖包,比如PCRE、Boost,但是这需要在配置的过程中,严格地分析配置输出的结果。
SWIG大体上是一个将C/C++代码转换为其他各种语言的工具。对于SWIG代码生成器而言,C是输入语言,其他更高级别的语言是目标语言。当SWIG运行时必须指定一个目标语言。SWIG可以被多次引用,但是目标语言不同时必须每次引用都指定目标语言。将C/C++变为不同目标语言的接口的能力是SWIG的一个核心优势和特点。
SWIG大体上由两部分组成。核心部分从输入的ISO C/C++和SWIG扩展语法(extensions to the C/C++ standards)创建一个语法树,然后语法树来到第二个部分,一个生成特定于高级语言的代码的目标语言模块。SWIG支持许多不同的目标语言。这些语言要么是已支持,要么是正在实验中。提供以上的实验状态,是因为不是所有的目标语言都支持。幸运的是几乎支持大部分流行的语言。
ps:以下代码在swig的解压目录下面,例如/swig-3.0.12/Examples/contract/simple_c下面就有,所以不用自己写。
说明SWIG的最好方式就是写一个例子。现在有如下C代码;
/* File : example.c */
#include
int Circle (int x, int y, int radius) {
/* Draw Circle */
printf("Drawing the circle...\n");
/* Return -1 to test contract post assertion */
if (radius == 2)
return -1;
else
return 1;
}
假设你想将C代码中的函数和全局变量My_variable转换到Python中,你要先写一个SWIG接口文件(interface file),后缀是.i,代码如下。
/* File : example.i */
/* Basic C example for swig contract */
/* Tiger, University of Chicago, 2003 */
%module example
%contract Circle (int x, int y, int radius) {
require:
x >= 0;
y >= 0;
radius > x;
ensure:
Circle >= 0;
}
%inline %{
extern int Circle (int x, int y, int radius);
%}
此接口文件包括ISO C函数原型和变量声明。%module指令是SWIG所创建的模块名。%{ %}块里面插入了额外代码,比如C头文件或者其他C的声明,会放在生成的C打包代码中。
以Python为例,SWIG版本为3.0.2(运行在Centos7上,ps:在win上面一直出现reference错误,应该是库的问题,后来我在Centos7上则没有出现问题,以下例子运行成功)。
[root@localhost simple_c]# swig -python example.i
[root@localhost simple_c]# gcc -c -fpic example.c example_wrap.c -I/usr/include/python2.7/
[root@localhost simple_c]# gcc -shared example.o example_wrap.o -o _example.so
[root@localhost simple_c]# python
Python 2.7.5 (default, Apr 2 2020, 13:16:51)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import example
>>> example.Circle(1,3,4)
Drawing the circle...
1
>>>
注:
脚本语言是建立在一个可以执行命令和脚本的语法分析器之上的。在这个语法分析器里面,有一个执行命令和访问变量的机制。一般来说,这个机制被用于实现语言的内建函数。然而,为了扩展解释器,经常添加新的命令和变量。为了这个,大多数的语言都定义了用来添加新命令的特殊接口。另外,一个特殊的其他语言函数接口定义了那些新的命令应该如何接入到解释器中。
一般来说,当添加新命令到脚本解释器中时,需要做两件事:第一,需要写一个特殊的“打包”函数,用来将解释器和底层C函数像胶水一样黏在一起;第二,需要提供参数、函数名等的详细打包信息到解释器。
接下来的小节描述了这个过程。
现假设有如下C代码:
int fact(int n){
if(n<=1)
return 1;
else
return n*fact(n-1);
}
为了使脚本语言访问到此函数,有必要写一个“打包”函数。“打包”函数必须做一下几个事情:
例如,Tcl打包函数如下所示:
int wrap_fact(ClientData clientData, Tcl_Interp *interp, int argc, char *argv[]) {
int result;
int arg0;
if (argc != 2) {
interp->result = "wrong # args";
return TCL_ERROR;
}
arg0 = atoi(argv[1]);
result = fact(arg0);
sprintf(interp->result, "%d", result);
return TCL_OK;
}
只要创建了一个打包函数,最后一步就只是告知脚本语言由这个函数了。当模块加载完成后,由语言调用的初始化函数完成。例如,将以上函数载入到Tcl解释器中需要如下代码:
int Wrap_Init(Tcl_Interp *interp) {
Tcl_CreateCommand(interp, "fact", wrap_fact, (ClientData) NULL, (Tcl_CmdDeleteProc *) NULL);
return TCL_OK;
}
执行完之后,Tcl马上就会有一个新的叫做“fact”的命令,和其他Tcl命令一样的。
尽管只描述了将新函数添加到Tcl的过程,添加新函数到Perl和Python的过程也是大体相同的。都是需要写出特殊的打包器和额外的初始化代码。只有一些特殊细节会有不同。
变量链接指的是将C/C++全局变量映射到脚本语言解释器中的问题。例如,假设有如下变量:
double Foo=3.5;
从脚本访问此变量时可能会以如下这种方式(以Perl为例):
$a = $Foo * 2.3; # 声明
$Foo = $a + 2.0; # 赋值
为了提供这种存取,变量一般使用get/set函数来管理。例如,不管在什么时候取变量的值,必须调用"get"函数。同样的,在改变变量的值时,也必须调用set函数。
在许多语言中,get/set函数是进行声明和赋值的操作。因此,给 F o o 赋值会暗中调用 g e t 函数,输入 Foo赋值会暗中调用get函数,输入 Foo赋值会暗中调用get函数,输入Foo = 4则会调用底层set函数来改变其值。
大多数情况下,一个C程序或者库也许会定义如下的一系列常量。例如:
#define RED 0xff0000
#define BLUE 0x0000ff
#define GREEN 0x00ff00
为了使常量可用,它们的值存储在脚本语言变量中,如 R E D , RED, RED,BLUE,$GREEN。几乎所有的脚本语言都提供了创建变量的C函数,所以存储常量是简单的工作。
虽然脚本语言访问简单的函数和变量没有问题,但是访问C/C++的机构和变量却存在不同的问题。这是因为结构的实现是极大的和数据表现形式和布局挂钩的。另外,具体的语言特色是很难映射到解释器的。例如,C++中的继承在Perl的接口中是如何表示的呢?
解决结构最直接的方法是实现一系列可以隐藏数据底层表示的访问器函数。例如:
struct Vector {
Vector();
~Vector();
double x, y, z;
};
可以转化为如下形式的函数:
Vector *new_Vector();
void delete_Vector(Vector *v);
double Vector_x_get(Vector *v);
double Vector_y_get(Vector *v);
double Vector_z_get(Vector *v);
void Vector_x_set(Vector *v, double x);
void Vector_y_set(Vector *v, double y);
void Vector_z_set(Vector *v, double z);
现在,解释器会如下这样使用:
% set v [new_Vector]
% Vector_x_set $v 3.5
% Vector_y_get $v
% delete_Vector $v
% ...
因为访问函数提供了访问对象内部的机制,所以解释器不必知道Vector中的具体实现。
在具体的例子中,有可能会使用低级别的访问函数来创建代理类,或者叫影子类(shadow class)。代理类是由脚本语言生成的一种特殊的对象,以一种看起来是原生结构的方式(代理真正的C++类)访问C/C++类(或结构)。例如,有如下C++声明代码:
class Vector {
public: Vector();
~Vector();
double x, y, z;
};
代理类机制可以通过解释器以一种更自然的方式访问结构。例如,Python会这样子用:
>>> v = Vector()
>>> v.x = 3
>>> v.y = 4
>>> v.z = -13
>>> ...
>>> del v
同样的,Perl5会这样用:
$v = new Vector;
$v->{x} = 3;
$v->{y} = 4;
$v->{z} = -13;
当使用代理类时,实际上有两个对象真正的在工作,一个是脚本语言的,一个是底层C/C++对象。
用脚本语言使用C/C++应用的最后一步就是添加你自己的扩展到脚本语言中。这里主要有两种方法,优先选择的方法是以共享库的形式创建动态可加载扩展(dynamically loadable extension),另外就是可以重编译脚本语言解释器,然后将扩展添加到其中。
为了创建共享库或DLL,就要查看你的编译器和链接器的手册。然而,在一些平台上此过程会这样:
# Build a shared library for Solaris
gcc -fpic -c example.c example_wrap.c -I/usr/local/include ld -G example.o example_wrap.o -o example.so
# Build a shared library for Linux
gcc -fpic -c example.c example_wrap.c -I/usr/local/include
gcc -shared example.o example_wrap.o -o example.so
为了使用共享库,你可以使用脚本语言的交互式命令(load, import, use, etc…)。此会引入你的模块并允许你开始使用它。如下例:
% load ./example.so
% fact 4
24
%
当使用C++语言时,创建共享库的过程也许会更加复杂,因为C++模块需要额外代码保证正确的操作。在很多机器上,你可以由以上方式来创建共享的C++模块,但需要改变一些链接行,如下:
c++ -shared example.o example_wrap.o -o example.so #注:在Linux平台应该是g++
当将扩展构建为共享库时,你的扩展经常会依赖于你的机器上的其他共享库。为了使共享库运行,需要找到所有正在运行的库。否则将会得到一个如下的错误:
>>> import graph
Traceback (innermost last):
File "" , line 1, in ?
File "/home/sci/data1/beazley/graph/graph.py", line 2, in ?
import graphc
ImportError: 1101:/home/sci/data1/beazley/bin/python: rld: Fatal Error: cannot successfully map soname 'libgraph.so' under any of the filenames /usr/lib/libgraph.so:/ lib/libgraph.so:/lib/cmplrs/cc/libgraph.so:/usr/lib/cmplrs/cc/libgraph.so:
>>>
此错误表示SWIG创建的扩展模块依赖于“libgraph.so”共享库,但是此库无法定位。为了解决问题,下面有几种方法可以尝试。
当使用静态链接时,你使用扩展重建脚本语言解释器。此过程通常包括编译一个小的、加了你自定义命令到语言中的主程序,然后开始解释。然后你就将库和程序链接在一起,以生成新的脚本语言可执行文件。
即使所有的平台都支持静态链接,但这并不是构建脚本语言扩展的最好的方式。实际上,几乎没有实践的理由来做这件事,而是用共享库代替。
此章节描述了SWIG的基本操作,输入文件的结构,以及如何处理标准ISO C声明。C++支持在下一个章节。然而C++程序员应该阅读本章节以理解基础。各个目标语言的特殊细节会在以后的章节。
带着运行SWIG的命令行如下所示:
swig [options] filename
filename指的是SWIG接口文件或者C/C++头文件。swig -help可以查看所有的帮助。下面是一系列常用的选项。还为每种目标语言定义了其他选项。ps:以下只截取部分。
支持的目标语言选项
-csharp -生成c#打包器
-go -生成Go打包器
-java -生成Java打包器
-python -生成Python打包器
常规选项
-c++ -打开C++处理器
-co -在SWIG库中检查
-copyright -显示版权信息
-debug-csymbols -在标志表中显示C标志
-o -设置C/C++的输出文件名为
-version -显示版本信息
作为输入,SWIG希望文件中含有ISO C/C++声明和特殊SWIG指令。通常来说,一个SWIG接口文件后缀为.i或.swg。在某些情况下,SWIG可以直接用于源文件或原始头文件。然而,这不是最经典的例子,还有一些你不想如此做的理由(将在后面说明)。
最经典的SWIG接口文件如下所示:
%module mymodule %{
#include "myheader.h"
%}
// Now list ISO C/C++ declarations
int foo;
int bar(int x);
模块名之前使用%module来表明。在%{ … %}里面的会简单的复制到由SWIG创建的结果打包文件中。
SWIG的输出是一个C/ c++文件,其中包含构建扩展模块所需的所有打包器代码。SWIG也许依据目标语言会生成一些特殊文件。默认情况下,.i为后缀的输入文件会转化为file_wrap.c或file_wrap.cxx(看是否使用-c++c选项)。因此,你必须使用-o选项来改变SWIG生成的打包文件名。在某些情况下,编译器根据文件后缀名来确定源语言(C,C++等)。因此,当你不想使用默认设置时,必须使用-o选项来改变SWIG打包文件的后缀名。例如:
swig -c++ -python -o example_wrap.cpp example.i
SWIG创建的C/C++输出文件通常包含为目标语言构建扩展模块所需的所有内容。
也可以使用-outdir选项来指定Python文件生成的目录,默认目录是在当前生成的C/C++文件目录下。如:
swig -c++ -python -outdir pyfiles -o cppfiles/example_wrap.cpp example.i
如果制订了cppfiles和prfiles,那么生成的文件会如下所示:
cppfiles/example_wrap.cpp
pyfiles/example.py
如果使用的-outcurrentdie选项(没有-o),SWIG就像经典C/C++编译器一样在当前目录下生成文件。没有加这个选项,那么默认输出目录就是输入文件的目录。如果-o和-outcurrentdie同时使用,-outcurrentdir会被忽略,如果不被-outdir覆盖,那么语言文件的输出目录就是和C/C++文件相同的目录。
C和C++风格的注释可以在接口文件的任何位置出现。在SWIG的早期版本中,注释被用来生成文档文件。然而,此功能正在修复中并会在之后的SWIG版本中。
像C一样,SWIG使用一个增强的C预处理器来预处理所有的输入文件。所有标准的预处理风格都支持,包括文件包含,条件编译和宏命令,#include会被忽略,除非使用了-includeall命令行选项。因为SWIG有时会处理原始C头文件,所以禁用了#include。在这种情况下,你仅仅是想让扩展模块包含提供的头文件中的函数,而不是其中包含的所有内容(即系统头文件、C库函数等)。
还应该注意的是,SWIG预处理器会跳过%{…%}中的内容。此外,预处理器包括了大量比常规C预处理器更强大的宏指令处理。这些扩展在“Preprocessor”章节中有描述。
大部分SWIG操作都由特殊的指令控制,这些指令前有一个“%”,以区别于普通的C声明。这些指令用于给SWIG提示或以某种方式改变SWIG的解析行为。
因为SWIG指令不是合法的C语法,所以通常不可能将指令包括在头文件中。然而,SWIG指令在使用条件编译的情况下可以包含在C头文件中:
/* header.h --- Some header file */
/* SWIG directives -- only seen if SWIG is running */
#ifdef SWIG
%module foo
#endif
SWIG是在解析输入文件时由SWIG定义的特殊预处理符号。
所有其他的类型(结构体,类,数组等),SWIG应用一个非常简单的原则:
所有都是指针
换句话说,SWIG使用引用来控制其他的所有东西。这个规则很有用,因为大多数C/C++程序员大量使用指针,而SWIG可以使用类型检查指针机制,这已经用于将指针转换为基本类型。
尽管听起来复杂,但是其实非常简单。假设有如下接口文件代码:
%module fileio
FILE *fopen(char *, char *);
int fclose(FILE *);
unsigned fread(void *ptr, unsigned size, unsigned nobj, FILE *);
unsigned fwrite(void *ptr, unsigned size, unsigned nobj, FILE *);
void *malloc(int nbytes);
void free(void *);
在此文件中,SWIG不知道FILE是什么,但是因为这是一个指针,所以是什么并不真正影响。如果你想将此模块打包到Python中,你可以根据你的想法使用以下函数:
# Copy a file
def filecopy(source, target):
f1 = fopen(source, "r")
f2 = fopen(target, "w")
buffer = malloc(8192)
nbytes = fread(buffer, 8192, 1, f1)
while (nbytes > 0):
fwrite(buffer, 8192, 1, f2)
nbytes = fread(buffer, 8192, 1, f1)
free(buffer)
在这种情况下,f1,f2和buffer都是包含C指针的不透明的对象。里面的值是什么并不影响,不知道值是什么,程序仍然可以很好的工作。
参考:
http://www.javashuo.com/article/p-hjtynpis-ec.html
http://www.swig.org/Doc4.0/SWIGDocumentation.pdf