22.2 创建Python扩展

为Python创建扩展需要3个主要的步骤:

1.创建应用程序代码;

2.利用样板来包装代码;

3.编译与测试。

在这一节中,我们会将这3步逐一介绍给大家。

22.2.1 创建您的应用程序代码

首先,我们要建立的是一个“库”,要记住,我们要建立的是将在Python内运行的一个模块。所以在设计你所需要的函数与对象的时候要注意到,你的C代码要能够很好地与Python的代码进行双向的交互和数据共享。

然后,写一些测试代码来保障你的代码的正确性。你可以在C代码中放一个main()函数,使得你的代码可以被编译并链接成一个可执行文件(而不是一个动态库),当你运行这个可执行文件时,程序可以对你的软件库进行回归测试。这是一种很符合Python风格的做法。

在下面的例子中,我们就将釆用这种做法。测试用例分别针对我们想要导出到Python世界的两个函数。一个是递归求阶乘的函数fac()。另一个reverse()函数则实现了一个简单的字符串反转算法,其主要目的是修改传入的字符串,使其内容完全反转,但不需要用申请内存后反着复制的方法。由于涉及到指针的使用,我们务必要在设计和调试时小心谨慎,以防把问题带入Python.

例22.1中所列出的Extestl.c是我们的第一个版本。

代码中,包含了两个函数fac()和reverse()。分别实现了我们刚刚所说的两个功能。fac()接受一个整型参数并递归计算结果,在退出最后一层调用后最终返回到调用代码中。

最后一段代码是必要的main()函数。我们在这里面写测试代码,传不同的参数给fac()和reverse()。有了这个函数,我们就可以了解我们的代码是否能得到正确的结果。

现在,我们就可以编译这段代码了。在大部分有gcc编译器的Unix系统中,我们都可以用以下指令进行编译:

22.2 创建Python扩展 - 图1

我们可以输入一下命令来运行我们的程序,并得到如下输出

22.2 创建Python扩展 - 图2

22.2 创建Python扩展 - 图3

例22.1 纯C版本库(Extestl.c)

下面列出了我们想要包装并在Python解释器中使用的C函数的代码,main()是测试函数。

22.2 创建Python扩展 - 图4

我们要再强调一次,你应该尽可能地完善你的代码。因为,在把代码集成到Python中后再来调试你的核心代码,查找潜在的bug是件很痛苦的事情。也就是说,调试核心代码与调试集成这两件事应该分开来做。要知道,与Python的接口代码写得越完善,集成的正确性就越容易保证。

我们的两个函数都只接受一个参数,并返回一个值。这是很标准的情况,与Python集成的时候应该不会有什么问题。注意,到现在为止,我们所做的都还与Python没什么关系。我们只是简单地创建了一个C/C++的应用程序而已。

22.2.2 用样板来包装你的代码

整个扩展的实现都是围绕着13.15.1节所说的“包装”这个概念进行的。你的设计要尽可能让你的实现语言与Python无缝结合。接口的代码被称为“样板”代码,它是你的代码与Python解释器之间进行交互所必不可少的一部分。

我们的样板主要分为4步。

1.包含Python的头文件。

2.为每个模块的每一个函数增加一个形如PyObject* Module_func()的包装函数。

3.为每个模块增加一个形如PyMethodDefModuleMethods[]的数组。

4.增加模块初始化函数void initModule().

1.包含Python头文件

首先,你要找到Python的头文件在哪,并且确保你的编译器有权限访问它们。在大多数类Unix的系统里,它们都会在/usr/local/include/python2.x或/usr/include/python2.x目录中。其中,“2.x”是你所使用的Python的版本号。如果你曾编译并安装过Python解释器,那应该不会碰到什么问题,因为这时,系统一般都会知道你的文件安装在哪。像下面这样在你的代码里加入一行:

22.2 创建Python扩展 - 图5

这部分比较简单。接下来再看看怎么在样板中加入其他的部分。

2.为每个模块的每一个函数增加一个型如PyObject* Module_func()的包装函数

这一部分最需要技巧。你需要为所有想被Python环境访问的函数都增加一个静态的函数,函数的返回值类型为PyObject*,函数名前面要加上模块名和一个下划线(_).

比方说,我们希望在Python中,能够导入(import)我们的fac()函数,其所在的模块名为Extest,那么我们就要创建一个包装函数叫Extest_fac().在使用这个函数的Python脚本中,使用方法是先“import Extest”然后调用“Extest. fac() ”(或者先“from Extest import fac”,然后直接调用“fac()”)

包装函数的用处就是先把Python的值传递给C,然后调用我们想要调用的相关函数。当这个函数完成要返回Python的时候,把函数的计算结果转换成Python的对象,然后返回给Python.

对于fac()函数来说,当客户程序调用Extestfac()的时候,我们的包装函数就会被调用。它接受一个Python的整型参数,把它转为C的整型,然后调用C的fac()函数,得到一个整型的返回值,最后把这个返回值转为Python的整型数作为整个函数调用的结果返回(在你头脑中,要保持一个想法:我们所写的其实就是“deffac(n)”这段声明的一个代理函数,当代理函数返回的时候,就像是这个想像中Python的fac()函数在返回一样).

那么怎样才能完成这样的转换呢?答案是,在从Python到C的转换就用PyArg_Parse*()系列函数;在从C转到Python的时候,就用Py_BuildValue()函数。

PyArg_Parse()系列函数的用法跟C的sscanf()函数很像,都接受一个字符串流,并根据一个指定的格式字符串进行解析,把结果放入到相应的指针所指的变量中去。它们的返回值为1表示解析成功,返回值为0表示失败。

Py_BuildValue()的用法跟sprintf()很像,把所有的参数按格式字符串所指定的格式转换成一个Python的对象。

表22.1罗列了这些函数的概要。

22.2 创建Python扩展 - 图6

表22.2所列出的转换代码用于在C与Python之间做数据的转换。

22.2 创建Python扩展 - 图7

这些转换代码出现在格式字符串当中,用于指定各个值的数据类型,以便于在两种语言之间做转换。注:由于Java的所有数据类型都是类,所以Java的转换类型不一样。Python对象在Java中所对应的数据类型请参考Jython的相关文档。C#也有同样的问题。

下面是完整的Extest_fac()包装函数:

22.2 创建Python扩展 - 图8

首先,我们要解析Python传过来的数据。例子中,我们使用格式字符串“i”,表示我们期望得到一个整型的变量。如果传进来的的确是一个整型的变量,那就把它保存到num变量中。否则, PyArg—ParseTuple()会返回NULL,同时,我们的函数也返回一个NULL。这时,就会产生一个TypeError异常,通知客户我们期望传入一个整型变量。

然后,我们会调用fac()函数,其参数为num,把返回结果放在res变量中。最后,通过调用Py_BuildValue()函数,格式字符串为“i”,把结果转为Python的整型类型并返回。这样,我们就完成了整个调用过程。

事实上,包装函数写得多了之后,你会慢慢地把代码写得越来越短,以减少中间变量的使用,同时也会增加代码的可读性。我们以Extest_fac()函数为例,把它改写得短小一些,只使用一个变量num:

22.2 创建Python扩展 - 图9

那么reverse()怎么实现呢?既然你已经知道怎么返回一个值了,那我们把reverse()的需求稍微改一下,变成返回两个值。我们将返回一个包含两个字符串的元组。第一个值是传进来的字符串,第二个值是反转后的字符串。

我们将把这个函数命名为Extest.doppel(),以示与reverse()函数的区别。把代码包装到Extest_doppel()函数后,我们得到如下代码:

22.2 创建Python扩展 - 图10

跟Extest_fac()类似,我们接收一个字符串型的参数,保存到orig_str中。注意,这次,我们要使用“s”格式字符串。然后调用strdup()函数把这个字符串复制一份(由于我们要同时返回原始字符串和反转后的字符串,所以我们需要复制一份)。把新复制的字符串传给reverse函数,我们就得到了反转后的字符串。

如你所见,我们用“ss”格式字符串让Py_BuildVahie()函数生成了一个含有两个字符串的元组,分别放了原始字符串和反转后的字符串。这样就完成所有的工作了吗?很不幸,还没有。

我们碰到了C语言的一个陷阱:内存泄漏。即内存被申请了,但没有被释放。就像去图书馆借了书,但是没有还一样。无论何时,你都应该释放所有你申请的,不再需要的内存。看,我们写的代码犯了多大的罪过啊(虽然看上去好像很无辜的样子)!

Py_BuildValue()函数生成要返回的Python对象的时候,会把转入的数据复制一份。上例中,那两个字符串就会被复制出来。问题就在于,我们申请了用于存放第二个字符串的内存,但是,在退出的时候没有释放它。于是,这片内存就泄漏了。正确的做法是:先生成要返回的对象,然后释放在包装函数中申请的内存。我们必须要这样修改我们的代码:

22.2 创建Python扩展 - 图11

22.2 创建Python扩展 - 图12

我们用dupe_str变量指向了新申请的字符串,并依此生成了要返回的对象。然后,我们调用free()函数释放这个字符串,最后返回到调用程序,终于完成了我们要做的事情。

为每个模块增加一个形如PyMethodDef ModuleMethods[]的数组。

现在,我们己经完成了两个包装函数。我们需要把它们列在某个地方,以便于Python解释器能够导入并调用它们。这就是ModuleMethods[]数组要做的事情。

这个数组由多个数组组成。其中的每一个数组都包含了一个函数的信息。最后放一个NULL数组表示列表的结束。我们为Extest模块创建一个ExtestMethods[]数组:

22.2 创建Python扩展 - 图13

每一个数组都包含了函数在Python中的名字,相应的包装函数的名字以及一个METH_VARARGS常量。其中,METH_VARARGS常量表示参数以元组形式传入。如果我们要使用PyArg_ParseTupleAndKeywords()函数来分析命名参数的话,我们还需要让这个标志常量与METH KEYWORDS常量进行逻辑与运算常量。最后,用两个NULL来结束我们的函数信息列表。

3.增加模块初始化函数void initModule()

所有工作的最后一部分就是模块的初始化函数。这部分代码在模块导入的时候被解释器调用。在这段代码中,我们需要调用Py_InitModule()函数,并把模块名和ModuleMethods[]数组的名字传递进去,以便解释器能正确地调用我们模块中的函数。对Extest模块来说,initExtest()函数应该是这个样子的:

22.2 创建Python扩展 - 图14

这样,所有的包装都已经完成了。我们把以上代码与之前的Extestl.c合并到一个新文件~Extest2.c中。到此为止,我们的开发阶段就已经结束了。

创建扩展的另一种方法是先写包装代码,使用桩函数、测试函数或哑函数。在开发过程中慢慢地把这些函数用有实际功能的函数替换。这样,你可以确保Python和C之间的接口函数是正确的,并用它们来测试你的C代码。

22.2.3 编译

现在,我们己经到了编译阶段。为了让你的新Python扩展能被创建,你需要把它们与Python库放在一起编译。现在已经有了一套跨30多个平台的规范,它极大地方便了编写扩展的人。distutils包被用来编译、安装和分发这些模块、扩展和包。这个模块在Python2.0的时候就已经出现了,并用于代替1.x版本时的用Makefile来编译扩展的方法。使用distutils包的时候我们可以方便地按以下步骤来做:

1.创建setup.py;

2.通过运行setup.py来编译和连接您的代码;

3.从Python中导入您的模块;

4.测试功能。

1.创建 setup.py

下一步就是要创建一个setup.py文件。编译最主要的工作由setup()函数来完成。在这个函数调用之前的所有代码,都是一些预备动作。为了能编译扩展,你要为每一个扩展创建一个Extension实例,在这里,我们只有一个扩展,所以只要创建一个Extension实例:

22.2 创建Python扩展 - 图15

第一个参数是(完整的)扩展的名字,如果模块是包的一部分的话,还要加上用“.”分隔的完整的包的名字。我们这里的扩展是独立的,所以名字只要写“Extest“就好了。sources参数是所有源代码的文件列表。同样,我们也只有一个文件Extest2.c。

现在,我们可以调用setup()了。Setup()需要两个参数: 一个名字参数表示要编译哪个东西,一个列表列出要编译的对象。由于我们要编译的是一个扩展,我们把ext_modules参数的值设为扩展模块的列表。语法如下:

22.2 创建Python扩展 - 图16

例22.2

这个脚本会把我们的扩展编译到build/lib.*子目录中。

22.2 创建Python扩展 - 图17

由于我们只有一个模块,我们把我们扩展模块对象的实例化操作放到了setup()的调用代码中。模块的名字我们就传预先定义的“常量” MOD:

22.2 创建Python扩展 - 图18

setup()函数还有很多选项可以设置。限于篇幅,不能完全罗列。读者可以在本章最后所列的官方文档中找到setup.py和setup()函数相关的信息。例22.2给出了我们例子所要用的完整的脚本代码。

2.通过运行setup.py来编译和连接代码

现在,我们已经有了setup.py文件。运行setup.py build命令就可以开始编译我们的扩展了。在我们的Mac机上的输出如下(使用不同版本的Python或是不一样的操作系统时,输出会有一些不同):

22.2 创建Python扩展 - 图19

22.2 创建Python扩展 - 图20

22.2.4 导入和测试

1.从Python中导入您的模块

你的扩展会被创建在你运行setup.py脚本所在目录下的build/lib.*目录中。你可以切换到那个目录中来测试你的模块,或者也可以用以下命令把它安装到你的Python中。

22.2 创建Python扩展 - 图21

如果安装成功,你会看到:

22.2 创建Python扩展 - 图22

现在,我们可以在解释器里测试我们的模块了:

22.2 创建Python扩展 - 图23

2.测试功能

我们想要做的最后一件事就是加上一个测试函数。事实上,我们已经写好一个了,就是main()函数。现在,在我们代码中放一个main()函数是一件比较危险的事,因为一个系统中只能有一个main()函数。我们把main()函数改名为test(),加个Extest_test()函数把它包装起来,然后在ExtestMethods中加入这个函数就不会有这样的问题了。代码如下:

22.2 创建Python扩展 - 图24

22.2 创建Python扩展 - 图25

Extest_test()模块函数只负责运行test()函数,并返回一个空字符串。Python的None作为返回值,传给了调用者。现在,我们可以在Python中调用同样的test()函数了:

22.2 创建Python扩展 - 图26

在例22.3中,我们给出了Extest2.c的最终版本。这个版本会输出我们刚才所看到的结果。

在本例中,我们把我们的C代码和Python相关的代码分开放,一段在上面,一段在下面。

这样可以让代码更具可读性。对于小程序来说,没有任何问题。但在实际应用中,源代码会越写越大。一部分人就会考虑把他们的包装函数放在另一个源文件中。起个诸如ExtestWrappers.c之类好记的名字。

22.2.5 引用计数

也许你还记得,Python使用引用计数作为跟踪一个对象是否不再被使用,所占内存是否应该被回收的手段。它是垃圾回收机制的一部分。当创建扩展时,你必需对如何操作Python对象格外小心。你时时刻刻都要注意是否要改变某个对象的引用计数。

一个对象可能有两类引用。一种是拥有引用,你要对这个对象的引用计数加1,以表示你也拥有这个对象的所有权。如果这个Python对象是你自己创建的,那么这时你肯定拥有这个对象的所有权。

当你不再需要一个Python对象时,你必须要交出你的所有权,要么把引用计数减1,要么把所有权交给别人,要么就把这个对象存到其他的容器中(元组、列表等)。没有交出所有权就会导致内存泄漏。

你也可以拥有对象的借引用。相对来说,这种方式的责任就小一些。除非是别人在外面把对象传递给你。否则,不要用任何方式修改对象里的数据。你也不用时刻考虑对象引用计数的问题,只要你不会在对象的引用计数减为0之后再去使用这个对象。你也可以把借引用对象用的数量加1,从而真正地引用这个对象。

例22.3 C库的Python包装版本(Extest2. c)

22.2 创建Python扩展 - 图27

22.2 创建Python扩展 - 图28

22.2 创建Python扩展 - 图29

Python提供了一对C的宏,可以用来改变Python对象的引用计数,见表22.3.

22.2 创建Python扩展 - 图30

在上面的Extest_test()函数中,我们创建了一个空字符串的PyObject对象,用以返回None。或者,你也可以对空对象(PyNone)的引用计数加1,成为PyNone的拥有者,然后直接返回PyNone,见下例。

22.2 创建Python扩展 - 图31

Py_INCREF()和Py_DECREF()两个函数也有一个先检查对象是否为空的版本,分别为Py_XINCREF()和Py_XDECREF0。

我们强烈建议读者阅读Python文档的扩展和嵌入Python部分中的关于引用计数的内容(见附录中的文档参考部分)。

22.2.6 线程和全局解释器锁(GIL)

编译扩展的人必须要注意,他们的代码有可能会被运行在一个多线程的Python环境中。早在18.3.1节,我们就介绍了Python虚拟机(PVM)和全局解释器锁(GIL),并描述了在PVM中,任何情况下同时只会有一个线程被运行,其他线程会被GIL停下来。而且,我们指出调用扩展代码等外部函数时,代码会被GIL锁住,直到函数返回为止。

前面我们也提到过一种折衷方案,可以让编写扩展的程序员释放GIL,例如在系统调用前就可以做到。这是通过将你的代码和线程隔离实现的,这些线程使用了另外两个C宏PyBEGIN_ALLOW THREADS和Py_END_ALLOW_THREADS,保证了运行和非运行时的安全性。由这些宏包裹的代码将会允许其他线程的运行。

同引用计数宏一样,我们强烈建议阅读关于扩展和嵌入Python的文档和Python/C API参考手册。