C/C++ 和 Python 之间的交互 2019-04-27 # C/C++ 和 Python 之间的交互 Tensorflow 的核心代码是 C 和 C++ 实现然后提供 Python 的接口,使用者无需关心 C++ 的细节,直接使用 Python 就能调用相关功能,而且这些功能多是比较复杂且耗时的操作。类似的有 caffe , xgboost 的工具包。 C++ 和 Python 之间的交互有很多种,而且在不同场景下可以选择不同的方式。这篇文章列举一些常用的方式,并给出简单的例子方便理解和选择不同方式来解决自己的问题。 ## 直接调用 CPython 的接口 Python 官方文档介绍了直接在 C 中调用 CPython [接口](<https://docs.python.org/3/c-api/index.html>)的方式来实现 Python 扩展。这需要了解 Python 提供的接口,然后利用 distutils/setuptools 来构建扩展 如下面的例子,C 代码是现在 ```hello.c``` 文件中,然后写 ```setup.py``` 运行 ```python setup.py build_ext —inplace``` 然后就可以在当前目录下直接 ```import hello``` ``` include <stdio.h> #include <Python.h> static PyObject* hello_world(PyObject *self, PyObject *args) { printf("Hello, world!\n"); Py_RETURN_NONE; } static PyObject* hello(PyObject *self, PyObject *args) { const char* name; if (!PyArg_ParseTuple(args, "s", &name)) { return NULL; } printf("Hello, %s!\n", name); Py_RETURN_NONE; } static PyMethodDef hello_methods[] = { { "hello_world", hello_world, METH_NOARGS, "Print 'hello world' from a method defined in a C extension." }, { "hello", hello, METH_VARARGS, "Print 'hello xxx' from a method defined in a C extension." }, {NULL, NULL, 0, NULL} }; static struct PyModuleDef hello_definition = { PyModuleDef_HEAD_INIT, "hello", "A Python module that prints 'hello world' from C code.", -1, hello_methods }; PyMODINIT_FUNC PyInit_hello(void) { Py_Initialize(); return PyModule_Create(&hello_definition); } ``` ```python # setup.py from distutils.core import setup, Extension hello_module = Extension('hello', sources = ['hello.c']) setup(name='hello', version='0.1.0', description='Hello world module written in C', ext_modules=[hello_module]) ``` 官放还提供了一种方式直接把整个 CPython 嵌入到项目里,有需要可以查看[详细文档](<https://docs.python.org/3/extending/embedding.html>) ## Cython 按官方的实现方式我们需要按照 C/C++ 去实现功能,[Cython ](<http://docs.cython.org/en/latest/index.html>) 的想法是用 Python 本身的语法去实现功能,然后 Cython 会翻译 对应的 ```.c``` 文件,然后按照上面的方式利用 distutils/setuptools 来构建扩展。Cython 的实现一般放在 ```.pyx``` 的后缀文件中。下面还是一个例子. hello.pyx ```python def say_hello_to(name): print("Hello %s!" % name) ``` setup.py ```python rom distutils.core import setup from Cython.Build import cythonize setup(name='Hello world app', ext_modules=cythonize("hello.pyx")) ``` 在当前目录执行```python setup.py build_ext —inplace``` 可以得到动态对应的 ```.c``` 文件和编译好的动态链接库。 在当前目录可以直接import 当然你可以 ```python setup.py install ``` 来安装到系统下。 ## ctypes 这是在 Python 官方标准库中实现的包,相对于直接调用 CPython 的接口的优雅之处在于,我们只需要写 C 代码然后剩下的交给 ctypes 去处理接好了,我们不需要去花时间了解 CPython 的接口。 hello.c ```c #include <stdio.h> void hello(void); void hello() { printf("hello world"); } ``` 编译: ```gcc -shared -Wl,-install_name,hello.so -o hello.so -fPIC hello.c``` 然后就可以在 ```hello.py`` 中直接调用 hello.py ```python import ctypes hello = ctypes.CDLL('hello.so') hello.hello() ``` 类似的还有[cffi](<https://cffi.readthedocs.io/en/latest/goals.html>) ## swig [swig](<http://www.swig.org/index.php>) 强大之处在于可以把 C/C++ 写的东西和十几种其他语言进行交互,这其中就包括 Python 。但是也需要我们写一个```.i``` 后缀的接口文件。swig 有优势也有很多不足,[Stack Overflow](<https://stackoverflow.com/questions/135834/python-swig-vs-ctypes>) 的比较 ctypes 和 swig 的问题下一答者记录了他在使用 Swig 不同时间段的不同感受. Tensorflow 的实现方式就是利用了 swig 来实现的。下面是一个官方的例子. example.c ```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); } ``` example.i ``` /* example.i */ %module example %{ /* Put header files here or function declarations like below */ extern double My_variable; extern int fact(int n); extern int my_mod(int x, int y); extern char *get_time(); %} extern double My_variable; extern int fact(int n); extern int my_mod(int x, int y); extern char *get_time(); ``` 构建 ```shell swig -python example.i gcc -c example.c example_wrap.c -I/usr/local/include/python2.7 ld -shared example.o example_wrap.o -o _example.so ``` 当然可以自己修改 Python 的路径 使用的时候就直接在 Python 中 ```import example``` ## pybind11 如果是使用 C++ 还可以用 [pybind11]()<https://github.com/pybind/pybind11> 比 swig 更轻,与之类似的还有 [Boost.Python](<https://www.boost.org/doc/libs/1_58_0/libs/python/doc/>) caffe 的 Python 就可以用 Boost.Python 编译。如果你主要使用的语言是 C++ 但是要给使用 Python 的用户提供相对友好的接口就可以使用这种方式。 example.c ```c #include <pybind11/pybind11.h> int add(int i, int j) { return i + j; } PYBIND11_MODULE(example, m) { m.doc() = "pybind11 example plugin"; // optional module docstring m.def("add", &add, "A function which adds two numbers"); } ``` 编译 ``` c++ -O3 -Wall -shared -std=c++11 -fPIC `python3 -m pybind11 --includes` example.cpp -o example`python3-config --extension-suffix` ``` 然后就可以在 Python 中直接导入了,前提是的安装 pybind11 。 ## 总结 Python 语言本身是一种表达方式,通过 Python 解释器翻译成其他语言,由于 CPython 是 C 语言实现所有比较方便的可以在 C/C++ 和 Python 之间进行调用,问题就是这个翻译工作谁来做,怎么做,以及翻译方式是否方便和维护。