使用pybind11為Python編寫C++擴展(一)配置篇:Build(編譯和鏈接)


目錄

最后決定選用 pybind11,理由如下:

  1. 比python原生的C API看起來人性多了
  2. 我的C++代碼不是現成的,需要一定的C++開發工作量,所以感覺cython不是很方便。如果C++接口已經給好了,只需要簡單包裝一下,Cython可能更好。
  3. pybind11聲稱只包含頭文件,且能通過pip安裝,感覺比boost_python輕量且最后這個擴展包容易分發。此外,感覺它的文檔也比boost python友好不少……

Setuptools

參考官方的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的源碼方式)發布包的話,Extensionsource_files字段必須是相對路徑。否則build的時候會因為egg-info里的SOURCE.txt里有絕對路徑而報錯。但由此帶來的問題是我們不能確定跑setup.py的時候工作目錄是啥,為了保險起見,需要把它設置成setup.py所在的目錄

  • 在安裝包之前,為了獲取一些metadata,setuptools會先跑一次setup.py,這個時候如果沒有裝pybind11,會報錯。為了解決這個問題:

    • 為了能正常執行到setup函數,我們需要先保證沒有pybind11的情況下執行這個文件也不會報錯。所以我們需要把所有依賴pybind11.setup_helpers的部分都放到try里。

      也有其它的方法,比如直接復制一個setup_helpers啥的,具體可以看官方的Setuptools構建文檔

    • 根據setuptools的文檔setup_requires並不會安裝包,所以pybind11也需要加到install_requires里。
    • 最后,在setup.py install安裝本包的時候,setuptools會先安裝依賴項,這時就可以成功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")
# Exclude the file that define the python module to avoid segment fault in CLion debugger
list(REMOVE_ITEM A_NAME_FOR_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/path/to/exposure.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_executabletarget_link_libraries對應就行,是最后的可執行文件的名字
  • A_NAME_FOR_SOURCEA_NAME_FOR_TEST是一個CMake的中間變量名,隨便寫,它們分別代表了GLOB找到的一堆源文件,和用於測試的一堆文件
  • INCLUDE_DIR里是庫abc的頭文件,LINK_DIR里必須包含庫文件,動態庫類似libabc.so,靜態庫類似libabc.a
  • Link到pybind11::embed的原因是防止帶python對象的那部分C++代碼編譯失敗。
  • 如果是為了在C++里測試,源文件部分要去掉把C++模塊注入python的那個源文件,即寫了PYBIND11_MODULE的那個(用REMOVE_ITEM命令),不然LLDB debugger可能segment fault(我也不知道為啥……)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM