Python与C的交互

Python部落(python.freelycode.com)组织翻译,禁止转载,欢迎转发。

满世界都是轮子,从轮子的更深一层又总可以找到另一个轮子。于是,每隔一段时间,世界上一些开发者就会从Python的开发转向对C的探索。从zlib到SQLite,再到OpenSSL,亦或是为了追求速度、效率,亦或是为了追求功能,这种潮流强劲有力、波涛汹涌。好消息是,Python与C的交互可以作为探索的终点。

历史简介

Cpython是最广泛使用的python解释器,通过名字就可以看出来,它是用C编写的。Python的核心开发者提倡并展示了Python强大的C语言根基,在可移植性上采用了传统策略,与世界流行的“一次编写,导出运行”的方式形成了鲜明对比。Python社区与核心开发者携手,开发了几种连接C的方法。这些交互方式经过多年的发展,使Python拥有了优秀的接口环境去调用操作系统、数据处理相关库以及各种C语言提供的资源。

下面的表展示了很多交互方法:

交互方法完成时间典型用户优点

缺点

C扩展包1991Python标准库丰富的文档与教程,可全面控制需编译,可移植性有限,需管理引用,C功底要求高
SWIG1996crfsuite可一次性绑定多种语言对于Python开发来说,超出使用范围
ctypes2003oscrypto无需编译,适用范围广访问、改变C的结构体较为繁琐且容易出错
Cpython2007Gevent, kivy类Python、高成熟度、高性能需编译、新的语法与工具链
cffi2013Cryptography, pypy易于集成,pypy兼容性较高太新、更新速度快

一个表格包含的历史与简介有限,但是所有方法都可以归为以下三类:

  1. 直接写C语言

  2. 编写代码后转义为C语言

  3. 调用含有C接口的库

三类方法各有千秋,下面我们对每一种进行分析,并且列举一些实际应用案例。

编写C

Python的核心开发者能做到的,你也能。为Python写C扩展可以提供很棒的接口,前提是需要了解、编写、建立和调试C代码。由于中断整个进程的段错误比Python的异常更糟糕,所以错误造成的后果也更严重,尤其是在异步环境中同一个进程处理大量请求的时候。此外还需要考虑,扩展要兼容Cpython,以及在其他执行环境中的适用。

在PayPal,我们使用过C扩展来提升服务序列化的速度。当我们解决了构建和可移植性的问题的时候,也就完成了各项引用工作,并且可以直接为新的代码写C扩展。

转义为C

一些开发者经过多年的C编写决定再进一步。

Python风格

Cpython是python的扩展集,它能够将注释型的Python转化为C扩展,已经有近十年的历史了,可以追溯到Python的前身Pyrex。除了成熟性之外,Cpython的一些其他特点值得我们关注:

  • 每个Python文件都是有效的Cpython文件,可进行增量、迭代优化

  • 生成的C代码可移植度高,在Windows、Mac和Linux上均可生成

  • 应用时只需加载生成的C代码,无需安装Cpython

更何况,受科学计算的推动,生成的C代码会使用一些手写起来困难又晦涩的表现技巧。Cpython代码与Python本身高度集成,可以深入到堆栈跟踪及每一行代码。这些足以体现Cpython的强大。

PayPal确实从一些高性能的Cpython应用中获益匪浅,比如gevent,lxml和Numpy。然而我们在2011年第一次使用Cpython时并没有坚持下去,直到2015年,所有的本地扩展才转为使用Cpython编写或重新编写。这在PayPal几乎史无前例。

尝试SWIG

早期在PayPal的Python开发者让我们使用SWIG(简化装饰器及接口生成器)来封装PayPal的C++底层结构。然而经过一段时间后,我们发现与Python风格的技术相比,每一处修改都显得步履蹒跚。不久,我们决定放弃使用。

很久之前,SWIG的扩展模块很符合Python程序员的选择。但近些日子,SWIG的发展似乎更迎合C开发者的追求:简洁快速地封装与其他语言的绑定。这也说明,SWIG的替代库与SWIG中用于Python的用法一样多,我们可以试试其他方法。

调用C

上面的示例交互方法都涉及到额外的构建步骤、可移植性问题以及Python以外的语言知识。下面我们深入一些更接近Python本身动态特性的交互方法:ctypes和cffi。

ctypes和cffi都利用了C语言的外部函数接口(FFI),通过这个底层接口可以获取编译文件的可调用入口,比如Linux、FreeBSD等系统中的共享对象(.so文件)和Windows系统中的动态链接库(.dll文件)。共享文件在调用之前需要一些额外的工作,所以ctypes和cffi都使用了libffi,通过libffi才可以动态调用其他C语言库。

libffi可以弥补一些C语言库的不足。Linux的.so文件、Windows的.dll文件和OS X的.dylib文件只能提供符号:名称到内存地址的映射,而更多的是函数指针。动态链接器并不提供使用内存地址的方法。当动态地将共享库链接到C代码时,头文件就会提供函数签名。只要共享的库和应用程序被ABI(应用程序二进制接口)兼容,就不会出错。ABI由C语言编译器定义,通常都小心管理,避免频繁修改。

然而,Python并不是一个C语言编译器,所以即便有内存地址和函数签名,也不能够调用C,这就是libffi的作用所在。如果符号定义了调用接口的位置,头部文件定义了需调用的接口,那么libffi就会将二者转化为调用接口的方法。这还不够,在其他任务中我们仍需要一个高于libffi的层,来将本地的Python类型与C类型相互转换。

ctypes

ctypes是一个较早的Python化的与FFI交互的方法,最值得注意的是它被包含在Python标准库中。

ctypes不仅能用,并且很好用,广泛应用于Cpython,PyPy,Jython,IronPython等优秀Python项目中。通过ctypes可以在无需任何外部依赖时,利用纯Python代码访问C的接口。这在快速调用C时很方便,比如一个Windows接口没有暴露在操作系统模块中的情形。如果你有少量的小模块需要访问一两个C函数,ctypes可以在不添加重量级依赖的情况下就可以实现。

一段时间以来,PayPal的Python代码放弃了SWIG转向ctypes。我们发现调用普通共享对象(由C++建立的外部C程序)要比处理SWIG的工具链方便的多。在调用广泛使用的共享对象时,ctypes在整个代码中用的很少。典型的例子就是优秀的开源项目oscrypto,它主要用来实现网络安全。ctypes对于大型库、更新较快的库来说并不适用。将签名从头文件移到Python代码中极易出错,而且工作量庞大。

cffi

cffi来自于PyPy项目,是现在最流行的集成C的方法。开发者们过去一直在寻找一种能够发挥PyPy优化潜力的方法,最终他们创建了一个库,并弥补了ctypes很多不足。使用起来也很方便,从C的头文件中引用复制即可,无需人工编写函数签名的Python表达。

cffi的便利,也带来了局限性。考虑预处理宏时,C几乎变成了另一种语言。当宏执行字符串替换时,复杂程度超乎你的想象。由于cffi受到预处理宏的约束,cffi是否适用需要考虑你所集成的库。

cffi确实实现了在PyPy下优于ctypes的目的,但是在Cpython下仍然不相伯仲。cffi依然很年轻,它的未来很值得我们期待。

集三者之大成:PKCS11

PKCS11是一套密码标准,许多硬件、软件安全系统在交互时都采用它。200多页的核心规范说明中介绍了客户端接口:一套大量的C头部信息。信息里有很多向前兼容绑定,但是不同供应商的设备又有不同的喜好,它是如何做到兼容的呢?

元编程

上面提到,ctypes对于新增的接口支持的并不好。转换函数签名的复杂工作就会造成转录错误。我们确实有少量的自动化操作高需求,但ctypes远不够完美。

第二个方法是cffi,它在第一版支持的特征子集中能够正常运行。但是PKCS11定义函数遵循的是自己的CK_DECLARE_FUNCTION宏,而不是常规的C语法。因此cffi会跳过预处理命令#define宏,造成C代码不可被解析,最终不可用。另外,还有一些编译器或者操作系统自带的宏符号也会被跳过,如__cplusplus,_WIN32_,__linux__等。所以就算cffi成功处理了很多宏,也会出现新的错误。

简而言之,问题很严峻,PKCS11很粗糙,尤其体现在以下方面:

  1. 大量的重要常量使用#define定义

  2. 同一文件中,宏被多次定义

  3. pkcs11.h文件被包含多次,甚至作为一个结构体时也会被包含多次

后来我们发现,这套标准很少变动,最好的办法就是为标准所使用的特定规则编写一个严格的分析程序,再通过Cpython生成C,这样我们终于能够完全顺利地访问客户端了,在一些特殊情况下甚至还有性能的提升。啃下这块硬骨头花了我们一天半的时间,并且大家对结果很满意。

解析表达式语法

解析表达式语法(PEGs)使用了真正意义的编译器的来生成抽象语法树,并且支持正则表达式,这与Python生成抽象语法树有着本质区别。一些人把解析表达式看成是递归的正则表达式。Python有一些库很好,例如parsimonious和parsley。parsimonious很简练,我们以前使用过。

我们定义了两种语法,一种用于pkcs11f.h,一种用于pkcs11t.h:

WechatIMG81.jpeg

代码语法风格很强,短小精悍。纵观全局,代码描述的是一个很清晰地过程:

  1. 代码放在头文件中,用来获取抽象语法树

  2. 按照抽象语法树,筛选出应用中重要的部分与函数签名

  3. 从函数签名数据结构中生成代码

仅仅200行代码就实现了可靠的强大标准,并兼具Cpython的可移植性与高性能。这体现了解析表达式语法的强大,解析表达式语法也是PayPal在Python实践应用中的佼佼者。

总结

探索过程并不是一帆风顺的,但我们坚持住了,并且很欣慰能够攻克难关。回顾一下:

  • Python与C关系十分紧密

  • 不同的集成C的技术有不同的适用环境,我们的建议是:

    • ctypes用于动态调用小而稳定的接口

    • cffi用于动态调用更大的接口,PyPy更能体现优越性

    • 如果你擅长旧式的C扩展,也可以选择

    • 其余情况推荐基于Cpython的C扩展

    • 不推荐SWIG

  • 解析表达式语法很赞

从文章可以看出我们热爱Python的原因。Python是一个很棒、很火的语言,但作为一门操作系统和生态系统的语言来说又有一些不足。恰恰是从无到有、从少到多、上下贯通、不断发展使得Python成为了不可替代、无与伦比的语言。

英文原文:https://www.paypal-engineering.com/2016/09/22/python-by-the-c-side/
译者:爱生活没道理
 

2月15日11:00到13:00网站停机维护,13:00前恢复
iPy智能助手 双击展开
查看更多聊天记录
(Ctrl+回车)换行