前言
由于Semester project要求使用Python在一个C++开源项目上做算法实现,为了避免重复底层实现,需要将C++程序封装成Python可调用的包。目前有许多选择包括Cython,SWIG,boost.python, 不过考虑到不想对代码做改动所以选择了SWIG,因为SWIG仅仅需要添加一个interface文件即可,非常简单易行。但需要注意的是,如果输入输出不是C++标准类型 —— int, float, string等——的话,不支持自动转换,需要自行转换。
SWIG (Simplified Wrapper and Interface Generator)
官方链接为:http://www.swig.org/, 其官网的tutorial比较旧,很多相关的内容都不准确,不建议参考,本文主要参考来源是[1]和[2],主要工作是验证了如何进行生成C++ class的Python对象并进行调用。
1. installation
在命令行中输入以下命令即可安装SWIG
sudo apt install swig python3-dev
2. converting C++ to Python
2.1 介绍和简单测试
SWIG最为核心的部分是 interface 文件,这一文件指出了要将哪些函数接口暴露给Python来进行调用。interface文件以 .i 作为后缀名,基本格式如下:
%module example 声明在Python中调用时所使用的名字,例: import example
%{
#include "example.h" 声明编译所使用的头文件
%}
以下是暴露给Python的变量和函数,在Python中通过使用类似 example.fact(n) 的形式即可使用
extern double a;
extern int func(int n);
使用 interface 文件的方法如下所示,这一步生成c/c++对应的_wrap文件,供编译链接使用。
swig -python -c++ example.i
以下提供一个完整的示例:
# 参考自: http://note.qidong.name/2018/01/hello-swig-example/
1.新建example.c文件,粘贴以下内容,这里是几个简单函数的定义;
#include <time.h>
double My_variable = 3.0;
int fact(int n) {
if (n <= 1) return 1;
else return n*fact(n-1);
}
int my_mod(int x, int y) {
return (x%y);
}
char *get_time()
{
time_t ltime;
time(<ime);
return ctime(<ime);
}
2.新建example.h文件,粘贴以下内容,这是变量和函数声明;
#ifndef EXAMPLE_H
#define EXAMPLE_H
extern double My_variable;
extern int fact(int n);
extern int my_mod(int x, int y);
extern char *get_time();
#endif
3.新建example.i文件,粘贴以下内容,声明了可以被python调用的函数和变量名;
%module example
%{
#include "example.h"
%}
extern double My_variable;
extern int fact(int n);
extern int my_mod(int x, int y);
extern char *get_time();
4. 新建test.py文件,粘贴以下内容,这里主要是测试是否成功转为Python module;
import example
print('My_varaiable: %s' % example.cvar.My_variable)
print('fact(5): %s' % example.fact(5))
print('my_mod(7,3): %s' % example.my_mod(7,3))
print('get_time(): %s' % example.get_time())
5. 新建Makefile文件,粘贴以下内容,这里包括自动进行编译、链接、warping和测试;
_example.so : example.o example_wrap.o
gcc -shared example.o example_wrap.o -o _example.so -lpython3.5m
example.o : example.c
gcc -c -fPIC -I/usr/include/python3.5m example.c
example_wrap.o : example_wrap.c
gcc -c -fPIC -I/usr/include/python3.5m example_wrap.c
example_wrap.c example.py : example.i example.h
swig -python example.i
clean:
rm -f *.o *.so example_wrap.* example.py*
test:
python3 test.py
all: _example.so test
.PHONY: clean test all
.DEFAULT_GOAL := all
完成文件创建后,直接在该目录下打开命令行,输入 make ,有正常输出即可。
2.2 class,namespace
在上一小节中,进行测试的代码仅仅使用了 C支持的普通数据,并没有使用C++中的class和namespace等结构,但在实际中,大多数C项目都是C++项目并会包含至少一个class。对于这类情况,只需要改动Makefile内容和 Python的调用:
在makefile中:
gcc 改写为 g++ 以支持C++编译
在Python的测试文件中:
import example 改写为 from example import class, 然后直接使用 a = class(), a.func 即可
以下是一个示例:
1.新建number.cpp文件,粘贴以下内容,这里是一个namespace内一个class的几个简单函数的定义;
#include "number.h"
#include <iostream>
using namespace std;
namespace ns{
Number::Number(int start) {
data = start;
cout << "Number: " << data << endl; // cout and printf both work
}
Number::~Number( ) {
cout << "~Number: " << data << endl;
}
void Number::add(int value) {
data += value;
cout << "add " << value << endl;
}
void Number::sub(int value) {
data -= value;
cout << "sub " << value << endl;
}
void Number::display( ) {
cout << "Number = " << data << endl;
}
}
2.新建number.h文件,粘贴以下内容,这是namespace和class的声明;
#ifndef Number_H
#define Number_H
namespace ns{
class Number
{
public:
Number(int start);
~Number( );
void add(int value);
void sub(int value);
void display( );
int data;
};
}
#endif
3.新建number.i文件,粘贴以下内容,这里直接暴露头文件是使头文件中所有内容对Python可用;
%module number
%{
#include "number.h"
%}
%include "number.h"
4. 新建test.py文件,粘贴以下内容,这里主要是测试是否成功转为Python module以及是否可用;
from number import Number
num = Number(1)
num.add(4)
num.display()
num.sub(2)
num.display()
num.data = 99
print(num.data)
5. 新建Makefile文件,粘贴以下内容,这里包括自动对C++文件进行编译、链接、warping和测试;
_number.so : number.o number_wrap.o
g++ -shared number.o number_wrap.o -o _number.so -lpython3.5m
number.o : number.cpp
g++ -c -fPIC -I/usr/include/python3.5m number.cpp
number_wrap.o : number_wrap.cxx
g++ -c -fPIC -I/usr/include/python3.5m number_wrap.cxx
number_wrap.cxx number.py : number.i number.h
swig -python number.i
clean:
rm -f *.o *.so number_wrap.* number.py*
test:
python3 test.py
all: _number.so test
.PHONY: clean test all
.DEFAULT_GOAL := all
2.3 使用外部库
虽然上一小节的示例已经比较有代表性,但在实际中,很多C++的算法实现都或多或少使用了外部库,例如OpenCV和eigen,有的C++实现可能还需要较新的C++标准才支持。对于这一问题,只需要对makefile进行修改,即在编译时指定C++标准并给出外部库的位置和和相应命令即可,示例如下。
_patchmatch.so : patchmatch.o patchmatch_wrap.o
g++ -std=c++11 -shared patchmatch.o patchmatch_wrap.o -o _patchmatch.so `pkg-config opencv --libs` -lpython3.5m
patchmatch.o : patchmatch.cpp
g++ `pkg-config opencv --cflags` -std=c++11 -c -fPIC -I/usr/include/python3.5m patchmatch.cpp
patchmatch_wrap.o : patchmatch_wrap.cxx
g++ -std=c++11 -c -fPIC -I/usr/include/python3.5m patchmatch_wrap.cxx
patchmatch_wrap.cxx patchmatch.py : patchmatch.i patchmatch.h
swig -python -c++ patchmatch.i
clean:
rm -f *.o *.so patchmatch_wrap.* patchmatch.py*
test:
python3 test.py
all: _patchmatch.so test
.PHONY: clean test all
.DEFAULT_GOAL := all
示例中, -std=c++11 是指定使用C++11标准, `pkg-config opencv --libs` 则是给出程序中使用的opencv库的位置。如果未能指定正确的C++版本,编译将会报错某些操作,格式,变量定义不支持,需要使用C++11标准; 如果未能给出opencv库,在编译时将出现形如 _2N2FREEIPN not found in **.so 等错误,而且所显示的乱码 .so 文件中确实不存在。
3. 一些建议和值得注意的小问题
1. 尽量不要直接在 .i 文件中直接 %include " **.h " 来包含整个头文件,确定自己所需要的功能,只声明那些 function/class/namespace 即可,除非在Python中确实需要使用到所有的变量和函数。
2. SWIG不支持C++中 operator[] 的索引操作,所以尽量不要在 .i 文件中出现该操作符,否则编译时将报类似 warning: unsupported operator[] (using %extend)。虽然是warning,但不会生成_wrap.c/cxx 文件,将导致无法编译。不过,如果是.i文件中暴露的函数所调用的函数(该函数未被暴露)使用了 operator[], 由于是C++程序内部的实现,不会暴露给Python,因此不会报错。
3. 如果确实想要给Python module实现一个类似于 operator[] 的功能,可以考虑使用 python 提供的 __getitem__ 功能, 通过类似 %rename(__getitem__) operator[] 并 %extend __getitem__(int idx){ return $self [idx] } 的方式给一个不支持 operator[]的 wrapped Python module 扩展该功能。
Reference:
[1] http://web.mit.edu/ghudson/trac/src/swig-1.3.25/Doc/Manual/Python.html 关于SWIG非常详尽的文档;
[2] http://note.qidong.name/2018/01/hello-swig-example/ 通过简单函数转Python比较好地展示了SWIG的原理和步骤;
后记
花费了整个周日,算是了解了基本的一点SWIG用法,中间有许多坑,例如operator[],在.i 文件中暴露整个头文件以及第三方库的乱码等问题,不过目前接口数据类型的问题还在解决中。因为project中暴露的函数接口是cv::Mat,而opencv-python现在仅输出numpy.array,所以需要进行跨语言数据转换。所幸有前人实现过类似功能,等顺利完成再对使用的工具进行介绍。不过不得不承认,SWIG直接使用interface文件和cmake编译命令即可完成转换而不需要新学一个工具或者语言,确实是相当简单易用。