参考链接
https://docs.python.org/2/extending/building.html#building
python3官方文档: https://docs.python.org/3/extending/extending.html
https://www.tutorialspoint.com/python/python_further_extensions.htm
关键参考这个例子
https://github.com/JacekPierzchlewski/CppClass4Python3/blob/master/car/car2py.cpp
网上下载了Cython的书籍,已经上传nas
http://www.jyguagua.com/wp-content/uploads/2017/03/OReilly.Cython-A-Guide-for-Python-Programmers.pdf
直接看第八章吧,其他的说的都是用不上的。
https://notes-on-cython.readthedocs.io/en/latest/function_declarations.html
导言
首先先上结论,对于c++ 11,使用pybind11能够更加完美支持C++11的新特性,Cython更加适合纯C或者C++11之前的C++,包括对模板类以及MemoryView的支持。手工写这两者都需要大量的转换,最好是能够写一个自动wrap的小工具会好一些。Cython主要的问题在于对enum class的支持不足,其他的坑暂时还没有发现更多。
主要概念
需要认清以下几个主要的概念:
Extension
Python大部分的解释器都是CPython,也就是实际上就是一个能够运行.py文件的C程序,python是支持通过so的方式对这个C程序进行扩展的,这个扩展是相当于直接加在了运行时的解释器上面,而不是类似于解析器读取py文件运行这样的机制,因此称之为Python的扩展Extension而不是package,setup函数
setup函数来自setup_tools, setup_tools主要是改写了原本distutils里面的setup函数,我们一般只用setup_tools的setup函数,能够支持一些新的特性。
常见的开头长这样:1
2
3import setuptools
from setuptools import setup, Extension
from Cython.Build import cythonize
下面我们会具体说明使用到的cythonize函数,也就是Cython方案的核心之一。
Cython方案
Cython方案是诸多C++ wrapper中最接近python语法的一种,它能够比较好地用接近python的语法调用c++,不足之处就是对于enum class的支持非常差,导致无法在接口模块直接使用。为了使用一些C++支持而Cython不支持的特性,我们需要独立出来这部分代码,用纯C++来wrap一遍,去掉enum class这一类输入,用简单变量比如纯C的enum或者int来简单包装一下,再输入到最早需要接收enum class的函数中去。
1 | // shared_lib.h |
Cython编译整体有两个阶段,第一阶段就是pyx转c/c++, 第二阶段就是这个c/c++转换为python的extension。第一阶段由cython提供,第二阶段就用的python官方的setuptools来实现了。第二阶段还能加入很多纯C/C++的代码,也最灵活。
pyx和pyd
类比于C++的.cpp和.h文件,pyx和pyd就是Cython的实现和头文件,注意声明来自C的变量和函数一定要在pyd里面进行,否则编译的时候会出现符号重定义的现象,这和C++是一样一样的。
wrap函数
返回bool值
返回bool值得函数在这里需要用bint来替代作为python object
wrap变量
接收str变量到char*
1 | # void process_func(unsigned char*); |
接收numpy变量
numpy的array在传到C++层面时候,其存储不一定是连续的,因此不能够直接取array.data作为buffer的起点,而应该先转换为continuous array之后,这个array.data才是能够直接作为c++指针的起点。比如我们用opencv在python层读了一个图片进来,这时候要怎样变成一个unsigned char*
的指针呢?注意,这个指针指向的内存由python所管理,因此千万别尝试接管这部分内存,否则很可能出现内存泄漏。1
2
3
4
5
6
7cpdef receive_numpy(self, image_data):
row, col = image_data.shape[0], image_data.shape[1]
cdef np.ndarray[np.uint8_t, ndim=3, mode = 'c'] np_buff = np.ascontiguousarray(image_data, dtype = np.uint8)
cdef unsigned char* im_buff = <unsigned char*> np_buff.data
# do whatever you want with im_buff, but do not free the memory it points to.
# ...
pass
接收bool变量
有一个比较奇怪的现象,就是容器里面的bool不能用bint替换,因为没有默认的强制类型转换函数,必须使用原生的c++ bool1
2
3
4from libcpp cimport bool
cdef extern from "wrapper.h":
cdef struct Foo:
vector[bool] boolFlags
shared library的依赖管理
我们给shared library添加wrapper, 在这样的使用场景下,so库是预编译的,拿到的时候应该就是so库及其相应的头文件,我们知道,python作为C程序的一种,他要想要使用到这个so库,加载的方式和传统的C程序也是没有什么两样的,比如搜索/usr/lib, /lib, ${LD_LIBRARY_PATH}, RPATH等等,因为我们希望在pip install 和uninstall的时候,pip能够管理我们全部生成的文件,在卸载的时候也能够卸载干净,因此我们不可能将so库放在/usr/lib或者/lib这样的全局位置,并且这样的操作setuptools默认是不支持的,且需要sudo权限。而设置LD_IBRARY_PATH则需要在启动python之前就完成设置,一旦python启动,后续不能够再设置,当然也有办法是python设置完之后,重启python,这种骚操作我觉得还是少用为妙。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import sys, os
mymodule_path = "/home/me/python/mymodule"
try:
sys.path.append(mymodule_path)
import mymodule
except ImportError:
if sys.platform == 'win32':
os.environ['PATH'] = mymodule_path
elif sys.platform == 'darwin':
os.environ['DYLD_LIBRARY_PATH'] = mymodule_path
else:
os.environ['LD_LIBRARY_PATH'] = mymodule_path
args = [sys.executable]
args.extend(sys.argv)
os.execv(sys.executable, args)
那么就只剩下一个关键的途径了,那就是设置RPATH,也就是runtime_library_dirs了,这是写在so库中的一个路径,我们可以用readelf -d xxx.so
来看到,也可以用ldd xxx.so
来看到如果在ldd当前目录下运行程序,有什么样的xxx.so依赖的其他so库能够被找到。RPATH有一个特殊的符号,$ORIGIN
,注意这个不是一个变量,而是一个特定的符号,他表示,无论这个so被扔到那里了,$ORIGIN就是so所在的目录位置,于是,相对路径就变得可行起来了,比如我们添加这样的一行编译:1
2
3set_target_properties(xxx PROPERTIES LINK_FLAGS "-Wl,-rpath,$ORIGIN")
或者
set_target_properties(xxx PROPERTIES INSTALL_RPATH "$ORIGIN")
而在setuptools里面,则是在Extension加上这个property:1
ext = Extension(runtime_library_dirs=["$ORIGIN"], ... , )
再次提醒,这个$ORIGIN并不会被替换成其他的变量,因为这是一个特殊符号。但是,这个只解决了由wrapper直接依赖的so的问题,假设我们的模块是mylib, mylib依赖的A.so可以由上述方式找到,但是A.so可能依赖另外一些shared lib, 比如B.so, 这就要求A.so在编译的时候就已经将RPATH设置正确,否则也是没有办法的,不过一般A.so, B.so这些也都是我们能够接触到源码的,所以改一下编译也不是难题,并且实在不行还可以用patchelf这个工具做后处理修改掉rpath.
Cython打包过程
像我们这种打包shared lib的,不可避免需要将被wrap的so安装到sitepackage里面去,那么怎么实现这件事呢?那就是用package_data啦,注意,package_data只能打包在package所在的文件夹之内的数据文件,不在这个路径下的,是不能被打包的,也不会报错。并且,某个文件要想被打包,其顶层一定有一个 \_init__.py 作为当前文件夹是package的声明。比如:
1 | |--data |
在setup.py里面,要想打包data里面的内容,那么:
只需要setup(package_data={'data': ["model.*"]}, ..., )
即可,当然目录还能更加复杂一些。
python setup.py install/sdist/bdist/bdist_wheel
install为直接安装,一般就是安装到本机
sdist作用基本相当于直接将当前源码打包,然后放到其他机器同样运行python setup.py install来安装,就是所谓的源码发布
bdist作用相当于将pyx, pyd等编译之后只取二进制文件,实际上并不好用
bdist_wheel是我觉得最好用的,它切掉了全部的源码,只留下必要的文件。
所有需要被打包的文件,都通过MANIFEST.in来进行指定。
static问题
由于python的extension在不同的系统,动态库其symbol的可见性是不一样的,比如windows就全局共享一个可见性,因此建议所有的extension的函数都带上static,只有模块的初始化函数XXX_Init除外。