# 15.10 用Cython包装C代码

## 问题

你想使用 Cython 来创建一个 Python 扩展模块，用来包装某个已存在的 C 函数库。

## 解决方案

使用 Cython 构建一个扩展模块看上去很手写扩展有些类似， 因为你需要创建很多包装函数。不过，跟前面不同的是，你不需要在 C 语言中做这些——代码看上去更像是 Python。

作为准备，假设本章介绍部分的示例代码已经被编译到某个叫 `libsample` 的 C 函数库中了。 首先创建一个名叫 `csample.pxd` 的文件，如下所示：

```python
# csample.pxd
#
# Declarations of "external" C functions and structures

cdef extern from "sample.h":
    int gcd(int, int)
    bint in_mandel(double, double, int)
    int divide(int, int, int *)
    double avg(double *, int) nogil

    ctypedef struct Point:
         double x
         double y

    double distance(Point *, Point *)
```

这个文件在 Cython 中的作用就跟 C 的头文件一样。 初始声明 `cdef extern from "sample.h"` 指定了所需的 C 头文件。 接下来的声明都是来自于那个头文件。文件名是 `csample.pxd` ，而不是 `sample.pxd` ——这点很重要。

下一步，创建一个名为 `sample.pyx` 的问题。 该文件会定义包装器，用来桥接 Python 解释器到 `csample.pxd` 中声明的 C 代码。

```python
# sample.pyx
# Import the low-level C declarations
cimport csample

# Import some functionality from Python and the C stdlib
from cpython.pycapsule cimport *

from libc.stdlib cimport malloc, free

# Wrappers
def gcd(unsigned int x, unsigned int y):
    return csample.gcd(x, y)

def in_mandel(x, y, unsigned int n):
    return csample.in_mandel(x, y, n)

def divide(x, y):
    cdef int rem
    quot = csample.divide(x, y, &rem)
    return quot, rem

def avg(double[:] a):
    cdef:
        int sz
        double result

    sz = a.size
    with nogil:
        result = csample.avg(<double *> &a[0], sz)
    return result

# Destructor for cleaning up Point objects
cdef del_Point(object obj):
    pt = <csample.Point *> PyCapsule_GetPointer(obj,"Point")
    free(<void *> pt)

# Create a Point object and return as a capsule
def Point(double x,double y):
    cdef csample.Point *p
    p = <csample.Point *> malloc(sizeof(csample.Point))
    if p == NULL:
        raise MemoryError("No memory to make a Point")
    p.x = x
    p.y = y
    return PyCapsule_New(<void *>p,"Point",<PyCapsule_Destructor>del_Point)

def distance(p1, p2):
    pt1 = <csample.Point *> PyCapsule_GetPointer(p1,"Point")
    pt2 = <csample.Point *> PyCapsule_GetPointer(p2,"Point")
    return csample.distance(pt1,pt2)
```

该文件更多的细节部分会在讨论部分详细展开。 最后，为了构建扩展模块，像下面这样创建一个 `setup.py` 文件：

```python
from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

ext_modules = [
    Extension('sample',

              ['sample.pyx'],
              libraries=['sample'],
              library_dirs=['.'])]
setup(
  name = 'Sample extension module',
  cmdclass = {'build_ext': build_ext},
  ext_modules = ext_modules
)
```

要构建我们测试的目标模块，像下面这样做：

```
bash % python3 setup.py build_ext --inplace
running build_ext
cythoning sample.pyx to sample.c
building 'sample' extension
gcc -fno-strict-aliasing -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes
 -I/usr/local/include/python3.3m -c sample.c
 -o build/temp.macosx-10.6-x86_64-3.3/sample.o
gcc -bundle -undefined dynamic_lookup build/temp.macosx-10.6-x86_64-3.3/sample.o
  -L. -lsample -o sample.so
```

如果一切顺利的话，你应该有了一个扩展模块 `sample.so` ，可在下面例子中使用：

```python
>>> import sample
>>> sample.gcd(42,10)
2
>>> sample.in_mandel(1,1,400)
False
>>> sample.in_mandel(0,0,400)
True
>>> sample.divide(42,10)
(4, 2)
>>> import array
>>> a = array.array('d',[1,2,3])
>>> sample.avg(a)
2.0
>>> p1 = sample.Point(2,3)
>>> p2 = sample.Point(4,5)
>>> p1
<capsule object "Point" at 0x1005d1e70>
>>> p2
<capsule object "Point" at 0x1005d1ea0>
>>> sample.distance(p1,p2)
2.8284271247461903
```

## 讨论

本节包含了很多前面所讲的高级特性，包括数组操作、包装隐形指针和释放 GIL。 每一部分都会逐个被讲述到，但是我们最好能复习一下前面几小节。 在顶层，使用 Cython 是基于 C 之上。`.pxd` 文件仅仅只包含 C 定义（类似 .h 文件）， `.pyx` 文件包含了实现（类似 .c 文件）。`cimport` 语句被 Cython 用来导入 `.pxd` 文件中的定义。 它跟使用普通的加载 Python 模块的导入语句是不同的。

尽管 .pxd 文件包含了定义，但它们并不是用来自动创建扩展代码的。 因此，你还是要写包装函数。例如，就算 `csample.pxd` 文件声明了 `int gcd(int, int)` 函数， 你仍然需要在 `sample.pyx` 中为它写一个包装函数。例如：

```python
cimport csample

def gcd(unsigned int x, unsigned int y):
    return csample.gcd(x,y)
```

对于简单的函数，你并不需要去做太多的事。 Cython 会生成包装代码来正确的转换参数和返回值。 绑定到属性上的 C 数据类型是可选的。不过，如果你包含了它们，你可以另外做一些错误检查。 例如，如果有人使用负数来调用这个函数，会抛出一个异常：

```python
>>> sample.gcd(-10,2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "sample.pyx", line 7, in sample.gcd (sample.c:1284)
    def gcd(unsigned int x,unsigned int y):
OverflowError: can't convert negative value to unsigned int
```

如果你想对包装函数做另外的检查，只需要使用另外的包装代码。例如：

```python
def gcd(unsigned int x, unsigned int y):
    if x <= 0:
        raise ValueError("x must be > 0")
    if y <= 0:
        raise ValueError("y must be > 0")
    return csample.gcd(x,y)
```

在 `csample.pxd` 文件中的 `in_mandel()` 声明有个很有趣但是比较难理解的定义。 在这个文件中，函数被声明为然后一个 bint 而不是一个 int。 它会让函数创建一个正确的 Boolean 值而不是简单的整数。 因此，返回值 0 表示 False 而 1 表示 True。

在 Cython 包装器中，你可以选择声明 C 数据类型，也可以使用所有的常见 Python 对象。 对于 `divide()` 的包装器展示了这样一个例子，同时还有如何去处理一个指针参数。

```python
def divide(x,y):
    cdef int rem
    quot = csample.divide(x,y,&rem)
    return quot, rem
```

在这里，`rem` 变量被显示的声明为一个 C 整型变量。 当它被传入 `divide()` 函数的时候，`&rem` 创建一个跟 C 一样的指向它的指针。 `avg()` 函数的代码演示了 Cython 更高级的特性。 首先 `def avg(double[:] a)` 声明了 `avg()` 接受一个一维的双精度内存视图。 最惊奇的部分是返回的结果函数可以接受任何兼容的数组对象，包括被 numpy 创建的。例如：

```python
>>> import array
>>> a = array.array('d',[1,2,3])
>>> import numpy
>>> b = numpy.array([1., 2., 3.])
>>> import sample
>>> sample.avg(a)
2.0
>>> sample.avg(b)
2.0
```

在此包装器中，`a.size0` 和 `&a[0]` 分别引用数组元素个数和底层指针。 语法 `&a[0]` 教你怎样将指针转换为不同的类型。 前提是 C 中的 `avg()` 接受一个正确类型的指针。 参考下一节关于 Cython 内存视图的更高级讲述。

除了处理通常的数组外，`avg()` 的这个例子还展示了如何处理全局解释器锁。语句 `with nogil:` 声明了一个不需要 GIL 就能执行的代码块。 在这个块中，不能有任何的普通 Python 对象——只能使用被声明为 `cdef` 的对象和函数。 另外，外部函数必须现实的声明它们能不依赖 GIL 就能执行。 因此，在 `csample.pxd` 文件中，`avg()` 被声明为 `double avg(double *, int) nogil` .

对 Point 结构体的处理是一个挑战。本节使用胶囊对象将 Point 对象当做隐形指针来处理，这个在 15.4 小节介绍过。 要这样做的话，底层 Cython 代码稍微有点复杂。 首先，下面的导入被用来引入 C 函数库和 Python C API 中定义的函数：

```python
from cpython.pycapsule cimport *
from libc.stdlib cimport malloc, free
```

函数 `del_Point()` 和 `Point()` 使用这个功能来创建一个胶囊对象， 它会包装一个 `Point *` 指针。`cdef del_Point()` 将 `del_Point()` 声明为一个函数， 只能通过 Cython 访问，而不能从 Python 中访问。 因此，这个函数对外部是不可见的——它被用来当做一个回调函数来清理胶囊分配的内存。 函数调用比如 `PyCapsule_New()`、`PyCapsule_GetPointer()` 直接来自 Python C API 并且以同样的方式被使用。

`distance` 函数从 `Point()` 创建的胶囊对象中提取指针。 这里要注意的是你不需要担心异常处理。 如果一个错误的对象被传进来，`PyCapsule_GetPointer()` 会抛出一个异常， 但是 Cython 已经知道怎么查找到它，并将它从 `distance()` 传递出去。

处理 Point 结构体一个缺点是它的实现是不可见的。 你不能访问任何属性来查看它的内部。 这里有另外一种方法去包装它，就是定义一个扩展类型，如下所示：

```python
# sample.pyx

cimport csample
from libc.stdlib cimport malloc, free
#...

cdef class Point:
    cdef csample.Point *_c_point
    def __cinit__(self, double x, double y):
        self._c_point = <csample.Point *> malloc(sizeof(csample.Point))
        self._c_point.x = x
        self._c_point.y = y

    def __dealloc__(self):
        free(self._c_point)

    property x:
        def __get__(self):
            return self._c_point.x
        def __set__(self, value):
            self._c_point.x = value

    property y:
        def __get__(self):
            return self._c_point.y
        def __set__(self, value):
            self._c_point.y = value

def distance(Point p1, Point p2):
    return csample.distance(p1._c_point, p2._c_point)
```

在这里，cdif 类 `Point` 将 Point 声明为一个扩展类型。 类属性 `cdef csample.Point *_c_point` 声明了一个实例变量， 拥有一个指向底层 Point 结构体的指针。 `__cinit__()` 和 `__dealloc__()` 方法通过 `malloc()` 和 `free()` 创建并销毁底层 C 结构体。 x 和 y 属性的声明让你获取和设置底层结构体的属性值。 `distance()` 的包装器还可以被修改，使得它能接受 `Point` 扩展类型实例作为参数， 而传递底层指针给 C 函数。

做了这个改变后，你会发现操作 Point 对象就显得更加自然了：

```python
>>> import sample
>>> p1 = sample.Point(2,3)
>>> p2 = sample.Point(4,5)
>>> p1
<sample.Point object at 0x100447288>
>>> p2
<sample.Point object at 0x1004472a0>
>>> p1.x
2.0
>>> p1.y
3.0
>>> sample.distance(p1,p2)
2.8284271247461903
```

本节已经演示了很多 Cython 的核心特性，你可以以此为基准来构建更多更高级的包装。 不过，你最好先去阅读下官方文档来了解更多信息。


---

# 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://l1nwatch.gitbook.io/python-cookbook/di-15-zhang-c-yu-yan-kuo-zhan/15.10-yong-cython-bao-zhuangcdai-ma.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.
