最後決定選用
pybind11
,理由如下:
- 比python原生的C API看起來人性多了
- 我的C++程式碼不是現成的,需要一定的C++開發工作量,所以感覺cython不是很方便。如果C++介面已經給好了,只需要簡單包裝一下,Cython可能更好。
- pybind11聲稱只包含標頭檔案,且能通過pip安裝,感覺比boost_python輕量且最後這個擴充套件包容易分發。此外,感覺它的文件也比boost python友好不少……
Setuptools
這種方式適合python包的構建、打包、分發、上傳到PyPi一條龍服務。python使用C++擴充套件需要在setup.py
裡配置好Extension
。以下是一個setup.py
的樣例:
import glob
import os.path
from distutils.core import setup
__version__ = "0.0.1"
# make sure the working directory is BASE_DIR
BASE_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:
pass
setup(
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,
)
這樣最後python包的檔案結構是:
project_dir
|-- package
| |-- __init__.py
| |-- this_package.xxxx.so
| |-- other.py
|-- setup.py
-
如果是在本地專案開發過程中需要構建
.so
庫檔案:python setup.py build_ext --inplace
你的
.so
庫會在package
目錄下,你可以直接像用python模組一樣在python測試檔案裡引入:import package.this_package
最好不要直接用
python setup.py build
或者python setup.py build_ext
。它們會把動態庫編譯到一個單獨的build
目錄下,前者會帶上你包裡其它的python檔案,後者則只會有動態庫。目錄的路徑比較複雜,匯入比較麻煩,要麼需要新增sys.path
,要麼要寫比較醜陋的import
。感覺只適合作為分發包前的一箇中間步驟,對我們本地測試自己的包沒啥用。 -
如果最後需要分發你這個包:
- 原始碼分發
sdist
:setuptools會直接打包原始碼,不需要先build。分發前記得刪掉.so
檔案。 在安裝的時候,setuptools會在要安裝這個包的機器上當場build,這時候你安裝進site-packages
裡的包也會長得跟目錄package
一樣。 - 二進位制wheel分發:
bdist_wheel
:setuptools會在本機build好,然後一股腦塞進wheel裡,最後分發的是那個.whl
檔案。這個檔案跟作業系統和計算機的體系架構有關,這也是為啥很多我們熟悉的包,比如numpy
,有很多個版本的.whl
檔案,我們需要找對應的下載。
當然不管怎樣,這些都是setuptools的自動操作,你只要配置好了
setup.py
,一切都好說。 - 原始碼分發
一些需要注意的點(坑):
-
如果需要通過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啥的,具體可以看文件。
- 根據setuptools的文件,
setup_requires
並不會安裝包,所以pybind11
也需要加到install_requires
裡。 - 最後,在安裝本包前,setuptools會先安裝依賴項,然後再跑
setup.py install
,這時就可以成功build和安裝了。
- 為了能正常執行到
-
如果你的外接庫不在系統查詢動態庫的指定路徑裡,那麼指定link_dirs之後,編譯和連結不會出錯。但執行的時候還是會因為找不到動態庫而報錯。可以通過新增
runtime_library_dirs
(等價於-Wl,-rpath
),或者給LD_LIBRARY_PATH
環境變數裡新增這個路徑。 -
編譯後的
.so
的位置,以及你的C++ module在python裡的名字,取決於你給Extension
寫的名字。如果它叫package.a.b.c.this_package
,那麼最後這個.so
檔案就會在project_dir/package/a/b/c
下。此外,哪怕你這個project只想匯出一個
.so
裡的模組,把它放到一個資料夾裡包裝起來也會更好。因為如果你只想匯出一個this_package
,把setup函式裡的配置改成了packages=['this_package']
,這個.so
檔案會直接被加到site-packages
,感覺不是很優雅。但是,要保證執行
.so
不出錯,在C++裡通過PYBIND11_MODULE
把這個擴充套件expose到python裡的時候,名字也要對應:PYBIND11_MODULE(this_package, m) {}
CMake
參考官方的CMake構建文件。
如果是編譯嵌入python的C++程式,可以用CMake,比較方便。
雖然python extension似乎也可以用CMake,但是還是setuptools比較方便。
我這裡主要是用CMake編譯C++部分的測試。CMakeLists.txt
大概長這樣:
# the CMakeList to test the C++ part from a C entry point
cmake_minimum_required(VERSION 3.21)
project(project_name)
set(CMAKE_CXX_STANDARD 14)
# Find pybind11
find_package(pybind11 REQUIRED)
# If any library (e.g. libabc.so) is needed
include_directories(INCLUDE_DIR)
link_directories(LINK_DIR)
# Add source file
file(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
。- Link到
pybind11::embed
的原因是防止帶python物件的那部分C++程式碼編譯失敗。