SWIG学习记录
1 什么是SWIG?
SWIG官网链接
SWIG是一种实现将C/C++语言编写的代码移植到其他高级语言编程项目中的开发工具。简而言之,就是将C/C++语言编写的功能封装起来,以供其他高级语言调用。SWIG可用于不同类型的目标语言,包括常见的脚本语言,如Javascript、Perl、PHP、Python、Tcl和Ruby。支持的语言列表还包括非脚本语言,如c#, D, Go语言,Java,包括Android, Lua, OCaml, Octave, Scilab和r。还支持一些解释和编译的Scheme实现(Guile, MzScheme/Racket)。
SWIG通常用于解析C/ c++接口,并生成上述目标语言调用C/ c++代码所需的“粘合代码”。除此之外,解析树还可以导出为XML。
SWIG是Simple Wrapper and Interface的缩写,是一个帮助将C++函数接口封装成其他高级语言可调用API的开发工具。在以往,想要为C++代码创建.NET的API,一般情况下,必须使用C++托管(Managed C++),去写大量托管代码才能生成的。SWIG开发编译工具让这个过程变得简单,你只需要在接口文件中告诉SWIG要为哪些类创建.NET API,SWIG就会自动托管代码,生成.NET API。
1.2 特性
SWIG能够包装所有的ISO C99,并且提供了对ISO c++ 98到c++ 17的封装支持。
1.2.1 预处理
SWIG提供了一个完整的C预处理器,具有以下特性:
- Macro 扩展。
- 自动将#define语句包装为常量。
- 支持C99(可变宏扩展)。
2 SWIG安装
SWIG可以运行在Unix、Microsoft Windows和Macintosh操作系统环境中。SWIG可以在32位和64位版本的Windows上工作。
SWIG没有附带通常的Windows类型安装程序,但是它非常容易上手。主要步骤如下:
1、从SWIG官网下载swigwin压缩包,然后解压缩到一个目录中。这就是Windows平台需要下载的全部内容。
2、设置环境变量,以便使用Visual c++运行示例。
使用Microsoft Visual c++是编译和链接SWIG输出的最常见方法。
官网下载: 直接在官网下载相应运行环境下的swig版本,我下载的是windows系统下的swigwin。
系统环境变量配置: swig可执行程序运行需要链接到目标语言环境,需要配置系统环境变量,因为SWIG需要找到高级语言的include和库目录。如果你不想使用环境变量,那么就用硬编码的值更改.dsp文件中出现的所有环境变量。
下表是各类高级语言需要配置的环境变量:
语言 | 变量名 | 变量值 | 备注 |
C# | 无 | 无 | C#无序配置环境变量,因为C#项目文件中已包含所需环境路径。 |
Java | JAVA_INCLUDE | 包含jni.h文件的目录 | 以JDK1.3为例:D:\jdk1.3\include |
JAVA_BIN | 包含javac.exe执行文件的bin目录 | 以JDK1.3为例:D:\jdk1.3\bin | |
Perl | PERL5_INCLUDE | 包含perl.h文件的目录 | 以nsPerl 5.004_04为例:D:\nsPerl5.004_04\lib\CORE |
PERL5_LIB | Perl library链接路径 | 以nsPerl 5.004_04为例:D:\nsPerl5.004_04\lib\CORE\perl.lib | |
Python | PYTHON_INCLUDE | 包含Python.h文件的目录 | 以Python 2.1.1为例:D:\python21\include |
PYTHON_LIB | Python library链接路径 | 以Python 2.1.1为例:D:\python21\libs\python21.lib |
从官网下载的swigwin-4.0.2压缩包,解压后如下图所示,我们使用其中的开发工具swig.exe。
在解压目录下直接进入cmd,在命令行中运行swig.exe程序,查看帮助信息,可以看到swig可以将C++链接到如下的目标语言。
3 SWIG基础介绍
SWIG安装好,系统环境变量配置完成后,SWIG为C/C++代码生成其他语言API主要需要进行两步工作。
- 编写相应C++代码的接口文件(.i或.idl),告诉SWIG要为哪些类的方法创建API。然后在命令行中使用swig.exe可执行文件,编译生成托管代码。
- 将swig生成的托管代码,以及C++工程的动态库dll文件加入到目标语言项目的相应环境中。就可以实现在目标语言项目中调用C++代码中封装的API了。
接下来,将描述SWIG的基本操作,包括SWIG的输入文件结构,以及如何处理标准的ISO C声明的。
3.1 运行SWIG
这里插一段来介绍一下SWIG的接口文件,这是使用SWIG最主要的工作。引用自https://swig.org/Doc4.0/SWIG.html#SWIG,如果想详细了解SWIG接口文件工作机制,可以看看这篇文章。
查看帮助命令: swig - -help,例如:查询有关C#的帮助信息。
swig -csharp -help
swig的基本命令形式形如:swig [options] filename,其中filename是SWIG的接口文件或C/C++头文件。[options]是各种可选的SWIG命令参数。
参数也可以在命令行选项文件(也称为响应文件)中传递,这在超过系统命令行长度限制时很有用。为此,将参数放在一个文件中,然后提供文件名@前缀,就像这样:
swig @file
文件中的选项由空格分隔。
3.1.1 输入格式
SWIG 需要一个包含 ISO C/C++ 声明和特殊 SWIG 指令的文件作为输入。通常,这是一个特殊的 SWIG 接口文件,用特殊的 .i 或 .swg 后缀表示。
模块名由特定的 %module 指令提供。%{…%} 块中的所有内容都只会逐字复制到 SWIG 最终创建的wapper文件中。此部分几乎总是用于包含头文件和其他声明用于生成的包装器代码的编译。
重点强调,因为你仅仅在 SWIG 输入文件中包含一个声明,该声明不会自动出现在生成的wapper代码中——因此你需要确保在 %{…%} 部分中包含正确的头文件。应该注意的是,SWIG 不解析或解释包含在 %{…%} 中的文本。
3.1.2 输出
SWIG 的输出是一个 C/C++ 文件,其中包含构建扩展模块所需的所有wapper代码。SWIG 可能会根据目标语言生成一些其他文件。默认情况下,名为 file.i 的输入文件将转换为文件 file_wrap.c 或 file_wrap.cxx(取决于是否使用了 -c++ 选项)。可以使用 -o 选项更改 C/C++ 输出文件的名称。在某些情况下,编译器使用文件后缀来确定源语言(C、C++ 等)。
因此,如果需要不同于默认值的东西,则必须使用 -o 选项来更改 SWIG 生成的包装器文件的后缀。例如:
$ swig -c++ -csharp -o example_wrap.cpp example.i
对于许多目标语言,SWIG 还将生成目标语言的代理类文件。这些特定于语言的文件的默认输出目录与生成的 C/C++ 文件是同一目录。这可以使用 -outdir 选项进行修改。例如:
$ swig -c++ -csharp -outdir cshfiles -o cppfiles/example_wrap.cpp example.i
SWIG创建的C/ c++输出文件通常包含为目标脚本语言构建扩展模块所需的所有内容。要构建最终的扩展模块,需要编译SWIG输出文件,并将其与C/ C++程序的其余部分链接起来,以创建一个共享库。
如果目录 cppfiles 和 csharpfiles 存在,将生成以下内容:
cppfiles/example_wrap.cpp
csharpfiles/example.cs
3.1.3 注释
C 和 C++ 样式的注释可以出现在接口文件中的任何位置。
3.1.4 预编译
与C一样,SWIG通过增强版的C预处理器对所有输入文件进行预处理。支持所有标准的预处理器特性,包括文件包含、条件编译和宏。但是,除非提供了 -includeall 命令行选项,否则#include语句将被忽略。禁用include的原因是,SWIG有时用于处理原始的C头文件。在这种情况下,你通常只希望扩展模块包含所提供头文件中的函数,而不是该头文件可能包含的所有内容(例如,系统头文件、C库函数等)。
3.1.5 SWIG指令
SWIG的大多数操作都是由特殊指令控制的,这些指令前面总是带一个“%”,以区别于普通的C声明。这些指令用于给出SWIG提示或以某种方式改变SWIG的解析行为。
因为SWIG指令不是合法的C语法,所以通常不可能在头文件中包含它们。但是,SWIG指令可以像这样使用条件编译来包含在C头文件中:
/* header.h --- Some header file */
/* SWIG directives -- only seen if SWIG is running */
#ifdef SWIG //SWIG是由SWIG在解析输入文件时定义的特殊预处理符号。
%module foo
#endif
3.1.6 解析器的局限性
尽管SWIG可以解析大多数C/ c++声明,但它并没有提供完整的C/ c++解析器实现。这些限制大多与非常复杂的类型声明和某些高级c++特性有关。具体来说,以下特性目前不支持:
- 非常规类型声明。例如,SWIG不支持如下声明(即使这是合法的C):
/* Non-conventional placement of storage specifier (extern) */
const int extern Number;
/* Extra declarator grouping */
Matrix (foo); // A global variable
/* Extra declarator grouping in parameters */
void bar(Spam (Grok)(Doh));
实际上,很少(如果有的话)C程序员会写这样的代码,因为这种风格从来没有出现在编程书籍中。
- 不建议在c++源文件(.c、.cpp或.cxx文件中的代码)上运行SWIG。通常的方法是提供SWIG头文件来解析c++定义和声明。主要原因是,如果SWIG解析一个作用域定义或声明(对于c++源文件来说是正常的),它会被忽略,除非之前解析了符号的声明。例如
/* Bar不会被封装,除非foo已经被定义并且
foo中的bar声明已经被解析过了 */
int foo::bar(int) {
... whatever ...
}
- c++的某些高级特性(如嵌套类)还没有得到完全支持。C++ Nested classes
在出现解析错误时,可以使用条件编译跳过违规代码,或者直接从接口文件中删除有问题的代码。
SWIG不提供完整的c++解析器实现的原因之一是,它被设计为使用不完整的规范,并且在处理C/ C++数据类型时非常宽松(例如,即使缺少类声明或不透明的数据类型,SWIG也可以生成接口)。不幸的是,这种方法使得实现C/ c++解析器的某些部分变得极其困难。
3.2 C简单声明的封装
SWIG 通过创建一个接口文件来包装简单的 C 声明,与 C 程序中声明的使用方式非常地匹配。这里有一个接口文件的例子:
%module example
%inline %{
extern double sin(double x);
extern int strcmp(const char *, const char *);
extern int Foo;
%}
#define STATUS 50
#define VERSION "1.1"
在这个文件中,有两个函数 sin() 和 strcmp(),一个全局变量 Foo,以及两个常量 STATUS 和 VERSION。当 SWIG 创建扩展模块时,这些声明可以分别作为脚本语言的函数、变量和常量访问。
在python中调用
>>> example.sin(3)
5.2335956
>>> example.strcmp('Dave', 'Mike')
-1
>>> print example.cvar.Foo
42
>>> print example.STATUS
50
>>> print example.VERSION
1.1
3.2.1 基本类型处理
为了构建接口,SWIG必须将C/ c++数据类型转换为目标语言中的等效类型。通常,脚本语言提供的基本类型比c语言更有限,因此,这个转换过程涉及到一定程度的类型强制。
大多数脚本语言提供了一个单一的整数类型,在C语言中使用int或long数据类型实现。下面的列表显示了SWIG将在目标语言中转换为整数的所有C数据类型:
int short long unsigned signed unsigned short unsigned long unsigned char signed char bool
bool数据类型被转换为整数值0和1,除非目标语言提供了特殊的布尔类型。
如果整型值太大而无法容纳,则会静默地截断它。在处理大整数值时需要格外小心。大多数脚本语言使用32位整数,因此映射64位长整数可能会导致截断错误。
尽管SWIG解析器支持long long数据类型,但并不是所有语言模块都支持它。这是因为long long通常超过了目标语言中可用的整数精度。虽然long long是ISO C99标准的一部分,但并不是所有C编译器都支持它。
SWIG可以识别以下浮点类型:
float double
SWIG不支持很少使用的long double数据类型。
3.2.2 全局变量
只要有可能,SWIG就会将C/ c++全局变量映射为脚本语言变量。例如,
%module example
double foo;
3.2.3 常量
常量可以使用#define、枚举或特殊的%constant指令创建。下面的接口文件显示了一些有效的常量声明:
#define I_CONST 5 // An integer constant
#define PI 3.14159 // A Floating point constant
#define S_CONST "hello world" // A string constant
#define NEWLINE '\n' // Character constant
enum boolean {NO=0, YES=1};
enum months {JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG,
SEP, OCT, NOV, DEC};
%constant double BLAH = 42.37;
#define PI_4 PI/4
#define FLAGS 0x04 | 0x08 | 0x40
SWIG允许使用常量表达式,但是不计算它们。相反,它将它们传递给输出文件,并让C编译器执行最终的计算(不过,SWIG确实执行一种有限形式的类型检查)。
对于枚举,最初的枚举定义必须包含在接口文件的某处(在头文件中或%{%}块中),这一点至关重要。SWIG只将枚举转换为向脚本语言添加常量所需的代码。它需要原始的枚举声明,以便获得C编译器分配的正确枚举值。
%constant指令用于更精确地创建对应于不同C数据类型的常量。虽然简单值通常不需要它,但在处理指针和其他更复杂的数据类型时,它更有用。通常,%constant只在希望向脚本语言接口添加未在原始头文件中定义的常量时使用。
3.2.4 const限定符
C编程的一个常见混淆是声明中的const限定符的语义含义——特别是当它与指针和其他类型修饰符混合使用时。
从swg -1.3开始,所有变量声明,不管是否使用const,都被包装为全局变量。如果一个声明恰好被声明为const,它会被包装成一个只读变量。要判断一个变量是否为const,需要查看const限定符(出现在变量名之前)最右边的位置。如果最右边的const出现在所有其他类型修饰符(如指针)之后,则该变量为const。否则就不是。
下面是一些const声明的例子。
const char a; // A constant character
char const b; // A constant character (the same)
char *const c; // A constant pointer to a character
const char *const d; // A constant pointer to a constant character
下面是一个非const声明的例子:
const char *e; // A pointer to a constant character. The pointer
// may be modified.
在这种情况下,指针e可以改变——只有所指向的值是只读的。
在使用c++程序时,一个常见的问题是如何处理const限定符(或缺少)可能破坏程序、与程序链接的所有程序以及与这些程序链接的所有程序的所有方式。
尽管SWIG知道如何在其内部类型系统中正确地处理const,并且知道如何生成没有const相关警告的包装器,但SWIG不会尝试在目标语言中保留const正确性。因此,可以将const限定对象传递给非const方法和函数。例如,考虑以下c++代码:
const Object * foo();
void bar(Object *);
...
// C++ code
void blah() {
bar(foo()); // Error: bar discards const
};
// C++中这样写肯定是错误的,因为bar()函数参数为非const型
//但在swig生成的封装代码会丢弃const限定
bar(foo())//此代码在python中正常运行
3.2.5 char*
当字符串从脚本语言传递给C char *时,指针通常指向存储在解释器内的字符串数据。修改这些数据几乎总是一个非常糟糕的主意。此外,一些语言可能明确地不允许它。例如,在Python中,字符串被认为是不可变的。
问题的主要来源是可能就地修改字符串数据的函数。一个典型的例子是这样的函数:
char *strcat(char *s, const char *t)
尽管SWIG肯定会为此生成一个包装器,但它的行为将是未定义的。事实上,它可能会导致您的应用程序由于分段错误或其他内存相关问题而崩溃。这是因为s指的是目标语言中的一些内部数据——您不应该接触的数据。
注意:除了只读输入值,不要依赖char *。但是,必须注意的是,您可以使用typemaps更改SWIG的行为。
3.3 指针和复杂对象
大多数C程序操作数组、结构体和其他类型的对象。
3.3.1 简单指针
指向基本C数据类型的指针是被SWIG完全支持的,例如:
int * double *** char **
SWIG并不试图将指向的数据转换为脚本表示,而是简单地将指针本身编码为包含指针的实际值和类型标记的表示。
NULL指针由字符串“NULL”或用类型信息编码的值0表示。
所有指针都被SWIG视为不透明对象。因此,指针可以由函数返回,并根据需要传递给其他C函数。出于所有实际目的,脚本语言接口的工作方式与在C程序中使用指针完全相同。唯一的区别是没有对指针解引用的机制,因为这需要目标语言理解底层对象的内存布局。
指针值的脚本语言表示永远不应该被直接操作。
SWIG通常不会将指针映射到高级对象,如关联数组或列表(例如,int* 转为 整数列表)。这是因为C声明中没有足够的信息来正确地将指针映射到更高层次的结构;SWIG不知道与指针相关联的底层语义;而且通过以一致的方式处理所有指针,SWIG的实现大大简化,而且不容易出错。
3.3.2 运行时指针类型检查
和C语言一样,void *匹配任何类型的指针。此外,NULL指针可以传递给任何希望接收指针的函数。虽然这有可能导致崩溃,但NULL指针有时也被用作标记值或表示缺失的/空值。因此,SWIG将NULL指针检查留给应用程序。
3.3.3 派生类型、结构和类
对于其他内容(结构体、类、数组等)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)
3.3.4 未定义的数据类型
当SWIG遇到未声明的数据类型时,它自动假定它是一个结构或类。例如,假设以下函数出现在SWIG输入文件中:
void matrix_multiply(Matrix *a, Matrix *b, Matrix *c);
SWIG根本不知道"Matrix"是什么。但是,它显然是指向某个对象的指针,因此SWIG使用其通用指针处理代码生成一个包装器。
需要提到的一个重要细节是,当有未指定的类型名称时,SWIG很乐意为接口生成包装器。然而,所有未指定的类型都在内部作为指向结构或类的指针处理。例如,考虑下面的声明:
void foo(size_t num);
如果size_t未声明,则SWIG生成期望接收size_t *类型的包装器。这时如下调用会报错。
foo(40);
TypeError: expected a _p_size_t.
解决这个问题的唯一方法是确保正确地使用typedef声明类型名称。
3.3.5 Typedef
与C一样,typedef可以用于在SWIG中定义新的类型名称。
出现在SWIG接口中的typedef定义不会传播到生成的包装器代码。
因此,它们要么需要在包含的头文件中定义,要么像这样放在declarations部分:
%{
/* Include in the generated wrapper file */
typedef unsigned int size_t;
%}
/* Tell SWIG about it */
typedef unsigned int size_t;
或者
%inline %{
typedef unsigned int size_t;
%}
在某些情况下,您可以包含其他头文件来收集类型信息。例如:
%module example
%import "sys/types.h"
在这种情况下,在命令行命令中你可以这样运行SWIG:
$ swig -I/usr/include -includeall example.i
需要指出的是,系统头文件是出了名的复杂,并且可能依赖于各种非标准的C编码扩展(例如,例如对GCC的特殊指令)。除非您精确地指定了正确的include目录和预处理器符号,否则这可能无法正确工作。