深圳幻海软件技术有限公司 欢迎您!

如何把C/C++程序编译成实用的Python模块

2023-02-26

 在Python遇到性能瓶颈时怎么办?答案是找对应功能的C/C++程序,把它编译成CPython模块,供Python调用来提高性能。如何把C/C++程序编译成Python模块比如Python中做科学计算,数据处理的Numpy模块就是使用C语言编写的,Numpy处理速度比Pandas快数倍。

 

在Python遇到性能瓶颈时怎么办?答案是找对应功能的C/C++程序,把它编译成CPython模块,供Python调用来提高性能。

如何把C/C++程序编译成Python模块

比如Python中做科学计算,数据处理的Numpy模块就是使用C语言编写的,Numpy处理速度比Pandas快数倍。Numpy的处理速度一点都不比go语言差。

本文就是介绍如何把C/C++程序编译成Python模块。本文偏技术,需要耐着性质看。

Python 作为一个胶水语言,可以很方便的通过C/C++来进行扩展,提高性能。前面我写了一篇文章介绍如何通过Python的ctypes加载普通的.so库。

其实,这还不算真正的用C/C++写Python的扩展模块。

本文将介绍如何使用C语言和C++写Python模块。

一、Python的C语言接口

Python语言最初是用C语言实现的一种脚本语言,后来被称为CPython,是因为后来它语言实现的Python,比如Python实现的Python——PyPy,Java语言实现的Python——Jython,.Net实现的Python——IronPython。

CPython具有优良的开放性和可扩展性,并提供了方便灵活的应用程序接口(API),从而使得C/C++程序员能够对Python解释器的功能进行扩展。

Python的C语言接口很适合封装C语言实现的各种函数,如果要封装C++的类,使用boost_python或者SWIG更方便和合适,还有一个类似boost_python的支持C++11的pybind11。

1 模块封装

假设我们有一个C函数:

/* 文件名:mylib.c */ 
int addone(int a) { 
 return a+1; 

  • 1.
  • 2.
  • 3.
  • 4.

如果想在Python解释器中调用该函数,则应该首先将其实现为Python中的一个模块,这需要编写相应的封装接口,如下所示:

/* wrap_mylib.c */ 
#include  
#include "mylib.h" 
PyObject* wrap_addone(PyObject* self, PyObject* args) 

 int n, result; 
 if (! PyArg_ParseTuple(args, "i:fact", &n)) 
 return NULL; 
 result = addone(n); /*这里调用C函数 */ 
 return Py_BuildValue("i", result); 

static PyMethodDef mylibMethods[] = 

 {"addone", wrap_addone, METH_VARARGS, "Add one to N"}, 
 {NULL, NULL} 
}; 
void initmylib() 

 PyObject* m; 
 m = Py_InitModule("mylib", mylibMethods); 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

上面就是一个典型的Python扩展模块,它至少应该包含三个部分:导出函数、方法列表和初始化函数。

2 导出函数

要在Python解释器中调用C语言中的某个函数,首先要为它编写对应的导出函数,上述例子中的导出函数为wrap_addone。在Python的C语言扩展中,所有的导出函数都具有相同的函数原型:

PyObject* wrap_method(PyObject* self, PyObject* args); 
  • 1.

这个函数是Python解释器和C函数进行交互的接口,一般以wrap_开头后面跟上C语言的函数名,这样命名把导出函数和C语言函数对应起来使得代码更加清晰。它带有两个参数:self和args。

参数self 只在C函数被实现为内联方法(built-in method)时才被用到,通常该参数的值为空(NULL)。

参数args 中包含了Python解释器要传递给C函数的所有参数,通常使用Python的C语言扩展接口提供的函数PyArg_ParseTuple()来获得这些参数值。

所有的导出函数都返回一个PyObject指针,如果对应的C函数没有真正的返回值(即返回值类型为void),则应返回一个全局的None对象(Py_None),并将其引用计数增1,如下所示:

PyObject* wrap_method(PyObject *self, PyObject *args) 

 Py_INCREF(Py_None); 
 return Py_None; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

3 方法列表

方法列表中列出了所有可以被Python解释器使用的方法,上述例子对应的方法列表为:

static PyMethodDef mylibMethods[] = 

 {"addone", wrap_addone, METH_VARARGS, "Add one to N"}, 
 {NULL, NULL} 
}; 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

方法列表中的每项由四个部分组成:

方法名

导出函数

参数传递方式

方法描述

方法名是从Python解释器中调用该方法时所使用的名字。

参数传递方式则规定了Python向C函数传递参数的具体形式,可选的两种方式是METH_VARARGS和METH_KEYWORDS,其中METH_VARARGS是参数传递的标准形式,它通过Python的元组在Python解释器和C函数之间传递参数,若采用METH_KEYWORD方式,则Python解释器和C函数之间将通过Python的字典类型在两者之间进行参数传递。

4 初始化函数

所有的Python扩展模块都必须要有一个初始化函数,以便Python解释器能够对模块进行正确的初始化。Python解释器规定所有的初始化函数的函数名都必须以init开头,并加上模块的名字。对于模块mylib来说,则相应的初始化函数为:

void initmylib() 

 PyObject* m; 
 m = Py_InitModule("mylib", mylibMethods); 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

当Python解释器需要导入该模块时,将根据该模块的名称查找相应的初始化函数,一旦找到则调用该函数进行相应的初始化工作,初始化函数则通过调用Python的C语言扩展接口所提供的函数Py_InitModule(),来向Python解释器注册该模块中所有可以用到的方法。

5 编译链接

要在Python解释器中使用C语言编写的扩展模块,必须将其编译成动态链接库的形式。下面以Linux为例,介绍如何将C编写的Python扩展模块编译成动态链接库:

$ gcc -fpic -shared -o mylib.so  
 -I/usr/include/python2.7  
 mylib.c wrap_mylib.c 
  • 1.
  • 2.
  • 3.

6 在Python中调用

上面编译生成的Python扩展模块的动态链接库,可以在Python中直接import。如下所示:

veelion@gtx:~$ python 
Python 2.7.12 (default, Nov 19 201606:48:10
[GCC 5.4.0 20160609] on linux2 
Type "help""copyright""credits" or "license" for more information. 
>>> import example 
>>> example.addone(7
8 
>>> 
>>> 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

这里生成的.so动态库和上一篇中不用Python的C语言生成的动态库是不一样的,从生成过程和使用方法就可以看出来,这里的动态库使用起来感觉就是一个Python模块,直接import就可以了。

二、用boost_python库封装C++类

安装boost python库:

sudo aptitude install libboost-python-dev 
  • 1.

示例

下面代码简单实现了一个普通函数maxab()和一个Student类:

#include  
#include  
int maxab(int a, int b) { return a>b?a:b; } 
class Student { 
 private: 
 int age; 
 std::string name; 
 public: 
 Student() {} 
 Student(std::string const& _name, int _age) { name=_name; age=_age; } 
 static void myrole() { std::cout << "I'm a student!" << std::endl; } 
 void whoami() { std::cout << "I am " << name << std::endl; } 
 bool operator==(Student const& s) const { return age == s.age; } 
 bool operator!=(Student const& s) const { return age != s.age; } 
}; 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

使用boost.python库封装也很简单,如下代码所示:

#include  
#include <boost/python.hpp> 
#include <boost/python/suite/indexing/vector_indexing_suite.hpp> 
#include  
#include "student.h" 
using namespace boost::python; 
BOOST_PYTHON_MODULE(student) {  
 // This will enable user-defined docstrings and python signatures, 
 // while disabling the C++ signatures 
 scope().attr("__version__") = "1.0.0"
 scope().attr("__doc__") = "a demo module to use boost_python."
 docstring_options local_docstring_options(true, false, false); 
 def
 "maxab", &maxab, "return max of two numbers. 

 );  
 class_("Student""a class of student"
 .def(init<>()) 
 .def(init<std::string, int>()) 
 // methods for Chinese word segmentation 
 .def
 "whoami", &Student::whoami, "method's doc string..." 
 )  
 .def
 "myrole", &Student::myrole, "method's doc string..." 
 )  
 .staticmethod("myrole"); 
 // 封装STL 
 class_<std::vector >("StudentVec"
 .def(vector_indexing_suite<std::vector >()) 
 ;  

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.

上述代码还是include了Python.h文件,如果不include的话,会报错误:

wrap_python.hpp:50:23: fatal error: pyconfig.h: No such file or directory 
  • 1.

编译

编译以上代码有两种方式,一种是在命令行下面直接使用g++编译:

g++ -I/usr/include/python2.7 -fPIC wrap_student.cpp -lboost_python -shared -o student.so 
  • 1.

首先指定Python.h的路径,如果是Python 3的话就要修改为相应的路径,编译wrap_student.cpp要指定-fPIC参数,链接(-lboost_python)生成动态库(-shared)。生成的student.so动态库就可以被python直接import使用了

In [1]: import student 
In [2]: student.maxab(25
Out[2]: 5 
In [3]: s = student.Student('Tom'12
In [4]: s.whoami() 
I am Tom 
In [5]: s.myrole() 
I'm a student! 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

另外一直方法是用python的setuptools编写setup.py脚本:

#!/usr/bin/env python 
from setuptools import setup, Extension 
setup(name="student"
 ext_modules=[ 
 Extension("student", ["wrap_student.cpp"],  
 libraries = ["boost_python"]) 
]) 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

然后执行命令编译:

python setup.py build 
or 
sudo python setup.py install 
  • 1.
  • 2.
  • 3.

三、SWIG封装C++类

Python调用C/C++代码的利器除了boost_python外,还有SWIG(Simplified Wrapper and Interface Generator),它是用来为脚本语言调用C和C++程序的软件开发工具,它实际上是一个编译器,获取C/C++的声明和定义,用一个壳封装起来,以便其它脚本语言访问这些声明。所以,SWIG 最大的好处就是将脚本语言的开发效率和 C/C++ 的运行效率有机的结合起来。

一个双数组Trie Tree的实现:cedar在中文分词、新词发现等算法中可以y用于词典的创建。本文以cedar的SWIG封装实现来说明SWIG的使用。

0. 安装swig

工欲善其事必先利其器,首先要安装swig,Ubuntu安装swig很简单:

sudo aptitude install swig 
  • 1.

1. 声明和定义C/C++代码

在cedar的swig目录下面有cedar的C++声明和实现代码trie.h,但是这个实现里面没有遍历所有key的函数方法,所以我添加了一个实现,首先定义一个数据结构来定义key:

// key-value pair return type for next_key() 
class kv_t { 
 public: 
 std::string key; 
 int value; 
}; 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

添加一个函数每次返回一个key,当key字符串为空时表示遍历结束,继续调用的话就又从头开始遍历:

// to iterate all keys 
kv_t next_key() const { 
static size_t from = 0, p = 0
union { int i; int x; } b; 
char key[256] = {0}; 
kv_t kv; 
if(from == 0) { 
b.i = _t->begin(from, p); 
}else
b.i = _t->next(from, p); 

if (b.i == trie_t::CEDAR_NO_PATH) { 
kv.key = ""; 
kv.value = 0
from = 0
p = 0
return kv; 

_t->suffix(key, p, from); 
kv.key = key; 
kv.value = b.x; 
return kv; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.

2. 编写接口文件.i

查看cedar.i可以看到SWIG的接口文件的编写规则:

首先在 %module 后面声明模块名称,这就是Python在import时使用的模块名称;

在%{ … %}之间包含相关头文件

在%include 后面可以声明对STL的支持

最后声明要封装的函数和变量,也可以之间包含头文件:%include “trie.h”

3. 封装代码

可以在Makefile里面看到python-bindings:

python-bindings: 
 swig -Wall -python -builtin -outdir python -c++ cedar.i 
 mv -f cedar_wrap.cxx python 
  • 1.
  • 2.
  • 3.

直接make或者单独运行上面的swig命令,就可以生成cedar.py和cedar_wrap.cxx文件。

4. 编译生成动态库

编译生成的cedar_wrap.cxx使用python distutils的setup,可以参考python/setup.py的编写。setup.py的build如下:

python setup.py build 
  • 1.

就会在当前目录下面创建目录build,下面生成lib.linux-x86_64-2.7/cedar.py 和 _cedar.so

四、 pybind11封装C++

从pybind11的名字可以看出,它是用来封装C++ 11代码为Python模块的库。它的目标和用法都是想Boost_python库看齐,但是它又比庞大的Boost库精简。我知道这个库的时间不长,也没有具体实践过。以前都是写C++,然后用boost封装。但是,感觉pybind11更简洁,所以下一个项目可以试试它。到时候再分享使用心得给大家。