# 9.4 使用Boost.Python构建C++和Python项目

**NOTE**:*此示例代码可以在* <https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-9/recipe-04> *中找到，其中有一个C++示例。该示例在CMake 3.5版(或更高版本)中是有效的，并且已经在GNU/Linux、macOS和Windows上进行过测试。*

Boost库为C++代码提供了Python接口。本示例将展示如何在依赖于Boost的C++项目中使用CMake，之后将其作为Python模块发布。我们将重用前面的示例，并尝试用Cython示例中的C++实现(`account.cpp`)进行交互。

## 准备工作

保持`account.cpp`不变的同时，修改前一个示例中的接口文件(`account.hpp`):

```cpp
#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);
}
```

## 具体实施

如何在C++项目中使用Boost.Python的步骤：

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()
   ```
2. 本示例中，依赖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()
   ```
3. 使用以下命令，定义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()
   ```
4. 最后，定义了一个测试:

   ```
   # 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
     )
   ```
5. 配置、编译和测试:

   ```
   $ 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`的模块选项。我们已经从第1章第3节了解到，CMake接受以下选项作为`add_library`的第二个有效参数:

* **STATIC**:创建静态库，也就是对象文件的存档，用于链接其他目标时使用，例如：可执行文件
* **SHARED**:创建共享库，也就是可以动态链接并在运行时加载的库
* **OBJECT**:创建对象库，也就是对象文件不需要将它们归档到静态库中，也不需要将它们链接到共享对象中

`MODULE`选项将生成一个插件库，也就是动态共享对象(DSO)，没有动态链接到任何可执行文件，但是仍然可以在运行时加载。由于我们使用C++来扩展Python，所以Python解释器需要能够在运行时加载我们的库。使用`MODULE`选项进行`add_library`，可以避免系统在库名前添加前缀(例如：Unix系统上的lib)。后一项操作是通过设置适当的目标属性来执行的，如下所示:

```
set_target_properties(account
  PROPERTIES
      PREFIX ""
  )
```

完成Python和C++接口的示例，需要向Python代码描述如何连接到C++层，并列出对Python可见的符号，我们也有可能重新命名这些符号。在上一个示例中，我们在另一个单独的`account.pyx`文件这样用过。当使用Boost.Python时，我们直接用C++代码描述接口，理想情况下接近期望的接口类或函数定义:

```cpp
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`模板包含在`<boost/python>`中，负责创建Python接口。该模块将公开一个`Account` Python类，该类映射到C++类。这种情况下，我们不需要显式地声明构造函数和析构函数——编译器会有默认实现，并在创建Python对象时自动调用:

```cpp
myaccount = Account()
```

当对象超出范围并被回收时，将调用析构函数。另外，观察`BOOST_PYTHON_MODULE`如何声明`deposit`、`withdraw`和`get_balance`函数，并将它们映射为相应的C++类方法。

这样，Python可以在`PYTHONPATH`中找到编译后的模块。这个示例中，我们实现了Python和C++层之间相对干净的分离。Python代码的功能不受限制，不需要类型注释或重写名称，并保持Python风格:

```python
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的轻量级替代方案。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://chenxiaowei.gitbook.io/cmake-cookbook/9.0-chinese/9.4-chinese.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
