这是我的《C 拓展 Python 实战》系列的第五篇,也是这个系列最后一篇。
在之前的文章中,我们的模块需要和 Python 一起编译,这次我们来看看如何使用 setup.py 来随时编译我们的模块。
在此之上,我们再来看看如何结合 CMake 把复杂的 C/C++ 项目打包成 Python Package。
一、使用 setup.py 来打包 C/C++ 模块为 Python Package
新建一个项目目录,结构如下:
1 2 3 4 5 6 py-cmodule-demo # 项目目录 ├── demo # 常规的 python module │ ├── __init__.py # 常规的 python 文件 ├── mymath # c 模块目录 │ └── mymath.c # c 模块代码 └── setup.py # 安装脚本
mymath.c
文件的内容和我们之前的一样,是用 C 写的 Pyhon 模块:
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 #include <Python.h> PyObject *sum (PyObject *self, PyObject *args) { int num1, num2; PyArg_ParseTuple(args, "ii" , &num1, &num2); return Py_BuildValue("i" , num1 + num2); } static PyMethodDef MymathMethods[] = { {"sum" , sum, METH_VARARGS, "add up two numbers and return their sum" }, {NULL } }; static struct PyModuleDef mymath_module = { PyModuleDef_HEAD_INIT, "mymath" , PyDoc_STR("A hello world example to demonstrate writing a python module in c language" ), -1 , MymathMethods }; PyMODINIT_FUNC PyInit_mymath (void ) { PyObject *m = PyModule_Create(&mymath_module); return m; }
最关键的部分在 setup.py
,安装时所有的依赖和行为都在这里定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from setuptools import setupfrom setuptools import Extensionmymath = Extension("mymath" , sources=["mymath/mymath.c" ], include_dirs=["/usr/include/python3.6m" ], library_dirs=["/usr/lib/x86_64-linux-gnu/" ] ) setup(name="demo" , version="1.0" , description="This is a demo package" , packages=["demo" ], ext_modules=[mymath] )
最基本的框架就如上所示,比较简单。
下面我们来试试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $ python setup.py install $ python Python 3.6.9 (default, Feb 26 2020, 21:43:10) [GCC 7.4.0] on linux Type "help", "copyright", "credits" or "license" for more information. > >> import mymath > >> help (mymath) > >> mymath.__doc__ 'A hello world example to demonstrate writing a python module in c language' > >> mymath.sum(99, 1) 100 > >>
二、setup.py 如何使用 CMake 来编译复杂的项目
上面使用 setup.py
编译了一个简单的 C 模块。不过它的弊端也很明显,那就是需要在 setup.py
里面组织 C 项目的结构,包括指定 sources, include_dirs, libraries 和其它编译选项。当项目变得复杂的时候,setup.py
也就变得异常难以管理。
而实际上,C 和 C++ 本身就有强大的 CMake 这个工具来进行项目管理了。在 setup.py
里面能不能直接用 CMake 呢?
我都在写这篇文章了,答案当然是有。不过有一些细节需要注意到。
加入 CMakeLists.txt
,并修改 setup.py
:
1 2 3 4 5 6 7 8 py-cmodule-demo ├── demo │ ├── __init__.py ├── mymath │ ├── CMakeLists.txt # 加入 CMakeLists.txt │ └── mymath.c └── setup.py # 修改 setup.py
先来看看 CMakeLists.txt
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 project (mymath)cmake_minimum_required (VERSION 3.10 )set (Python3_USE_STATIC_LIBS FALSE )find_package (Python3 COMPONENTS Interpreter Development)set (SOURCES "" )set (INCLUDES "" )set (LIBRARIES "" )list (APPEND SOURCES mymath.c)list (APPEND INCLUDES ${Python3_INCLUDE_DIRS} )list (APPEND LIBRARIES ${Python3_LIBRARIES} )include_directories (${INCLUDES} )add_library (mymath SHARED ${SOURCES} )target_link_libraries (mymath PUBLIC ${LIBRARIES} )set_target_properties (mymath PROPERTIES OUTPUT_NAME "mymath" PREFIX "" )
这个 CMakeLists.txt
文件虽小但是五脏俱全。尤其需要注意最后一行,当时我在这里卡了很久。
接下来对 setup.py
进行修改,让它能够根据 CMake 进行编译:
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 import osimport pathlibfrom setuptools import setupfrom setuptools import Extensionfrom setuptools.command.build_ext import build_extclass CMakeExtension (Extension ): """ 自定义了 Extension 类,忽略原来的 sources、libraries 等参数,交给 CMake 来处理这些事情 """ def __init__ (self, name ): super ().__init__(name, sources=[]) class BuildExt (build_ext ): """ 自定义了 build_ext 类,对 CMakeExtension 的实例,调用 CMake 和 Make 命令来编译它们 """ def run (self ): for ext in self.extensions: if isinstance (ext, CMakeExtension): self.build_cmake(ext) super ().run() def build_cmake (self, ext ): cwd = pathlib.Path().absolute() build_temp = f"{pathlib.Path(self.build_temp)} /{ext.name} " os.makedirs(build_temp, exist_ok=True ) extdir = pathlib.Path(self.get_ext_fullpath(ext.name)) extdir.mkdir(parents=True , exist_ok=True ) config = "Debug" if self.debug else "Release" cmake_args = [ "-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=" + str (extdir.parent.absolute()), "-DCMAKE_BUILD_TYPE=" + config ] build_args = [ "--config" , config, "--" , "-j8" ] os.chdir(build_temp) self.spawn(["cmake" , f"{str (cwd)} /{ext.name} " ] + cmake_args) if not self.dry_run: self.spawn(["cmake" , "--build" , "." ] + build_args) os.chdir(str (cwd)) mymath = CMakeExtension("mymath" ) setup(name="demo" , version="1.1" , description="This is a demo package" , packages=["demo" ], ext_modules=[mymath], cmdclass={"build_ext" : BuildExt} )
对 setup.py
主要的改动为:自定义 Extension 类,并通过自定义的 build_ext 类对这些实例通过 CMake 和 make 进行编译。
执行 python setup.py build
后,观察 build
文件夹下面的编译输出,会发现这样的文件结构:
1 2 3 4 5 6 7 8 9 10 build ├── bdist.linux-x86_64 ├── lib.linux-x86_64-3.6 │ ├── demo # 这其实就是对 demo 文件夹的一个复制,常规的 python module 打包时都是如此操作 │ │ └── __init__.py │ ├── mymath.cpython-36m-x86_64-linux-gnu.so # 这是一个空文件,不用管它 │ └── mymath.so # 这是 cmake 编译出来的文件,我们在 CMakeLists.txt 里面取消了 ‘lib’ 前缀。这样 python 就能直接 “import mymath” 了 └── temp.linux-x86_64-3.6 └── ...
执行 python setup.py install
就能安装了。
三、总结
C 项目除了在 CMake 中删除输出文件的 ‘lib’ 前缀,几乎不用做任何改动。setup.py
文件通过自定义的 Extension 类和 build_ext 类调用 CMake 和 Make 来编译 C 模块。
写代码时继续用 CMake 来组织程序,发布时再使用 setup.py
轻轻松松地打包成 Python Package,美滋滋~
虽然文章中用的实例非常简单,但是这种解决方案完全适用大型的 C 程序。更多的细节可以参考我的项目 font2png ,它使用强大而复杂的 fontforge 开源库把字体文件转换为 png 图片。在这个项目中,fontforge 的模块就全部是通过 setup.py
来编译的。
引用
Building C and C++ Extensions with distutils
Extending setuptools extension to use CMake in setup.py?