CMake 秘籍(五)(3)https://developer.aliyun.com/article/1524580
工作原理
在本食谱中,我们通过一个相对紧凑的CMakeLists.txt文件实现了 Python 与 C++的接口,但我们通过使用FindCython.cmake和UseCython.cmake模块实现了这一点,这些模块被放置在cmake-cython下。这些模块通过以下代码包含:
# directory contains UseCython.cmake and FindCython.cmake list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake-cython) # this defines cython_add_module include(UseCython)
FindCython.cmake包含在UseCython.cmake中,并定位和定义${CYTHON_EXECUTABLE}。后一个模块定义了cython_add_module和cython_add_standalone_executable函数,这些函数可用于创建 Python 模块和独立可执行文件。这两个模块都已从github.com/thewtex/cython-cmake-example/tree/master/cmake下载。
在本食谱中,我们使用cython_add_module来创建一个 Python 模块库。请注意,我们将非标准的CYTHON_IS_CXX源文件属性设置为TRUE,这样cython_add_module函数就会知道将pyx文件编译为 C++文件:
# tells UseCython to compile this file as a c++ file set_source_files_properties(account.pyx PROPERTIES CYTHON_IS_CXX TRUE) # create python module cython_add_module(account account.pyx account.cpp)
Python 模块在${CMAKE_CURRENT_BINARY_DIR}内部创建,为了让 Python test.py脚本能够找到它,我们通过自定义环境变量传递相关路径,该变量在test.py内部用于设置PATH变量。注意COMMAND是如何设置为调用 CMake 可执行文件本身以在执行 Python 脚本之前正确设置本地环境的。这为我们提供了平台独立性,并避免了用无关变量污染环境:
add_test( NAME python_test COMMAND ${CMAKE_COMMAND} -E env ACCOUNT_MODULE_PATH=$<TARGET_FILE_DIR:account> ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test.py )
我们还应该查看account.pyx文件,它是 Python 和 C++之间的接口文件,描述了 C++接口:
# describe the c++ interface cdef extern from "account.hpp": cdef cppclass Account: Account() except + void deposit(double) void withdraw(double) double get_balance()
在Account类构造函数中可以看到except +。这个指令允许 Cython 处理由 C++代码引发的异常。
account.pyx接口文件还描述了 Python 接口:
# describe the python interface cdef class pyAccount: cdef Account *thisptr def __cinit__(self): self.thisptr = new Account() def __dealloc__(self): del self.thisptr def deposit(self, amount): self.thisptr.deposit(amount) def withdraw(self, amount): self.thisptr.withdraw(amount) def get_balance(self): return self.thisptr.get_balance()
我们可以看到cinit构造函数、__dealloc__析构函数以及deposit和withdraw方法是如何与相应的 C++实现对应部分匹配的。
总结一下,我们找到了一种通过引入对 Cython 模块的依赖来结合 Python 和 C++的机制。这个模块可以通过pip安装到虚拟环境或 Pipenv 中,或者使用 Anaconda 安装。
还有更多内容
C 也可以类似地耦合。如果我们希望利用构造函数和析构函数,我们可以围绕 C 接口编写一个薄的 C++层。
Typed Memoryviews 提供了有趣的功能,可以直接在 Python 中映射和访问由 C/C++分配的内存缓冲区,而不会产生任何开销:cython.readthedocs.io/en/latest/src/userguide/memoryviews.html。它们使得可以直接将 NumPy 数组映射到 C++数组。
使用 Boost.Python 构建 C++和 Python 项目
本节的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-09/recipe-04找到,并包含一个 C++示例。本节适用于 CMake 版本 3.5(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。
Boost 库提供了另一种流行的选择,用于将 C++代码与 Python 接口。本节将展示如何使用 CMake 为依赖于 Boost.Python 的 C++项目构建,以便将它们的功能作为 Python 模块暴露出来。我们将重用前一节的示例,并尝试与 Cython 示例中的相同 C++实现(account.cpp)进行交互。
准备工作
虽然我们保持account.cpp不变,但我们修改了前一节的接口文件(account.hpp):
#pragma once #define BOOST_PYTHON_STATIC_LIB #include <boost/python.hpp> class Account { public: Account(); ~Account(); void deposit(const double amount); void withdraw(const double amount); double get_balance() const; private: double balance; }; namespace py = boost::python; BOOST_PYTHON_MODULE(account) { py::class_<Account>("Account") .def("deposit", &Account::deposit) .def("withdraw", &Account::withdraw) .def("get_balance", &Account::get_balance); }
如何操作
以下是使用 Boost.Python 与您的 C++项目所需的步骤:
- 与前一节一样,我们首先定义最小版本、项目名称、支持的语言和默认构建类型:
# define minimum cmake version cmake_minimum_required(VERSION 3.5 FATAL_ERROR) # project name and supported language project(recipe-04 LANGUAGES CXX) # require C++11 set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD_REQUIRED ON) # we default to Release build type if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) endif()
- 在本配方中,我们依赖于 Python 和 Boost 库以及 Python 解释器进行测试。Boost.Python 组件的名称取决于 Boost 版本和 Python 版本,因此我们探测几个可能的组件名称:
# for testing we will need the python interpreter find_package(PythonInterp REQUIRED) # we require python development headers find_package(PythonLibs ${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR} EXACT REQUIRED)
# now search for the boost component # depending on the boost version it is called either python, # python2, python27, python3, python36, python37, ... list( APPEND _components python${PYTHON_VERSION_MAJOR}${PYTHON_VERSION_MINOR} python${PYTHON_VERSION_MAJOR} python ) set(_boost_component_found "") foreach(_component IN ITEMS ${_components}) find_package(Boost COMPONENTS ${_component}) if(Boost_FOUND) set(_boost_component_found ${_component}) break() endif() endforeach() if(_boost_component_found STREQUAL "") message(FATAL_ERROR "No matching Boost.Python component found") endif()
- 使用以下命令,我们定义了 Python 模块及其依赖项:
# create python module add_library(account MODULE account.cpp ) target_link_libraries(account PUBLIC Boost::${_boost_component_found} ${PYTHON_LIBRARIES} ) target_include_directories(account PRIVATE ${PYTHON_INCLUDE_DIRS} )
# prevent cmake from creating a "lib" prefix set_target_properties(account PROPERTIES PREFIX "" ) if(WIN32) # python will not import dll but expects pyd set_target_properties(account PROPERTIES SUFFIX ".pyd" ) endif()
- 最后,我们为这个实现定义了一个测试:
# turn on testing enable_testing() # define test add_test( NAME python_test COMMAND ${CMAKE_COMMAND} -E env ACCOUNT_MODULE_PATH=$<TARGET_FILE_DIR:account> ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test.py )
- 现在可以配置、编译和测试代码:
$ mkdir -p build $ cd build $ cmake .. $ cmake --build . $ ctest Start 1: python_test 1/1 Test #1: python_test ...................... Passed 0.10 sec 100% tests passed, 0 tests failed out of 1 Total Test time (real) = 0.11 sec
它是如何工作的
与依赖 Cython 模块不同,本配方现在依赖于在系统上定位 Boost 库,以及 Python 开发头文件和库。
使用以下命令搜索 Python 开发头文件和库:
find_package(PythonInterp REQUIRED) find_package(PythonLibs ${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR} EXACT REQUIRED)
请注意,我们首先搜索解释器,然后搜索开发头文件和库。此外,对PythonLibs的搜索要求开发头文件和库的相同主要和次要版本与解释器发现的版本相同。这是为了确保在整个项目中使用一致的解释器和库版本。然而,这种命令组合并不能保证会找到完全匹配的两个版本。
在定位 Boost.Python 组件时,我们遇到了一个难题,即我们尝试定位的组件名称取决于 Boost 版本和我们的 Python 环境。根据 Boost 版本,组件可以称为python、python2、python3、python27、python36、python37等。我们通过从特定到更通用的名称进行搜索,并且只有在找不到匹配项时才失败来解决这个问题:
list( APPEND _components python${PYTHON_VERSION_MAJOR}${PYTHON_VERSION_MINOR} python${PYTHON_VERSION_MAJOR} python ) set(_boost_component_found "") foreach(_component IN ITEMS ${_components}) find_package(Boost COMPONENTS ${_component}) if(Boost_FOUND) set(_boost_component_found ${_component}) break() endif() endforeach() if(_boost_component_found STREQUAL "") message(FATAL_ERROR "No matching Boost.Python component found") endif()
可以通过设置额外的 CMake 变量来调整 Boost 库的发现和使用。例如,CMake 提供以下选项:
Boost_USE_STATIC_LIBS可以设置为ON以强制使用 Boost 库的静态版本。Boost_USE_MULTITHREADED可以设置为ON以确保选择并使用多线程版本。Boost_USE_STATIC_RUNTIME可以设置为ON,以便我们的目标将使用链接 C++运行时静态的 Boost 变体。
本配方引入的另一个新方面是在add_library命令中使用MODULE选项。我们从第 3 个配方,构建和链接共享和静态库,在第一章,从简单可执行文件到库中已经知道,CMake 接受以下选项作为add_library的第二个有效参数:
STATIC,用于创建静态库;即,用于链接其他目标(如可执行文件)的对象文件的档案SHARED,用于创建共享库;即,可以在运行时动态链接和加载的库OBJECT,用于创建对象库;即,不将对象文件归档到静态库中,也不将它们链接成共享对象
这里引入的MODULE选项将生成一个插件库;也就是说,一个动态共享对象(DSO),它不会被动态链接到任何可执行文件中,但仍然可以在运行时加载。由于我们正在用自己编写的 C++功能扩展 Python,Python 解释器将需要在运行时能够加载我们的库。这可以通过使用add_library的MODULE选项并阻止在我们的库目标名称中添加任何前缀(例如,Unix 系统上的lib)来实现。后者操作是通过设置适当的 target 属性来完成的,如下所示:
set_target_properties(account PROPERTIES PREFIX "" )
所有展示 Python 和 C++接口的示例都有一个共同点,那就是我们需要向 Python 代码描述如何与 C++层连接,并列出应该对 Python 可见的符号。我们还可以(重新)命名这些符号。在前面的示例中,我们在一个单独的account.pyx文件中完成了这一点。当使用Boost.Python时,我们直接在 C++代码中描述接口,最好靠近我们希望接口的类或函数的定义:
BOOST_PYTHON_MODULE(account) { py::class_<Account>("Account") .def("deposit", &Account::deposit) .def("withdraw", &Account::withdraw) .def("get_balance", &Account::get_balance); }
BOOST_PYTHON_MODULE模板包含在中,负责创建 Python 接口。该模块将暴露一个Account Python 类,该类映射到 C++类。在这种情况下,我们不必显式声明构造函数和析构函数——这些会为我们自动创建,并在 Python 对象创建时自动调用:
myaccount = Account()
当对象超出作用域并被 Python 垃圾回收机制收集时,析构函数会被调用。同时,注意BOOST_PYTHON_MODULE是如何暴露deposit、withdraw和get_balance这些函数,并将它们映射到相应的 C++类方法上的。
这样,编译后的模块可以在PYTHONPATH中找到。在本示例中,我们实现了 Python 和 C++层之间相对干净的分离。Python 代码在功能上不受限制,不需要类型注释或重命名,并且保持了pythonic:
from account import Account account1 = Account() account1.deposit(100.0) account1.deposit(100.0) account2 = Account() account2.deposit(200.0) account2.deposit(200.0)
account1.withdraw(50.0) assert account1.get_balance() == 150.0 assert account2.get_balance() == 400.0
还有更多内容
在本示例中,我们依赖于系统上已安装的 Boost,因此 CMake 代码尝试检测相应的库。或者,我们可以将 Boost 源代码与我们的项目一起打包,并将此依赖项作为项目的一部分进行构建。Boost 是一种便携式的方式,用于将 Python 与 C++接口。然而,考虑到编译器支持和 C++标准的可移植性,Boost.Python 并不是一个轻量级的依赖。在下面的示例中,我们将讨论 Boost.Python 的一个轻量级替代方案。
使用 pybind11 构建 C++和 Python 项目
本示例的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-09/recipe-05找到,并包含一个 C++示例。该示例适用于 CMake 版本 3.11(及更高版本),并在 GNU/Linux、macOS 和 Windows 上进行了测试。
在前一个示例中,我们使用了 Boost.Python 来实现 Python 与 C(++)的接口。在这个示例中,我们将尝试使用 pybind11 作为轻量级替代方案,该方案利用了 C++11 特性,因此需要支持 C++11 的编译器。与前一个示例相比,我们将展示如何在配置时获取 pybind11 依赖项,并使用我们在第四章,创建和运行测试,示例 3,定义单元测试并与 Google Test 链接中遇到的 FetchContent 方法构建我们的项目,包括 Python 接口,并在第八章,超级构建模式,示例 4,使用超级构建管理依赖项:III. Google Test 框架中进行了讨论。在第十一章,打包项目,示例 2,通过 PyPI 分发使用 CMake/pybind11 构建的 C++/Python 项目中,我们将重新访问此示例,并展示如何打包它并通过 pip 安装。
准备就绪
我们将保持account.cpp相对于前两个示例不变,只修改account.hpp:
#pragma once #include <pybind11/pybind11.h> class Account { public: Account(); ~Account(); void deposit(const double amount); void withdraw(const double amount); double get_balance() const; private: double balance; }; namespace py = pybind11; PYBIND11_MODULE(account, m) { py::class_<Account>(m, "Account") .def(py::init()) .def("deposit", &Account::deposit) .def("withdraw", &Account::withdraw) .def("get_balance", &Account::get_balance); }
我们将遵循 pybind11 文档中的“使用 CMake 构建”指南(pybind11.readthedocs.io/en/stable/compiling.html#building-with-cmake),并介绍使用add_subdirectory添加 pybind11 的 CMake 代码。然而,我们不会将 pybind11 源代码明确放入我们的项目目录中,而是演示如何在配置时使用FetchContent(cmake.org/cmake/help/v3.11/module/FetchContent.html)获取 pybind11 源代码。
为了在下一个示例中更好地重用代码,我们还将所有源代码放入子目录中,并使用以下项目布局:
. ├── account │ ├── account.cpp │ ├── account.hpp │ ├── CMakeLists.txt │ └── test.py └── CMakeLists.txt
如何操作
让我们详细分析这个项目中各个CMakeLists.txt文件的内容:
- 根目录的
CMakeLists.txt文件包含熟悉的头部信息:
# define minimum cmake version cmake_minimum_required(VERSION 3.11 FATAL_ERROR) # project name and supported language project(recipe-05 LANGUAGES CXX) # require C++11 set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD_REQUIRED ON)
- 在此文件中,我们还查询将用于测试的 Python 解释器:
find_package(PythonInterp REQUIRED)
- 然后,我们包含账户子目录:
add_subdirectory(account)
- 之后,我们定义单元测试:
# turn on testing enable_testing() # define test add_test( NAME python_test COMMAND ${CMAKE_COMMAND} -E env ACCOUNT_MODULE_PATH=$<TARGET_FILE_DIR:account> ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/account/test.py )
- 在
account/CMakeLists.txt文件中,我们在配置时获取 pybind11 源代码:
include(FetchContent) FetchContent_Declare( pybind11_sources GIT_REPOSITORY https://github.com/pybind/pybind11.git GIT_TAG v2.2 ) FetchContent_GetProperties(pybind11_sources) if(NOT pybind11_sources_POPULATED) FetchContent_Populate(pybind11_sources) add_subdirectory( ${pybind11_sources_SOURCE_DIR} ${pybind11_sources_BINARY_DIR} ) endif()
- 最后,我们定义 Python 模块。再次使用
add_library的MODULE选项。我们还为我们的库目标设置前缀和后缀属性为PYTHON_MODULE_PREFIX和PYTHON_MODULE_EXTENSION,这些属性由 pybind11 适当地推断出来:
add_library(account MODULE account.cpp ) target_link_libraries(account PUBLIC pybind11::module ) set_target_properties(account PROPERTIES PREFIX "${PYTHON_MODULE_PREFIX}" SUFFIX "${PYTHON_MODULE_EXTENSION}" )
- 让我们测试一下:
$ mkdir -p build $ cd build $ cmake .. $ cmake --build . $ ctest Start 1: python_test 1/1 Test #1: python_test ...................... Passed 0.04 sec 100% tests passed, 0 tests failed out of 1 Total Test time (real) = 0.04 sec
它是如何工作的
pybind11 的功能和使用与 Boost.Python 非常相似,不同的是 pybind11 是一个更轻量级的依赖项——尽管我们需要编译器的 C++11 支持。在account.hpp中的接口定义与前一个示例中的定义相当相似:
#include <pybind11/pybind11.h> // ... namespace py = pybind11; PYBIND11_MODULE(account, m) { py::class_<Account>(m, "Account") .def(py::init()) .def("deposit", &Account::deposit) .def("withdraw", &Account::withdraw) .def("get_balance", &Account::get_balance); }
再次,我们可以清楚地看到 Python 方法是如何映射到 C++函数的。解释PYBIND11_MODULE的库在导入的目标pybind11::module中定义,我们使用以下方式包含它:
add_subdirectory( ${pybind11_sources_SOURCE_DIR} ${pybind11_sources_BINARY_DIR} )
与前一个配方相比,有两个不同之处:
- 我们不要求系统上安装了 pybind11,因此不会尝试定位它。
- 在项目开始构建时,包含 pybind11
CMakeLists.txt的${pybind11_sources_SOURCE_DIR}子目录并不存在。
解决此挑战的一种方法是使用FetchContent模块,该模块在配置时获取 pybind11 源代码和 CMake 基础设施,以便我们可以使用add_subdirectory引用它。采用FetchContent模式,我们现在可以假设 pybind11 在构建树中可用,这使得我们能够构建并链接 Python 模块。
add_library(account MODULE account.cpp ) target_link_libraries(account PUBLIC pybind11::module )
我们使用以下命令确保 Python 模块库获得一个与 Python 环境兼容的定义良好的前缀和后缀:
set_target_properties(account PROPERTIES PREFIX ${PYTHON_MODULE_PREFIX} SUFFIX ${PYTHON_MODULE_EXTENSION} )
顶级CMakeLists.txt文件的其余部分用于测试(我们使用与前一个配方相同的test.py)。
CMake 秘籍(五)(5)https://developer.aliyun.com/article/1524582