《Cython系列》6. 使用Cython包装C、C++外部库

楔子

在前面的系列中我们知道了Cython如何通过提前编译的方式来对Python代码进行加速,这一节我们聚焦在另一个方向上:假设有一个C编写的库,那么如何才能让Python访问它呢?

事实上,Python访问C编写的库,我在其它文章中介绍过。当时的方式是将C代码编译成动态库的方式,然后通过Python自带的ctypes模块来调用它,当然除了ctypes,还有swig、cffi等专门的工具。而Cython也是支持我们访问C库的,只不过它是通过包装C库的方式让我们访问。

因为Cython同时理解C和Python,所以它可以在Python语言和C语言结合的时候控制所有的方方面面,在完成这一壮举的同时,不仅保持了Python的风格,还使得C代码更加容易定位和调试。

如果做得好,那么Cython包装的库会具备C的性能、最小的包装开销、友好的Python接口,用户完全不需要怀疑它们正在使用的包装好的代码。

在Cython中声明外部的C代码

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

cdef extern from "header_name":
    # 相应的声明,如果不需要的话可以写上一个pass

头文件使用单引号或者双引号括起来,事实上我们之前提到过。extern具有如下效果:

  • 1. Cython编译器会在生成的源文件中写入#include "header_name"
  • 2. 在extern语句块中的类型、函数以及其它声明都可以在Cython中直接使用
  • 3. Cython会在编译时检查C的声明是否正确,如果不正确会编译错误。

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

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

cdef extern from *:
    # 声明

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

Cython不会自动包装

extern语句块的目的很简单,但是乍一看可能会产生误导。在Cython中存在extern块(extern声明),确保我们能够以正确的类型调用声明的C函数、变量、结构体等等。但是它不会自动地为对象创建Python的包装器,我们仍然需要使用def、或者cpdef(可能还会cdef)来调用extern块中声明的C函数。因为如果不这么做,则无法从Python代码中访问extern块中声明的外部C函数。因为Cython不会自动解析C文件、以及包装C库给外部Python访问,我们需要手动实现这一点。

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

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

  • 1. 将typedef变成ctypedef

  • 2. 删除类似于restrict、volatile等不必要、以及不支持的关键字

  • 3. 确保函数的返回值和对应类型的声明在同一行

    //在C中,可以这么写
    int 
    foo(){
        return 123
    }
    
  • 4. 删除行尾的分号

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

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

#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中使用。

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中声明M_PI这个宏时,将其声明为double型的全局变量,同理对于MAX宏也是如此,就把它当成接收两个float、返回一个float的名为MAX函数。

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

Cython支持所有的C声明,这些声明和C是保持高度相似的,我们只需要做简单的修改就可以直接贴在extern里面了。

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

Cython支持所有的C声明,这些声明和C是保持高度相似的,我们只需要做简单的修改就可以直接贴在extern里面了。

嵌入C代码,个人写着写着突然很烦,所以不想写了,有兴趣可以自己去了解。另外除了C,Cython还可以包装C++,但笔者不会C++,所以也跳过了。

原文地址:https://www.cnblogs.com/traditional/p/13285339.html