CMake 秘籍(五)(4)

简介: CMake 秘籍(五)

CMake 秘籍(五)(3)https://developer.aliyun.com/article/1524580

工作原理

在本食谱中,我们通过一个相对紧凑的CMakeLists.txt文件实现了 Python 与 C++的接口,但我们通过使用FindCython.cmakeUseCython.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_modulecython_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__析构函数以及depositwithdraw方法是如何与相应的 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++项目所需的步骤:

  1. 与前一节一样,我们首先定义最小版本、项目名称、支持的语言和默认构建类型:
# 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()
  1. 在本配方中,我们依赖于 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()
  1. 使用以下命令,我们定义了 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()
  1. 最后,我们为这个实现定义了一个测试:
# 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
  )
  1. 现在可以配置、编译和测试代码:
$ 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 版本,组件可以称为pythonpython2python3python27python36python37等。我们通过从特定到更通用的名称进行搜索,并且只有在找不到匹配项时才失败来解决这个问题:

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_libraryMODULE选项并阻止在我们的库目标名称中添加任何前缀(例如,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是如何暴露depositwithdrawget_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 源代码明确放入我们的项目目录中,而是演示如何在配置时使用FetchContentcmake.org/cmake/help/v3.11/module/FetchContent.html)获取 pybind11 源代码。

为了在下一个示例中更好地重用代码,我们还将所有源代码放入子目录中,并使用以下项目布局:

.
├── account
│   ├── account.cpp
│   ├── account.hpp
│   ├── CMakeLists.txt
│   └── test.py
└── CMakeLists.txt

如何操作

让我们详细分析这个项目中各个CMakeLists.txt文件的内容:

  1. 根目录的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)
  1. 在此文件中,我们还查询将用于测试的 Python 解释器:
find_package(PythonInterp REQUIRED)
  1. 然后,我们包含账户子目录:
add_subdirectory(account)
  1. 之后,我们定义单元测试:
# 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
  )
  1. 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()
  1. 最后,我们定义 Python 模块。再次使用add_libraryMODULE选项。我们还为我们的库目标设置前缀和后缀属性为PYTHON_MODULE_PREFIXPYTHON_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}"
  )
  1. 让我们测试一下:
$ 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

相关文章
|
SQL 关系型数据库 MySQL
|
IDE 开发工具 开发者
手把手教你安装PyCharm 2025:开发者的Python IDE配置全流程+避坑指南
本教程详细介绍了PyCharm 2025版本在Windows系统下的安装流程及配置方法,涵盖AI代码补全与智能调试工具链等新功能。内容包括系统要求、安装步骤、首次运行配置(如主题选择与插件安装)、创建首个Python项目,以及常见问题解决方法。此外,还提供了切换中文界面和延伸学习资源的指导,帮助用户快速上手并高效使用PyCharm进行开发。
6083 61
|
10月前
|
人工智能 自然语言处理 搜索推荐
从扣子空间看 AI 智能体:与豆包、Kimi 较量及未来走向
本文探讨了当前 AI 智能体的发展现状、功能特点及其与传统 AI 大模型的差异,分析了其使用门槛与未来发展趋势,展望了其在多领域应用的潜力与挑战。
2335 0
|
人工智能 JSON API
使用 Qwen 生成数据模型和进行结构化输出
本教程展示如何使用CAMEL框架和Qwen模型生成结构化数据。CAMEL是一个强大的多智能体框架,支持复杂的AI任务;Qwen由阿里云开发,具备自然语言处理等先进能力。教程涵盖安装、API密钥设置、定义Pydantic模型,并演示了通过Qwen生成JSON格式的学生信息。最后,介绍了如何利用Qwen生成多个随机学生信息的JSON格式数据。欢迎在[CAMEL GitHub](https://github.com/camel-ai/camel)上为项目点星支持。
4332 70
|
存储 监控 druid
Druid、ClickHouse、Doris、StarRocks 的区别与分析
本文对比了 Druid、ClickHouse、Doris 和 StarRocks 四款大数据分析引擎。它们均为 OLAP 引擎,采用列式存储和分布式架构,适用于海量数据分析。Druid 擅长实时分析与高并发查询;ClickHouse 以超高性能著称,适合复杂查询;Doris 提供易用的 SQL 接口,性能均衡;StarRocks 则以其极速查询和实时更新能力脱颖而出。各引擎在数据模型、查询性能、数据更新和存储方面存在差异,适用于不同的业务场景。选择时需根据具体需求综合考虑。
7790 20
|
存储 Java C语言
MacOS环境-手写操作系统-04-实模式进入保护模式
MacOS环境-手写操作系统-04-实模式进入保护模式
263 1
|
XML C语言 数据格式
[ros基础] --- roslaunch使用详解
[ros基础] --- roslaunch使用详解
928 0
|
Java Spring
【实战】Spring生成beanName冲突的解决之道:附源码分析
【实战】Spring生成beanName冲突的解决之道:附源码分析
1419 0
【实战】Spring生成beanName冲突的解决之道:附源码分析
|
机器学习/深度学习 算法 异构计算
还不理解GPU推理卡和训练卡(简单易懂)
还不理解GPU推理卡和训练卡(简单易懂)
18179 2