Ubuntu16.04下通过 SWIG 将 C++ function/class/namespace 转换为 Python 下可调用的module


前言

由于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(&ltime);
  return ctime(&ltime);
}


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编译命令即可完成转换而不需要新学一个工具或者语言,确实是相当简单易用。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值