【翻译】Cython教程5_Cython与外部C代码交互

原创文章,转载请注明: 转载自勤奋的小青蛙
本文链接地址: 【翻译】Cython教程5_Cython与外部C代码交互

 

Cython的主要用途之一是包装现有的C代码库。这是通过使用外部声明来声明库函数和要使用的库中的C函数来实现的。

您还可以使用公共声明使Cython模块中定义的C函数和变量可用于外部C代码。这种需求预计不太频繁,但你可能想这样做,例如,如果你在另一个应用程序中嵌入Python作为脚本语言。正如一个Cython模块可以用作一个桥梁,允许Python代码调用C代码,它也可以用于允许C代码调用Python代码。

外部声明

默认情况下,在模块级声明的C函数和变量对模块是本地的(即它们具有C静态存储类)。它们也可以声明为extern,以指定它们在其他位置定义,例如:

cdef extern int spam_counter

cdef extern void order_spam(int tons)

引用C头文件

当你在上面的例子中使用一个extern定义时,Cython在生成的C文件中包含一个声明。如果声明与其他C代码将看不到的声明不完全匹配,这可能会导致问题。例如,如果要封装现有的C库,那么生成的C代码必须与库的其余部分具有完全相同的声明。

为了实现这一点,你可以告诉Cython声明将在C头文件中找到,如下所示:

cdef extern from "spam.h":

    int spam_counter

    void order_spam(int tons)

这个cdef extern代码块定义了如下三件事情:

  1. 它指示Cython为生成的C代码中的命名头文件放置一个#include语句。
  2. 它阻止Cython为相关块中的声明生成任何C代码。
  3. 它处理块中的所有声明,就像它们以cdef extern开头。

重要的是要理解Cython本身不读取C头文件,所以你仍然需要提供任何声明的Cython版本,你使用。然而,Cython声明并不总是必须完全匹配C,在某些情况下,它们不应该或不能。尤其是:

  1. 不要使用任何平台特定的C语言扩展,例如__declspec()。
  2. 如果头文件声明一个大结构,并且你只想使用几个成员,你只需要声明你感兴趣的成员。留下余下的没有任何危害,因为C编译器将使用头文件中的完整定义。

在某些情况下,你可能不需要任何struct的成员,在这种情况下,你可以只传递在struct声明的主体,例如:

cdef extern from "foo.h":
    struct spam:
        pass

注意:你只能在一个cdef extern从块里面这样做;任何其他地方的struct声明必须是非空的。

3.  如果头文件使用typedef名称(如word)来引用与平台相关的数值类型的风格,则需要一个相应的ctypedef语句,但不需要完全匹配类型,只是使用一些正确的一般类型(int,float等)。例如,:

ctypedef int word

将工作正常无论实际大小的单词是(提供的头文件正确定义它)。与Python类型(如果有)之间的转换也将用于此新类型。

4. 如果头文件使用宏来定义常量,则将它们转换为正常的外部变量声明。如果它们包含正常的int值,也可以将它们声明为枚举。请注意,Cython认为枚举等同于int,因此不要对非int值执行此操作。

5. 如果头文件使用宏定义了一个函数,那么声明它就像是一个普通的函数,具有适当的参数和结果类型。

6. 如果头文件使用宏定义了一个函数,那么声明它就像是一个普通的函数,具有适当的参数和结果类型。

如果你想包含一个C头,因为它是另一个头需要的,但不想使用它的任何声明,在extern-from块中放入pass关键字:

cdef extern from "spam.h":
    pass

如果要包括系统标题,请在引号中加上尖括号:

cdef extern from "<sysheader.h>":
    ...

如果你想包含一些外部声明,但不想指定一个头文件(因为它包含了你已经包含的其他头文件),你可以用*代替头文件名:

cdef extern from *:
    ...

在C中实现函数

当你想从一个Cython模块调用C代码,通常该代码将在一些外部库中,你链接你的扩展。但是,您也可以直接编译C(或C ++)代码作为Cython模块的一部分。在.pyx文件中,您可以输入以下内容:

cdef extern from "spam.c":
    void order_spam(int tons)

Cython将假设函数order_spam()在文件spam.c.中定义。如果你还想从另一个模块cimport这个函数,它必须在.pxd文件中声明(不是extern!):

cdef void order_spam(int tons)

为了这个工作,spam.c中的order_spam()的签名必须匹配Cython使用的签名,特别是函数必须是static:

static void order_spam(int tons)
{
    printf("Ordered %i tons of spam!\n", tons);
}

结构,联合和枚举声明的样式

在C头文件中可以使用两种主要方法来声明结构,联合和枚举:使用标签名称或使用typedef。基于这些的各种组合也存在一些变化。

重要的是使Cython声明与头文件中使用的样式相匹配,以便Cython能够对其生成的代码中的类型发出正确的引用。为了实现这一点,Cython提供了两种不同的语法来声明结构,联合或枚举类型。上面介绍的样式对应于标签名的使用。要获得另一个样式,您需要在声明前面加上ctypedef,如下图所示。

下表显示了可以在头文件中找到的各种可能的样式,以及应该放在cdef extern from块中的相应Cython声明。用结构体声明作为例子;这同样适用于联合和枚举声明。

QQ截图20170321215027

访问Python / C API例程

cdef extern from语句的一个特殊用途是在Python / C API中访问例程。例如,:

cdef extern from "Python.h":

    object PyString_FromStringAndSize(char *s, Py_ssize_t len)

将允许您创建包含空字节的Python字符串。

特殊类型

Cython预定义名称Py_ssize_t用于Python / C API例程。要使扩展与64位系统兼容,您应该始终使用此类型,它在Python / C API例程的文档中指定。

Windows调用约定

__stdcall和__cdecl调用约定说明符可以在Cython中使用,与Windows上的C编译器使用的语法相同,例如:

cdef extern int __stdcall FrobnicateWindow(long handle)

cdef void (__stdcall *callback)(void *)

如果使用__stdcall,该函数仅被认为与相同签名的其他__stdcall函数兼容。

解决命名冲突 - C名称规范

每个Cython模块都有一个用于Python和C名称的模块级命名空间。如果你想包装一些外部C函数并为Python用户提供相同名称的Python函数,这可能会很不方便。

Cython提供了几种不同的方法来解决这个问题。最好的方法,特别是如果你有很多C函数要包装,是将extern C函数声明放入一个.pxd文件,从而不同的命名空间,使用Cython模块之间共享声明中描述的功能。将它们写入.pxd文件允许它们跨模块重用,避免以正常的Python方式命名冲突,甚至可以很容易地在cimport上重命名它们。例如,如果你的decl.pxd文件声明了一个C函数eject_tomato:

cdef extern from "myheader.h":
    void eject_tomato(float speed)

那么您可以导入并将其包装在.pyx文件中,如下所示:

from decl cimport eject_tomato as c_eject_tomato

def eject_tomato(speed):
    c_eject_tomato(speed)

或者简单地cimport .pxd文件并将其用作前缀:

cimport decl

def eject_tomato(speed):
    decl.eject_tomato(speed)

注意,这没有运行时查找开销,如在Python中。 Cython在编译时解析.pxd文件中的名称。

对于在导入时命名空间或重命名不够的特殊情况,例如当C中的名称与Python关键字冲突时,可以使用C名称规范在声明时为C函数提供不同的Cython和C名称。假设,例如,你想包装一个外部C函数称为yield()。如果你声明它:

cdef extern from "myheader.h":
    void c_yield "yield" (float speed)

那么它的Cython可见名称将是c_yield,而其在C中的名称将是yield。然后,您可以将其包装:

def call_yield(speed):
    c_yield(speed)

对于函数,可以为变量,结构体,联合体,枚举,结构体和联合成员以及枚举值指定C名称。例如:

cdef extern int one "eins", two "zwei"
cdef extern float three "drei"

cdef struct spam "SPAM":
    int i "eye"

cdef enum surprise "inquisition":
    first "alpha"
    second "beta" = 3

请注意,Cython不会对您提供的字符串进行任何验证或名称调整。它会将裸体文本注入到C代码中,因此您完全可以使用此功能。如果你想声明一个名字xyz,并让Cython注入文本“使C编译器失败在这里”到它的C文件,你可以使用C名称声明。考虑这是一个高级功能,只有在一切失败的罕见情况下。

在C语言中使用Cython声明(意味着在C语言中调用Cython模块)

Cython提供了两种方法,用于从Cython模块中创建C语言声明,供外部C代码公用声明和C API声明使用。

 

公开声明

您可以通过使用public关键字声明C模块中链接的C代码来访问Cython模块中定义的C类型,变量和函数:

cdef public struct Bunny: # public type declaration
    int vorpalness

cdef public int spam # public variable declaration

cdef public void grail(Bunny *): # public function declaration
    print "Ready the holy hand grenade"

如果在Cython模块中有任何公共声明,则会生成一个名为modulename.h文件的头文件,其中包含等同的C声明,以包含在其他C代码中。

在C中嵌入Python的用户需要确保调用Py_Initialize()和Py_Finalize()。例如,在包含modulename.h的以下代码段中:

#include <Python.h>
#include "modulename.h"

void grail() {
    Py_Initialize();
    initmodulename();
    Bunny b;
    grail(b);
    Py_Finalize();
}

任何要使用这些声明的C代码都需要与扩展模块静态或动态链接。

如果Cython模块驻留在包中,则.h文件的名称由模块的完整点名组成,例如。一个名为foo.spam的模块将具有一个名为foo.spam.h的头文件。

C API声明

另一种使声明可用于C代码的方法是使用api keywor声明它们.您可以将此关键字与C函数和扩展类型一起使用。生成一个名为modulename_api.h的头文件,包含函数和扩展类型的声明,以及一个名为import_modulename()的函数。

想要使用这些函数或扩展类型的C代码需要包含头并调用import_modulename()函数。然后可以调用其他函数,并使用正常使用的扩展类型。

如果希望使用这些函数的C代码是多个共享库或可执行文件的一部分,则需要在使用这些函数的每个共享库中调用import_modulename()函数。如果你在调用其中一个api调用时遇到段错误(linux上的SIGSEGV)时崩溃,这可能表明包含生成分段故障的api调用的共享库在api调用崩溃之前不调用import_modulename()函数。

当您包含modulename_api.h时,Cython模块中的任何公共C类型或扩展类型声明也可用:

# delorean.pyx
cdef public struct Vehicle:
    int speed
    float power

cdef api void activate(Vehicle *v):
    if v.speed >= 88 and v.power >= 1.21:
        print "Time travel achieved"
# marty.c
#include "delorean_api.h"

Vehicle car;

int main(int argc, char *argv[]) {
    import_delorean();
    car.speed = atoi(argv[1]);
    car.power = atof(argv[2]);
    activate(&car);
}

注意:在Cython模块中定义的用作导出函数的参数或返回类型的任何类型都需要声明为public,否则它们不会包含在生成的头文件中,并且当您尝试编译使用标头的C文件时,您将收到错误。

使用api方法不需要C代码使用声明以任何方式与扩展模块链接,因为Python导入机制用于动态地建立连接。但是,只有函数可以以这种方式访问,而不是变量。

您可以在同一个函数上使用public和api,以使两种方法都可用,例如:

cdef public api void belt_and_braces():
    ...

但是,请注意,您应该在给定的C文件中包括modulename.h或modulename_api.h,而不是两者,否则您可能会遇到冲突的双重定义。

如果Cython模块驻留在包中,则:

  1. 头文件的名称包含模块的完整点名。
  2. 导入函数的名称包含用双下划线替换的点的全名。

例如。一个名为foo.spam的模块将有一个名为foo.spam_api.h的API头文件和一个名为import_foo__spam()的导入函数。

多个公共和API声明

您可以通过将它们封装在cdef块中,将一整组项目声明为public和/或api,例如:

cdef public api:
    void order_spam(int tons)
    char *get_lunch(float tomato_size)

这可以在.pxd文件(参见Cython模块之间的共享声明)中做一个有用的事情,以使模块的所有三个方法的公共接口可用。

获取和释放GIL

Cython提供了获取和释放(GIL)的方法。当从多线程代码调用(外部C)代码可能会阻塞,或者当想要从(本地)C线程回调中使用Python时,这可能很有用。释放GIL显然只应该对线程安全代码或使用其他保护手段免受竞争条件和并发问题的代码。

注意,获取GIL是阻塞线程同步操作,因此可能是昂贵的。可能不值得释放GIL进行次要计算。通常,并行代码中的I / O操作和实质计算将从中受益。

释放GIL

你可以使用with nogil语句释放一段代码的GIL:

with nogil:
    <code to be executed with the GIL released>

语句体中的代码不能以任何方式操作Python对象,并且不能在没有首先重新获取GIL的情况下调用任何操纵Python对象的操作。 Cython目前不检查这个。

获得GIL

一个C函数将被用作来自C代码的回调,该代码在没有GIL的情况下执行,需要获取GIL才能操作Python对象。这可以通过在函数头中用gil指定:

cdef void my_callback(void *data) with gil:
    ...

如果回调可能从另一个非Python线程调用,那么必须首先通过调用PyEval_InitThreads()来初始化GIL。如果你已经在你的模块中使用cython.parallel,那就不用担心这个问题了。

GIL也可以通过with gil语句获取:

with gil:
    <execute this block with the GIL acquired>

将函数声明为可调用而不使用GIL

您可以在C函数头或函数类型中指定nogil,以声明在没有GIL的情况下调用是安全的:

cdef void my_gil_free_func(int spam) nogil:
    ...

当你在Cython中实现这样的函数时,它不能有任何Python参数或Python对象返回类型。此外,任何涉及Python对象(包括调用Python函数)的操作必须首先明确获取GIL,例如。通过使用一个与gil块或通过调用已定义的gil函数。这些限制由Cython检查,如果发现任何Python交互内部的nogil代码部分,你会得到一个编译错误。

注意:nogil函数注释声明调用没有GIL的函数是安全的。它完全允许执行它同时持有GIL。该函数本身不释放GIL,如果它由调用者持有。

使用gil声明函数(即获取条目上的GIL)也隐式地使其签名nogil。

 

原创文章,转载请注明: 转载自勤奋的小青蛙
本文链接地址: 【翻译】Cython教程5_Cython与外部C代码交互

文章的脚注信息由WordPress的wp-posturl插件自动生成



|2|left
打赏

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: