pybind11,理由如下:参考官方的Setuptools构建文档
这种方式适合python包的构建、打包、分发、上传到PyPi一条龙服务。python使用C++扩展需要在setup.py里配置好Extension。以下是一个setup.py的样例:
import globimport os.pathfrom distutils.core import setup__version__ = "0.0.1"# make sure the working directory is BASE_DIRBASE_DIR = os.path.dirname(__file__)os.chdir(BASE_DIR)ext_modules = []try: from pybind11.setup_helpers import Pybind11Extension, ParallelCompile, naive_recompile # `N` is to set the bumer of threads # `naive_recompile` makes it recompile only if the source file changes. It does not check header files! ParallelCompile("NPY_NUM_BUILD_JOBS", needs_recompile=naive_recompile, default=4).install() # could only be relative paths, otherwise the `build` command would fail if you use a MANIFEST.in to distribute your package # only source files (.cpp, .c, .cc) are needed source_files = glob.glob('source/path/*.cpp', recursive=True) # If any libraries are used, e.g. libabc.so include_dirs = ["INCLUDE_DIR"] library_dirs = ["LINK_DIR"] # (optional) if the library is not in the dir like `/usr/lib/` # either to add its dir to `runtime_library_dirs` or to the env variable "LD_LIBRARY_PATH" # MUST be absolute path runtime_library_dirs = [os.path.abspath("LINK_DIR")] libraries = ["abc"] ext_modules = [ Pybind11Extension( "package.this_package", # depends on the structure of your package source_files, # Example: passing in the version to the compiled code define_macros=[('VERSION_INFO', __version__)], include_dirs=include_dirs, library_dirs=library_dirs, runtime_library_dirs=runtime_library_dirs, libraries=libraries, cxx_std=14, language='c++' ), ]except ImportError: passsetup( name='project_name', # used by `pip install` version='0.0.1', description='xxx', ext_modules=ext_modules, packages=['package'], # the directory would be installed to site-packages setup_requires=["pybind11"], install_requires=["pybind11"], python_requires='>=3.8', include_package_data=True, zip_safe=False,)一些需要注意的点(坑):
如果需要通过sdist(即.tar.gz的源码方式)发布包的话,Extension的source_files字段必须是相对路径。否则build的时候会因为egg-info里的SOURCE.txt里有绝对路径而报错。但由此带来的问题是我们不能确定跑setup.py的时候工作目录是啥,为了保险起见,需要把它设置成setup.py所在的目录。
在安装包之前,为了获取一些metadata,setuptools会先跑一次setup.py,这个时候如果没有装pybind11,会报错。为了解决这个问题:
setup函数,我们需要先保证没有pybind11的情况下执行这个文件也不会报错。所以我们需要把所有依赖pybind11.setup_helpers的部分都放到try里。也有其它的方法,比如直接复制一个setup_helpers啥的,具体可以看文档。
setup_requires并不会安装包,所以pybind11也需要加到install_requires里。setup.py install,这时就可以成功build和安装了。如果你的外接库不在系统查找动态库的指定路径里,那么指定link_dirs之后,编译和链接不会出错。但执行的时候还是会因为找不到动态库而报错。可以通过添加runtime_library_dirs(等价于-Wl,-rpath),或者给LD_LIBRARY_PATH环境变量里添加这个路径。
编译后的.so的位置,以及你的C++ module在python里的名字,取决于你给Extension写的名字。例如,你希望文件结构是这样:
project_dir |-- package | |-- __init__.py | |-- this_package.xxxx.so | |-- other.py |-- setup.py这样你最后在site-packages里只会新建一个包叫package。
此外,哪怕你这个project只想导出一个
.so里的模块,把它放到一个文件夹里包装起来也会更好。因为如果你只想导出一个this_package,把setup函数里的配置改成了packages=['this_package'],这个.so文件会直接被加到site-packages,感觉不是很优雅。
这时候你的Extension的名字就需要是package.this_package。这样.so的位置就是对的,你运行import package.this_package就会正确地找到.so并执行了。
但是,要保证执行.so不出错,在C++里通过PYBIND11_MODULE把这个扩展expose到python里的时候,名字也要对应:
PYBIND11_MODULE(this_package, m) {}参考官方的CMake构建文档。
如果是编译嵌入python的C++程序,可以用CMake,比较方便。
虽然python extension似乎也可以用CMake,但是还是setuptools比较方便。
我这里主要是用CMake编译C++部分的测试。CMakeLists.txt大概长这样:
# the CMakeList to test the C++ part from a C entry pointcmake_minimum_required(VERSION 3.21)project(project_name)set(CMAKE_CXX_STANDARD 14)# Find pybind11find_package(pybind11 REQUIRED)# If any library (e.g. libabc.so) is neededinclude_directories(INCLUDE_DIR)link_directories(LINK_DIR)# Add source filefile(GLOB A_NAME_FOR_SOURCE CONFIGURE_DEPENDS "source/path/*.cpp")file(GLOB A_NAME_FOR_TEST CONFIGURE_DEPENDS "test/path/*.cpp")add_executable(TARGET_NAME ${A_NAME_FOR_SOURCE} ${A_NAME_FOR_TEST})target_link_libraries(TARGET_NAME abc pybind11::embed)project_name随便写TARGET_NAME随便写,只要add_executable和target_link_libraries对应就行,是最后的可执行文件的名字A_NAME_FOR_SOURCE和A_NAME_FOR_TEST是一个CMake的中间变量名,随便写,它们分别代表了GLOB找到的一堆源文件,和用于测试的一堆文件INCLUDE_DIR里是库abc的头文件,LINK_DIR里必须包含库文件,动态库类似libabc.so,静态库类似libabc.a。pybind11::embed的原因是防止带python对象的那部分C++代码编译失败。