【python开发技术】`SWIG` 封装`python`接口的`C/C++`代码

`SWIG` 封装`python`接口的`C/C++`代码

Posted by ZhangPY on April 12, 2020

【python开发技术】SWIG 封装python接口的C/C++代码

何为SWIG

SWIG,全称 Simplified Wrapper and Interface Generator,可以将C/C++代码封装成pythonRuby以及Perl等语言脚本接口。

本文主要面向python接口的封装。

SWIG 封装python接口的C/C++代码

整个流程说明

  • *.i文件来声明所需接口;
  • 调用对应的 swig -python -c++ xxx.i 命令来自动生成对应的 *_wrap.cpython 文件;
  • python setuptools 使用拓展模块进行编译,生成对应的库文件以及封装的上层 python 使用接口。

它的好处在于,原来采用 python 提供的接口对 C/C++ 代码进行封装,需要用 python 提供的 C/C++ 开发库进行编写适配转化接口。而这里通过*.i文件来声明所需接口后,所有的这些适配接口,以及一个抽象层的代码都是自动生成的。

demo

一个常规的简单C代码

下面组织结构:.c文件和.h文件

// 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


// 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);
}

定义了一个全局变量 My_variable 以及三个功能函数 factmy_modget_time

期望的 python 接口

我们希望能够在 python 的脚本中按照下面的方式调用这些函数和全局变量:

# test.py

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())

这里函数可以直接被调用,对应的传参为基本数据类型可以自动转换; 而全局变量则是存在 example.cvar 当中,访问的时候调用 .cvar.

SWIG 文件需要做些什么

需要准备一个 example.i 文件,用来声明调用接口:


%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();

说明:

  1. %module 指定待访问的模块名,这里对应为 example;

除了 %module 之外,还有另外几种值得了解的类型:

  • %include 可以用来包含其它的 *.i 文件,比如常用的 %include "numpy.i"%include "std_vector.i"

numpy.i: a SWIG Interface File for NumPy,是 Numpyswig 文件。由于接口通常使用Numpy array 进行传参传数据,对于 swig 来讲想沟通numpy数组与C中指针,通过 typemap 来实现:

double rms(double* seq, int n);
def rms(seq):
    """
    rms: return the root mean square of a sequence
    rms(numpy.ndarray) -> double
    rms(list) -> double
    rms(tuple) -> double
    """
%{
#define SWIG_FILE_WITH_INIT
#include "rms.h"
%}

%include "numpy.i"

%init %{
import_array();
%}

%apply (double* IN_ARRAY1, int DIM1) {(double* seq, int n)};
%include "rms.h"

这样的一个列表 (double* IN_ARRAY1, int DIM1)被称为签名 signaturesdouble* IN_ARRAY1 用来说明 double* seq 是一个输入的一维的 array,而 int DIM1 用来说明维度大小。

类似地:

%apply (float* IN_ARRAY1, int DIM1) {(float* image, int len_image), (float* in, int len_in)}
%apply (float* INPLACE_ARRAY1, int DIM1) {(float * out, int len_out)}

对应

void bilateralfilter(float * image, int len_image, float * in, int len_in, float * out, int len_out, int H, int W, float sigmargb, float sigmaxy);

就这样类似地,将类型进行了明确。具体使用可以参考 https://docs.scipy.org/doc/numpy-1.13.0/reference/swig.interface-file.html

std_vector.i 库提供给 C++ STL 模板库中 vector的支持。

使用起来一个小小的 demos

/* File : example.h */

#include <vector>
#include <algorithm>
#include <functional>
#include <numeric>

double average(std::vector<int> v) {
    return std::accumulate(v.begin(),v.end(),0.0)/v.size();
}

std::vector<double> half(const std::vector<double>& v) {
    std::vector<double> w(v);
    for (unsigned int i=0; i<w.size(); i++)
        w[i] /= 2.0;
    return w;
}

void halve_in_place(std::vector<double>& v) {
    std::transform(v.begin(),v.end(),v.begin(),
                   std::bind2nd(std::divides<double>(),2.0));
}

用SWIG进行封装:

%module example
%{
#include "example.h"
%}

%include "std_vector.i"
// Instantiate templates used by example
namespace std {
   %template(IntVector) vector<int>;
   %template(DoubleVector) vector<double>;
}

// Include the header file with above prototypes
%include "example.h"

使用起来:

>>> from example import *
>>> iv = IntVector(4)         # Create an vector<int>
>>> for i in range(0,4):
...      iv[i] = i
>>> average(iv)               # Call method
1.5
>>> average([0,1,2,3])        # Call with list
1.5
>>> half([1,2,3])             # Half a list
(0.5,1.0,1.5)
>>> halve_in_place([1,2,3])   # Oops
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: Type error. Expected _p_std__vectorTdouble_t
>>> dv = DoubleVector(4)
>>> for i in range(0,4):
...       dv[i] = i
>>> halve_in_place(dv)       # Ok
>>> for i in dv:
...       print i
...
0.0
0.5
1.0
1.5
>>> dv[20] = 4.5
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "example.py", line 81, in __setitem__
    def __setitem__(*args): return apply(examplec.DoubleVector___setitem__,args)
IndexError: vector index out of range
  1. %{ %} 用来指定在 example_wrap.c 文件中所需要的声明,这里直接用 example.h 文件来代替,换成 example.h 中所声明的四个内容也是一样的;

  2. 剩下的四行,即为声明所需要的对外暴露的四个接口;

  3. 除了 % 部分的内容,其它均遵循 h 文件的语法;

这些完毕之后,直接使用 swig -python example.i,在 example.i 所在目录也会生成两个文件:example_wrap.cexample.py

编译 example_wrap.c

这里因为直接使用python脚本,那么直接使用setuptools来编写编译脚本,省去编写Makefile的步骤,即可:

from distutils.core import setup, Extension

import numpy

try:
    numpy_include = numpy.get_include()
except AttributeError:
    numpy_include = numpy.get_numpy_include()

example_module = Extension('_example',
                        sources=['example_wrap.c',
                                'example.c',],
                        extra_compile_args=["-fopenmp"],
                        include_dirs=[numpy_include]
                       )

setup(name='example',
      version="0.1",
      author="ZhangPY",
      description="Simple swig ext from docs",
      ext_modules=[example_module],
      py_modules=['example']
    )

注意:

  • Extension 的名称为 _example,因为生成的 example.py 为更进一步的封装调用,对 _example 进行的调用。所以 python setup.py build_ext 会在 build 目录下生成对应的 _example.cp36-win_amd64.pyd。将其与 example.py 放置在一起即可使用 example.py

参考文档

http://www.swig.org/Doc3.0/Python.html


20200412