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() { return123 }
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)) doublehypot(double, double); typedefint integral; typedefdouble real; voidfunc(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 函数。
因为里面有 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。
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 中的列表进行排序。
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 """
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。