1、在Cython中声明外部的C代码

要用 Cython 包装 C 源文件,必须在 Cython 中声明使用的 C 组件的接口。为此,Cython 提供了一个 extern语句,它的目的就是告诉 Cython,希望从指定的 C 头文件中使用 C 结构。语法如下:

1
2
3
cdef extern from "header_name":
# 相应的声明, 你希望使用哪些 C 结构, 那么就将其定义在这里
# 如果不需要的话可以写上一个pass
  • 1. Cython 编译器会在生成的源文件中写入 #include "header_name"
  • 2. 在 extern 语句块中的类型、函数以及其它声明都可以在 Cython 中直接使用
  • 3. Cython 会在编译时检查 C 的声明是否正确,如果不正确会编译错误。

extern 语句块中的声明类似于 C,会用它来介绍之前说的结构体、共同体。另外 extern 关键字可以和 cdef 组合,一起添加到任意的 C 声明中。

extern 会在生成的源文件中写入一个 #include 语句,但如果不希望写入这个语句,但是又希望和外部代码进行交互,那么可以通过 from * 来禁止 Cython 生成。

1
2
cdef extern from *:
# 声明

extern from 代码块内部写的是函数声明,这些声明要和 C 中的相匹配。

下面就来详细介绍 extern 怎么用,不过在介绍之前,需要了解一下extern它不会做哪些事情。

1.1 Cython不会自动包装

extern 语句块的目的很简单,但是乍一看可能会产生误导。在 Cython 中存在 extern 块(extern声明),确保能够以正确的类型调用声明的 C 函数、变量、结构体等等,但是它不会自动地为对象创建 Python 的包装器,仍然需要使用 def、或者 cpdef(可能还会使用 cdef)来调用 extern 块中声明的 C 函数。因为如果不这么做,则无法从 Python 代码中访问 extern 块中声明的外部 C 函数。因为 Cython 不会自动解析 C 文件、以及包装给外部 Python 访问,需要手动实现这一点。而这么做的原因也很好理解,因此 Cython 中包装器的实现已经非常简单了,完全可以自己自定制,自动实现的话反而会任务变得复杂。

2、声明外部的C函数以及给类型起别名

extern 块中最常见的声明是 C 函数和 typedef,这些声明几乎可以直接写在 Cython 中,只需要做一下修改:

  • 1. 将typedef变成ctypedef
  • 2. 删除类似于restrict、volatile等不必要、以及不支持的关键字
  • 3. 确保函数的返回值和对应类型的声明在同一行
1
2
3
4
5
//在C中,可以这么写,但是 Cython 中要在同一行
int
foo() {
return 123
}
  • 4. 删除行尾的分号

此外,在 Cython 中声明函数时,参数可以写在多行,就像 Python 一样。

下面定义一个 C 的头文件:header.h,写上一些简单的 C 声明和宏。

1
2
3
4
5
6
7
#define M_PI 3.1415926
#define MAX(a, b) ((a) >= (b) ? (a) : (b))
double hypot(double, double);
typedef int integral;
typedef double real;
void func(integral, integral, real);
real *func_arrays(integral[], integral[][10], real **);

如果想在 Cython 中使用的话,那么就把那些想用的写在 Cython 中,当然说不能直接照搬,因为 C 和 Cython 的声明还是有些略微的差异的,上面已经介绍过了。

1
2
3
4
5
6
7
8
cdef extern from "header.h":
double M_PI
float MAX(float a, float b)
double hypot(double x, double y)
ctypedef int integral
ctypedef double real
void func(integral a, integral b, real c)
real *func_arrays(integral[] i, integral[][10] j, real **k)

注意:我们在 Cython 中声明 C 中 M_PI 这个宏时,将其声明为 double 型的变量,同理对于 MAX 宏也是如此,就把它当成接收两个 float、返回一个 float 的名为 MAX 函数。

另外看到在 extern 块的声明中,为函数参数添加了一个名字。这是推荐的,但并不是强制的;如果有参数名的话,那么可以让通过关键字参数调用,对于接口的使用会更加明确。

Cython 支持 C 中的所有声明,甚至函数指针接收函数指针、返回包含函数指针的数组也是可以的。当然简单的类型声明:数值、字符串、数组、指针、void 等等已经构成了 C 声明的大多数,大多数时候可以直接将 C 中的声明复制粘贴过来,然后去掉分号就可以了。

1
2
3
4
cdef extern from "header2.h":
ctypedef void (*void_int_fptr)(int)
void_int_fptr signal(void_int_fptr)
# 上面两行等价于 void (*signal(void(*)(int)))(int)

所以可以进行非常复杂的声明,当然日常也很少会用到。

3、声明并包装C结构体、共同体、枚举

如果声明一个结构体、共同体、枚举,那么可以使用如下方式:

1
2
3
4
5
6
7
8
9
cdef extern from "header_name":
struct struct_name:
struct_members # 创建变量的时候通过 "cdef struct_name 变量" 的方式

union struct_name:
union_members

enum struct_name:
enum_members

如果是在 C 中,等价于如下:

1
2
3
4
5
6
7
8
9
10
11
struct struct_name {
struct_members
}; // 创建变量的时候通过 "struct struct_name 变量" 的方式

union union_name {
union_members
};

enum enum_name {
enum_members
};

当然在 C 中还可以使用 typedef。

1
2
3
4
5
6
7
8
9
10
11
typedef struct struct_name {
struct_members
} struct_alias; // 然后创建变量的时候直接通过 "struct_alisa 变量" 即可,所以定义结构体的时候 struct_name 也可以不写

typedef union union_name {
union_members
} union_alias;

typedef enum enum_name {
enum_members
} enum_alias;

Cython 中的 typedef 则是使用 ctypedef。

1
2
3
4
5
6
7
8
9
10
11
12
cdef extern from "header_name":

ctypedef struct struct_alias:
struct_members
# 创建变量的时候通过 "cdef struct_name 变量" 的方式
# 所以无论是哪种方式,在 Cython 中创建结构体变量的时候是没有任何区别的

ctypedef union struct_alias:
union_members

ctypedef enum struct_alias:
enum_members

Cython 中的 typedef 则是使用 ctypedef,此时就定义了一个类型别名。但是注意:如果结构体中没有字段,那么 Cython 中应该要给一个 pass 语句作为占位符。

3.1 举例说明

下面来实际演示一下,直接以结构体为例:

1
2
3
4
5
6
7
8
9
10
11
// c_src/header.h

struct Girl1 {
char * name;
int age;
};

typedef struct {
char *name;
int age;
} Girl2;

以上是一个 C 的头文件,我们在 Cython 中导入之后要怎么进行声明呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# cython/cython_test.pyx

cdef extern from "header.h":
struct Girl1:
char *name
int age

ctypedef struct Girl2:
char *name
int age

# 对于结构体而言, 里面的成员只能用 C 中的类型
# 而且如何创建结构体的对应实例, 我们之前也说过了, 直接 "cdef 结构体类型 变量名 = " 即可
cdef Girl1 g1 = Girl1("komeiji satori", 16)
cdef Girl1 g2 = Girl1("komeiji koishi", age=16)

# 可以看到无论是 cdef struct 定义的, 还是通过 ctypedef 起的类型别名, 使用方式没有任何区别
print(g1)
print(g2)

然后进行编译,测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# setup_exe.py

# -*- coding:utf-8 -*-
import _locale
_locale._getdefaultlocale = (lambda *args: ['en_US', 'utf8'])

from distutils.core import setup, Extension
from Cython.Build import cythonize

setup(
ext_modules=cythonize([
Extension(
name='cython_test',
sources=['cython/*.pyx'],
include_dirs=['c_src'])
],
build_dir='build',
compiler_directives={'language_level': 3}
)
)

下面来进行导入:

1
2
3
4
5
import cython_test
"""
{'name': b'komeiji satori', 'age': 16}
{'name': b'komeiji koishi', 'age': 16}
"""

因为里面有 print 所以导入的时候自动打印了,看到 C 的结构体到 Python 中会变成字典。

有一点需要注意:使用 cdef extern from 导入头文件的时候,代码块里面的声明应该在 C 头文件里面存在。假设还想通过 ctypedef 给 int 起一个别名,而这个逻辑在 C 的头文件中是不存在的,而是自己想这么做,那么这个逻辑就不应该放在 cdef extern from 中,而是应该放在全局区域,否则是不起作用的。cdef extern from 里面的类型别名、声明什么的,都是根据头文件来的,将头文件中想要使用的放在 cdef extern from 中进行声明。而自己单独设置的声明、类型别名(头文件中不存在相应的逻辑)应该放在外面。

此外,除了 cdef extern from 之外,ctypedef 只能出现在全局区域(说白了就是没有缩进),像 if 语句、for 循环、while 循环、函数等等,内部都不能出现 ctypedef。

4、包装C函数

在最开始介绍斐波那契数列的时候,已经演示过这种方式了,再来感受一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// header.h
typedef struct {
char *name;
int age;
} Girl;
// 里面定义一个结构体类型 和 一个函数声明
char *return_info (Girl g);


// source.c
#include <stdio.h>
#include <stdlib.h>
#include "header.h"

char *return_info (Girl g) {
// 堆区申请一块内存
char *info = (char *)malloc(20);
// 拷贝一个字符串进去
sprintf(info, "name: %s, age: %d", g.name, g.age);
// 返回指针
return info;
}

from libc.stdlib cimport free

cdef extern from "header.h":
# C 头文件中变量的声明 和 Cython 这里的声明是很类似的
ctypedef struct Girl:
char *name
int age

# 声明函数时不需要使用 cdef
char *return_info(Girl)


# 然后我们说如果想被 Python 访问, 还需要定义一个包装器
# 我们通过 Python 无法直接调用 return_info, 因为它没有暴露给 Python
# 我们需要在 Cython 内部定义一个可以暴露给 Python 的函数, 然后在这个函数中调用 return_info
cpdef bytes info(dict d):
cdef:
# 接收一个字典
str name = d["name"]
int age = d["age"]

# 根据对应的值创建结构体实例, 但 name 需要转成 bytes 对象, 因为 char * 对应 Python 的 bytes 对象
cdef Girl g = Girl(name=bytes(name, encoding="utf-8"), age=age)
# 构造出结构体之后, 传到 C 的函数中, 得到返回值, 也就是字符串的首地址
cdef char *p = return_info(g)
# 这里需要先拷贝给 Python, 此时会根据 p 这个 char * 来创建一个 Python 的 bytes 对象, 然后让变量 res 指向
# 至于为什么不直接返回 p, 是因为 p 是在堆区申请的, 我们需要将它释放掉
res = p
free(p)
# 返回 res
return res

然后来进行编译:

1
2
3
4
5
6
7
from distutils.core import setup, Extension
from Cython.Build import cythonize

ext = [Extension("cython_test",
sources=["cython_test.pyx", "source.c"])]

setup(ext_modules=cythonize(ext, language_level=3))

最后调用:

1
2
import cython_test
print(cython_test.info({"name": "satori", "age": 16})) # b'name: satori, age: 16'

看到整体没有任何问题,但是很明显这个例子有点刻意了,故意兜这么一个圈子。但这么做主要是想介绍 C 和 Cython 之间的交互方式,以及 Cython 调用 C 库是有多么的方便。

5、常量、其它修饰符、以及控制 Cython 生成的内容

正如之前提到的,Cython 理解 const 修饰符,但它在 cdef 声明中并不是有效的。它应该在 cdef extern from 语句块中使用,用来修饰一个函数的参数或者返回值。

1
2
typedef const int * const_int_ptr;
const double *returns_ptr_to_const(const_int_ptr);

如果在 Cython 中声明的话,应该这么做。

1
2
3
cdef extern from "header.h":
ctypedef const int* const_int_ptr
const double *returns_ptr_to_const(const_int_ptr)

看到声明真的非常类似,基本上没太大改动,只需要将 typedef 换成 ctypedef、并将结尾的分号去掉即可,但事实上即使分号不去掉在 Cython 中也是合法的,只不过这不是符合 Cython 风格的代码。

除了 const 还有 volatile、restrict,但这两个在 Cython 中是不合法的。

另外在 Cython 中,偶尔为函数、结构体使用别名是很有用的,这允许在 Cython 中以不同的名称引用一个 C 级对象,怎么理解呢?举个栗子:

1
2
3
4
5
6
7
 // header.h
unsigned long __return_int(unsigned long);

// source.c
unsigned long __return_int(unsigned long n) {
return n;
}

C 函数前面带了两个下划线,看着别扭,再或者它和 Python 中的某个内置函数的名称、或者关键字发生冲突等等,这个时候需要为其指定一个别名。

1
2
3
4
5
6
7
8
9
cdef extern from "header.h":
# 在 C 中定义的是 __return_int, 但是这里我们为其起了一个别名叫做 return_int
# 再比如 ctypedef void klass "class", C 中定义的是 class, 但这是 Python 的关键字, 所以将其起个别名叫 klass
unsigned long return_int "__return_int"(unsigned long)
# 这个过程就你就可以看做是: C 中定义的名称是 __return_int, 这里的声明是 unsigned long return_int (unsigned long)

# 然后我们这里直接通过别名进行调用
def py_return_int(n):
return return_int(n)

编译一下进行测试,这里编译的代码不变。

1
2
import cython_test
print(cython_test.py_return_int(123)) # 123

看到没有任何问题,Cython 做的还是比较周密的,为考虑到了方方面面。这里起别名不仅仅可以对函数、ctypedef 使用,还可以是结构体、枚举之类的。

1
2
3
4
5
6
7
8
9
10
cdef extern from "header_file":
# C: struct del {int a, b}; 显然 del 是 Python 的关键字
struct _del "del":
int a, b

# C: enum yield {ALOT; SOME; ALTITLE;};
enum _yield "yield":
ALOT
SOME
ALITTLE

在任何情况下,引号中的字符串都是生成的 C 代码中的对象名,Cython 不会检查该字符串的内容,因此可以使用(滥用)这一特性来控制 C 一级的声明。

5.1 错误检测和引发异常

对于外部 C 函数而言,如果出现了异常,那么一种常见的做法是返回一个错误的状态码或者错误标志。但这些异常是在 C 中出现的异常,不是在 Cython 中出现的,因此为了正确地表示 C 中出现的异常,必须要对其进行包装。当在 C 中出现异常时,显式地将其引发出来。如果不这么做、而只是单纯的异常捕获的话,那么是无效的,因为 Cython 不会对 C 中出现的异常进行检测,所以在 Python 中也是无法进行异常捕获的

而如果想做到这一点,需要将 except 字句和 cdef 回调一起绑定起来。

5.2 回调

Cython 支持 C 函数指针,通过这个特性,可以包装一个接收函数指针作为回调的 C 函数。回调函数可以是不调用 Python/C API 的纯 C 函数,也可以调用任意的 Python 代码,这取决于要实现的功能逻辑。因此这个强大的特性允许我们在运行时通过 cdef 创建一个函数来控制底层 C 函数的行为,如果能实现这个功能的话就好办了。

但涉及到跨语言边界的回调可能会变得很麻烦,因为直接调用 C 的函数会很简单,只不过 C 内部的逻辑与我们无关,只是单纯的调用。但如果说在运行时,还能对 C 内部的实现插上一脚就不是那么简单了,特别是涉及到合适的异常处理的时候。

下面举栗说明,首先在 C 的标准库 stdlib 中有一个 qsort 函数,希望对它进行包装,来对 Python 中的列表进行排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
cdef extern from "stdlib.h":
# 这是 C 里面一个用于对数组进行排序的函数, 第一个参数是数组指针
# 第二个元素是数组元素的个数, 因为数组在作为参数传递的时候会退化为指针, 所以无法通过 sizeof 计算出元素个数
# 第三个参数是元素的大小
# 第四个参数是一个回调函数, 显然是每两个元素之间进行比较的逻辑; a > b 返回正数、a < b 返回负数、a == b 返回 0
# 而这第四个参数也是我们需要从外界(Cython)进行传递的, 此时就涉及到了 Cython 和 C 之间的交互
void qsort(
void *array,
size_t count,
size_t size,
int(*compare)(const void *, const void *)
)
# 从堆区申请内存、以及释放内存, 没错, 除了 from libc.stdlib cimport malloc, free 之外我们也可以使用这种方式
# 因为这两个函数本身就在 stdlib 中
void *malloc(size_t size)
void free(void *ptr)


# 定义排序函数
cdef int int_compare(const void *a, const void *b):
cdef:
int ia = (<int *>a)[0]
int ib = (<int *>b)[0]
return ia - ib

# 因为列表支持倒序排序, 所以需要再定义一个倒序排序函数
cdef int int_compare_reverse(const void *a, const void *b):
# 直接在正序排序的基础上乘一个 -1 即可
return -int_compare(a, b)

# 给一个函数指针起的类型别名
ctypedef int(*qsort_cmp)(const void *, const void *)

# 一个包装器, 外界调用的是这个 pyqsort, 在 pyqsort 内部会调用 qsort
cpdef pyqsort(list x, bint reverse=False):
"""
将 Python 中的列表转成 C 的数组, 用于排序, 排序之后再将结果设置到列表中
:param x: 列表
:param reverse: 是否倒序排序
:return:
"""
cdef:
int *array
int i, N
# 计算列表长度, 并申请对应容量的内存
N = len(x)
array = <int *>malloc(sizeof(int) * N)
if array == NULL:
raise MemoryError("内存不足, 申请失败")
# 将列表中的元素拷贝到数组中
for i, val in enumerate(x):
array[i] = val

# 获取排序函数
cdef qsort_cmp cmp_callback
if reverse:
cmp_callback = int_compare_reverse
else:
cmp_callback = int_compare

# 调用 C 中的 qsort 函数进行排序
qsort(<void *> array, <size_t> N, sizeof(int), cmp_callback)

# 调用 qsort 结束之后, array 就排序好了, 然后再将排序好的结果设置在列表中
for i in range(N):
# 注意: 此时不能对 array 使用 enumerate, 因为它是一个 int *
x[i] = array[i]
# 此时 Python 中的列表就已经排序好了

# 别忘记最后将 array 释放掉
free(array)

当导入自定义的 C 文件时,应该通过手动编译的方式,否则会找不到相应的文件。但这里导入的是标准库中的头文件,不是自己写的,所以可以不用编译,通过 pyximport 的方式即可实现导入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pyximport
pyximport.install(language_level=3)

import random
import cython_test

# 看到此时的 pyqsort 和 内置函数 一样, 都属于 built-in function 级别的, 是不是很有趣呢
print(cython_test.pyqsort) # <built-in function pyqsort>
print(max) # <built-in function max>
print(isinstance) # <built-in function isinstance>
print(getattr) # <built-in function getattr>

# 然后来看看结果如何吧, 是不是能起到排序的效果呢
lst = [random.randint(10, 100) for _ in range(10)]
print(lst) # [65, 36, 12, 84, 97, 15, 19, 86, 11, 78]
# 排序
cython_test.pyqsort(lst)
# 再次打印
print(lst) # [11, 12, 15, 19, 36, 65, 78, 84, 86, 97]
# 然后倒序排序
cython_test.pyqsort(lst, reverse=True)
print(lst) # [97, 86, 84, 78, 65, 36, 19, 15, 12, 11]

目前看起来一切顺利,没有任何障碍,而且在外部自己实现了一个内置函数,这是非常了不起的。

但是如果出现了异常呢?我们目前还没有对异常进行处理,现在我们将逻辑改一下。

1
2
3
4
cdef int int_compare_reverse(const void *a, const void *b):
# 其它地方完全不变, 只是在用于倒序排序的比较函数中加入一行 [][3], 故意引发一个索引越界
[][3]
return -int_compare(a, b)

然后我们再调用它,看看会有什么现象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import pyximport
pyximport.install(language_level=3)

import cython_test

cython_test.pyqsort([2, 1, 3], reverse=True)
"""
IndexError: list index out of range
Exception ignored in: 'cython_test.int_compare_reverse'
Traceback (most recent call last):
File "D:/satori/1.py", line 7, in <module>
cython_test.pyqsort(lst, reverse=True)
IndexError: list index out of range
IndexError: list index out of range
Exception ignored in: 'cython_test.int_compare_reverse'
Traceback (most recent call last):
File "D:/satori/1.py", line 7, in <module>
cython_test.pyqsort(lst, reverse=True)
IndexError: list index out of range
IndexError: list index out of range
Exception ignored in: 'cython_test.int_compare_reverse'
Traceback (most recent call last):
File "D:/satori/1.py", line 7, in <module>
cython_test.pyqsort(lst, reverse=True)
IndexError: list index out of range
"""

明明出现了索引越界错误,但是程序居然没有立刻停下来,而是被忽略掉了。而每一次排序都需要调用这个函数,所以出现了多次 IndexError。虽然出现了异常,但不影响程序的执行,如果再最后加上一个 print 逻辑,会发现它依旧正常打印,这显然不是我们想要的。那么下面就来解决它。

5.3 异常传递

上面的索引越界是在 int_compare_reverse 中设置的,而它的调用是发生在什么地方呢?显然是发生在 C 中,因为它很明显是作为回调传递给了 qsort 这个 C 函数。所以 int_compare_reverse 中的索引越界是在执行 C 的 qsort 函数时候发生的,而不是在 Cython 中,如果发生的地点是在 Cython 中,那么会直接引发错误,当然也可以异常捕获,和 Python 没有任何区别。但不幸的是,这个函数是一个传递给 C 的回调函数,它是在 C 中被调用的。而为了解决这一点, Cython 提供了一个 except * 字句来帮更好的处理异常。举个栗子:

1
2
3
cdef int int_compare_reverse(const void *a, const void *b) except *:
[][3]
return -int_compare(a, b)

只需要在结尾加上一个 except *,那么便可自动实现异常的传递。看到这个 except 是不是有点熟悉呢?之前在介绍 C 中的除法时,说如果返回值是 C 的类型、并且 C 中的整型发生除以 0 的情况,异常不会向上抛,需要在函数的结尾指定 except ? -1,来充当一个哨兵。这里也是与之类似的,通过指定 except *,使得它在作为回调函数的时候,如果内部发生了异常,能够转成传递给 Cython。但是这样还是不够的,因为这样会导致类型不匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
cdef extern from "stdlib.h":
void qsort(
void *array,
size_t count,
size_t size,
# 这里也需要加上 except *, 因为类型要一致
int(*compare)(const void *, const void *) except *
)
void *malloc(size_t size)
void free(void *ptr)


# 显然这里也要加上 except *
cdef int int_compare(const void *a, const void *b) except *:
cdef:
int ia = (<int *>a)[0]
int ib = (<int *>b)[0]
return ia - ib

cdef int int_compare_reverse(const void *a, const void *b) except *:
[][3]
return -int_compare(a, b)

# 这里也是如此, 否则类型不匹配
ctypedef int(*qsort_cmp)(const void *, const void *) except *

然后再来调用一下试试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import pyximport
pyximport.install(language_level=3)

import cython_test

lst = [2, 1, 3]
cython_test.pyqsort(lst, reverse=True)
"""
Traceback (most recent call last):
File "cython_test.pyx", line 21, in cython_test.int_compare_reverse
[][3]
IndexError: list index out of range

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
File "D:/satori/1.py", line 7, in <module>
cython_test.pyqsort(lst, reverse=True)
SystemError: <built-in function pyqsort> returned a result with an error set
"""

看到此时程序就直接终止了,因为虽然错误在 C 中出现的,但是它传递给了 Cython,所以程序终止了。而且 Cython 在接收到这个异常时,并没有原原本本的直接输出,而是又引发了一个 SystemError,因为它是在 C 中出现的。

总结一下:Python 在调用 Cython 时(可以是 pyx、pyd),如果发生了异常,那么就看这个异常是在哪里发生的。如果是在 Cython 中,那么和纯 Python 中发生异常时的表现是一样的,可以使用 try except 进行异常捕获。但如果是在 C 中发生的(出现这种情况的可能性非常有限,基本上都是作为 C 函数的一个回调函数,在 C 函数中调用这个回调函数引发异常),那么异常不会导致程序停止、也无法进行异常捕获(因为异常会被忽略掉),需要在回调函数的结尾加上 except *,来使得在 C 中发生异常时能够传递给 Cython。

如果这里的索引越界是在 pyqsort 中出现的,那么直接就会出现 IndexError,程序终止。因为,异常在 Cython 中的表现和 Python 是一模一样的。

异常的传递真的是非常的不容易,通过 except * 这种方式,使得 Cython 中即可以定义一个 C 函数的回调函数、还能在出现异常的时候传递给 Cython,这个过程真的是走了很长的一段路。

6、在Cython中引入C++

下面来看看如何在 Cython 中引入 C++,这里主要介绍如何编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# distutils: language=c++
from libcpp.vector cimport vector
from libcpp.map cimport map

cdef vector[int] vect

cdef int i
for i in range(3):
vect.push_back(i) # 类似于 Python 列表的 append
for i in range(3):
print(vect[i])

cdef map[int, int] m
m[123] = 456
print(m[123])

注意:看一下最上面的注释,如果想要编译成功,那么必须要在开头加上 # distutils: language=c++。并且一定要通过 setup 进行编译,采用 pyximport 是会失败的。

然后来测试一下。

1
2
3
4
5
6
7
import cython_test
"""
0
1
2
456
"""

6.1 C++中的异常

然后是 C++ 中的异常,Cython 无法抛出 C++ 中的异常,并且也无法使用 try-except 语句进行捕获。但是可以进行特殊的声明,当 C++ 函数中引发异常的时候能够转化成 Python 的异常。先来看看 C++ 的异常和 Python 的异常之间的对应关系:

c++

假设在 C++ 的函数中可能引发 bad_cast,那么我们在声明函数时就可以这么做:

1
2
cdef extern from "some_file.h":
cdef int foo() except +TypeError

然后在调用 C++ 函数的时候,就可以进行异常捕获了,但如果不确定会引发什么错误,那么声明的时候通过 except +* 的方式即可,相当于 Python 的 Exception。

7、引入静态库和动态库

在 Windows 上静态库是以 .lib 结尾、动态库是以 .dll 结尾;在 Linux 上静态库则以 .a 结尾、动态库以 .so 结尾。

lib

7.1 Cython 静态库 动态库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import _locale
_locale._getdefaultlocale = (lambda *args: ['en_US', 'utf8'])

import os
from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize

setup(
name='databus.python',
version='v1.0',
author='wdn',
author_email='dongnian.wang@outlook.com',
description='databus python wrapper',
keywords='databus zmq',
package_dir={'': '.'},
packages=['databus'],
ext_modules=cythonize([
Extension(
'*',
['databus/*.pyx'],
include_dirs=['databus_c_lib/include'],
library_dirs=['databus_c_lib/lib'],
libraries=['databus'])
],
build_dir='build/cython',
compiler_directives={'language_level': 3}
)
)