# 9.3 使用Cython构建C++和Python项目

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

Cython是一个静态编译器，它允许为Python编写C扩展。Cython是一个非常强大的工具，使用Cython编程语言(基于Pyrex)。Cython的一个典型用例是加快Python代码的速度，它也可以用于通过Cython层使Python与C(++)接口对接。本示例中，我们将重点介绍后一种用例，并演示如何在CMake的帮助下使用Cython与C(++)和Python进行对接。

## 准备工作

我们将使用以下C++代码(`account.cpp`):

```cpp
#include "account.hpp"
Account::Account() : balance(0.0) {}
Account::~Account() {}
void Account::deposit(const double amount) { balance += amount; }
void Account::withdraw(const double amount) { balance -= amount; }
double Account::get_balance() const { return balance; }
```

代码提供了以下接口(`account.hpp`):

```cpp
#pragma once

class Account {
public:
  Account();
  ~Account();

  void deposit(const double amount);
  void withdraw(const double amount);
  double get_balance() const;

private:
    double balance;
};
```

使用这个示例代码，我们可以创建余额为零的银行帐户。可以在帐户上存款和取款，还可以使用`get_balance()`查询帐户余额。余额本身是`Account`类的私有成员。

我们的目标是能够直接从Python与这个C++类进行交互。换句话说，在Python方面，我们希望能够做到这一点:

```python
account = Account()

account.deposit(100.0)
account.withdraw(50.0)

balance = account.get_balance()
```

为此，需要一个Cython接口文件(调用`account.pyx`):

```python
# describe the c++ interface
cdef extern from "account.hpp":
  cdef cppclass Account:
    Account() except +
    void deposit(double)
    void withdraw(double)
    double get_balance()

# 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()
```

## 具体实施

如何生成Python接口:

1. `CMakeLists.txt`定义CMake依赖项、项目名称和语言:

   ```
   # define minimum cmake version
   cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
   # project name and supported language
   project(recipe-03 LANGUAGES CXX)
   # require C++11
   set(CMAKE_CXX_STANDARD 11)
   set(CMAKE_CXX_EXTENSIONS OFF)
   set(CMAKE_CXX_STANDARD_REQUIRED ON)
   ```
2. Windows上，最好不要保留未定义的构建类型，这样我们就可以将该项目的构建类型与Python环境的构建类型相匹配。这里我们默认为Release类型:

   ```
   if(NOT CMAKE_BUILD_TYPE)
       set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
   endif()
   ```
3. 在示例中，还需要Python解释器:

   ```
   find_package(PythonInterp REQUIRED)
   ```
4. 下面的CMake代码将构建Python模块:

   ```
   # directory cointaining UseCython.cmake and FindCython.cmake
   list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake-cython)

   # this defines cython_add_module
   include(UseCython)

   # 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)

   # location of account.hpp
   target_include_directories(account
     PRIVATE
         ${CMAKE_CURRENT_SOURCE_DIR}
     )
   ```
5. 定义一个测试：

   ```
   # 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
     )
   ```
6. `python_test`执行`test.py`，这里进行一些存款和取款操作，并验证余额:

   ```
   import os
   import sys
   sys.path.append(os.getenv('ACCOUNT_MODULE_PATH'))

   from account import pyAccount as 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
   ```
7. 有了这个，我们就可以配置、构建和测试代码了:

   ```
   $ mkdir -p build
   $ cd build
   $ cmake ..
   $ cmake --build .
   $ ctest

   Start 1: python_test
   1/1 Test #1: python_test ...................... Passed 0.03 sec
   100% tests passed, 0 tests failed out of 1
   Total Test time (real) = 0.03 sec
   ```

## 工作原理

本示例中，使用一个相对简单的`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模块和独立的可执行程序。这两个模块都可从 <https://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`变量。请注意，如何将命令设置为调用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++接口进行描述:

```python
# describe the c++ interface
cdef extern from "account.hpp":
  cdef cppclass Account:
    Account() except +
    void deposit(double)
    void withdraw(double)
    double get_balance()
```

可以看到`cinit`构造函数、`__dealloc__`析构函数以及`deposit`和`withdraw`方法是如何与对应的C++实现相匹配的。

总之，发现了一种机制，通过引入对Cython模块的依赖来耦合Python和C++。该模块可以通过`pip`安装到虚拟环境或Pipenv中，或者使用Anaconda来安装。

## 更多信息

C语言可以进行类似地耦合。如果希望利用构造函数和析构函数，我们可以在C接口之上封装一个C++层。

类型化Memoryview提供了有趣的功能，可以映射和访问由C/C++直接在Python中分配的内存，而不需要任何创建：<http://cython.readthedocs.io/en/latest/src/userguide/memoryviews.html> 。它们使得将NumPy数组直接映射为C++数组成为可能。


---

# 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.3-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.
