使用C语言来扩展Python程序和Zope服务器的教程
有几个原因使您可能想用C扩展Zope。最可能的是您有一个已能帮您做些事的现成的C库,但是您对把它转换成Python却不感兴趣。此外,由于Python是解释性语言,所以任何被大量调用的Python代码都将降低您的速度。因此,即使您已经用Python写了一些扩展,您仍然要考虑把其中最常被调用的部分改用C来写。不论哪种方式,扩展Zope都是从扩展Python开始。此外,扩展Python会给您带来其它的好处,因为您的代码将可以从任何Python脚本访问,而不只是从Zope。这里唯一要提醒的是在写本文的时候,Python的当前版本是2.1,但是Zope仍然只能和Python1.5.2一起运行。对C扩展来说,两个版本并没有什么变化,但如果您有兴趣对您的库进行Python包装,又想让它们都能在Zope下工作,您就得注意不要使用任何比1.5.2更新的东西。
Zope是什么?
Zope代表“ZObjectPublishingEnvironment(Z对象发布环境)”,它是用Python实现的应用程序服务器。“太棒了,”您说,“但应用程序服务器的确切含义是什么呢?”应用程序服务器就是一个长期运行的进程,它为“活动的内容”提供服务。Web服务器在运行期间调用应用程序服务器来构建页面。
扩展Python:有趣又有益
想扩展Zope,您首先要扩展Python。虽然扩展Python不像“脑外科手术”那样复杂,但也不像“在公园中散步”那样悠闲。有两个基本组件用于Python扩展。第一个显然是C代码。我将马上探讨它。另一个部分是安装文件。安装文件通过提供模块名称、模块的C代码的位置和您可能需要的所有编译器标志来描述模块。该文件被预处理,以创建makefile(在UNIX上)或MSVC++工程文件(MSVC++projectfile,在Windows上)。先说一下―Windows上的Python事实上是用Microsoft编译器编译的。Python.org的人也推荐用MSVC++编译扩展。显然,您应该能够成功说服GNU的编译者们,但我本人还没试过。
无论如何,还是让我们来定义一个叫做‘foo'的模块吧。‘foo'模块会有一个叫做‘bar'的函数。当我们要使用时,我们可以用importfoo;来把这个函数导入到Python脚本中,就跟导入任何模块一样。安装文件非常简单:
清单1.一个典型的安装文件
#Youcanincludecommentlines.The*shared*directiveindicates #thatthefollowingmodule(s)aretobecompiledandlinkedfor #dynamicloadingasopposedtostatic:.soonUnix,.dllonWindows. *shared* #Thenyoucanusethevariableslaterusingthe$(variable)syntax #that'make'uses.Thisnextlinedefinesourmoduleandtells #Pythonwhereitssourcecodeis. foofoomain.c
编写代码
那么我们实际上该怎样写Python知道如何使用的代码呢,您问?foomain.c(当然,您可以随意命名它)文件包含三项内容:一个方法表,一个初始化函数和其余的代码。方法表简单地将函数名与函数联系起来,并告知Python各个函数所使用的参数传递机制(您可以选择使用一般的位置参数列表或位置参数和关键词参数的混合列表)。Python在模块装入时调用初始化函数。初始化函数将完成模块所要求的所有初始化操作,但更重要的是,它还把一个指向方法表的指针传回给Python。
那我们就来看看我们的小型foo模块的C代码。
清单2.一个典型的Python扩展模块
#include<Python.h> /*Definethemethodtable.*/ staticPyObject*foo_bar(PyObject*self,PyObject*args); staticPyMethodDefFooMethods[]={ {"bar",foo_bar,METH_VARARGS}, {NULL,NULL} }; /*Here'stheinitializationfunction.Wedon'tneedtodoanything forourownneeds,butPythonneedsthatmethodtable.*/ voidinitfoo() { (void)Py_InitModule("foo",FooMethods); } /*Finally,let'sdosomething...involved...asanexamplefunction.*/ staticPyObject*foo_bar(PyObject*self,PyObject*args) { char*string; intlen; if(!PyArg_ParseTuple(args,"s",&string)) returnNULL; len=strlen(string); returnPy_BuildValue("i",len); }
深入研究
我们来看会儿这些代码。首先,请注意您必须包含Python.h。除非您已在包含路径(includepath)中设置了该文件的路径,否则您可能需要在安装文件中包含-I标志以指向该文件。
初始化函数必须命名为init<模块名>,在我们的例子中是initfoo。初始化函数的名称,毫无疑问,是Python在装入模块时所知道的关于模块的全部信息,这也是初始化函数的名称如此死板的原因。顺便说一下,初始化函数必须是文件中唯一未被声明为static的全局标识符。这对静态链接比对动态链接更重要,因为非static标识符将是全局可见的。对动态链接来说,这不是一个很大的问题,但如果您打算在编译期间链接所有东西,又没有把所有可以声明为static的东西声明为static,那么您很可能就会碰到名称冲突的问题。
现在我们来观察实际的代码,看看参数是怎样被处理的,返回值又是怎样被传递的。当然,一切都是PyObject―Python堆上的对象。您从参数中得到的是一个对“this”对象的引用(this用于对象方法,对类似bar()这样的无参数的老式函数来说是NULL)和一个存储在args中的参数元组。您用PyArg_ParseTuple找回您的参数,然后用Py_BuildValue把结果传回去。这些函数(还有更多)都归档在Python文档的“Python/CAPI”部分中。不幸的是,没有按名称排列的简单的函数清单,文档是按主题排列的。
另请注意,函数在出错的情况下返回NULL。返回NULL表示出错了;如果想让Python做得更好,您应该抛出异常。我会指点您去查阅关于如何做这件事的文档。
编译扩展
现在剩下的全部问题是编译模块。您可以通过两种方式进行。第一种是按照文档中的指导,运行make-fMakefile.pre.inboot,这样将会使用您的Setup来编译一个Makefile。然后您就用该Makefile编译您的工程。这种方式只适用于UNIX。对Windows来说,存在一个叫“compile.py”的脚本(请参阅本文后面的参考资料)。原始脚本很难找到;我从一个邮件列表中找到了一个来自RobinDunn(wxPython的幕后工作者)的被大量改动了的副本。这个脚本能在UNIX和Windows上工作;在Windows上,它将从您的Setup开始编译MSVC++工程文件。
要进行编译,您必须使包含的文件和库都可用。Python的标准Zope安装没有包含这些文件,因此您需要从www.python.org(请参阅参考资料)安装Python的常规安装。在Windows上,您还必须从源代码安装的PC目录中获取config.h文件;它是UNIX安装为您编译的config.h的手工版。因此,在UNIX上,您应该已经拥有它了。
一旦这些都完成后,您就会得到一个以“.pyd”为扩展名的文件。把这个文件放到Python安装目录下的“lib”目录(在Zope下,Python位于“bin”目录,因此您的扩展得结束于“bin/lib”目录,奇怪吧。)然后您就可以调用它了,就像调用任何源生的Python模块一样。
>>>importfoo; >>>foo.bar("Thisisatest"); 14
做到这里时,我的第一个问题是问自己该如何用C定义从Python中可见的类。事实上,我可能问了一个错误的问题。在我已研究的示例中,特定于Python的一切都只用Python来完成,也都只调用从您的扩展中导出的C函数。
把它带到Zope中去
一旦完成了您的Python扩展,下一步就是使Zope能和它一起工作。您有几种方式可以选择,但在一定程度上,您希望您的扩展以什么方式与Zope一起工作将首先影响到您编译扩展的方式。从Zope内使用Python(以及用C所做的扩展)代码的基本方式是:
- 如果函数很简单,您可以把它当作一个变量。这些被叫做“外部方法”。
- 更复杂的类,可以从Zope脚本中调用(这是Zope2.3的一个新功能)。
- 您可以定义一个ZopeProduct,然后可以用ZClass(一组已做好的、Web可访问的对象)扩展它,在脚本中使用它,根据它的自有权限发布它(它的实例被当作页来对待)。
当然,您自己的应用程序可以使用这些方式的组合。
创建外部方法
从Zope调用Python的最简单的方式是把您的Python代码做成外部方法。外部方法是被放到Zope安装目录下的“Extensions”目录中的Python函数。一旦那里有了这样一个Python文件,您就可以转到任意文件夹,选择“添加外部方法”,并添加调用要使用的函数的变量。然后您就可以往该文件夹中显示调用结果的任意页添加DTML字段。我们来看一个使用了上面所定义的Python扩展―foo.bar―的简单示例。
首先,来看扩展本身:我们把它放到一个例如叫foo.pyd的文件中。记住,这个文件位于Zope下的Extensions目录。为了能够顺利进行,当然,我们在上面创建的foo.pyd必须在位于bin/lib的Python库中。一个出于这个目的的、简单的包看起来可能像这样:
清单3.一个简单的外部方法(文件:Extensions/foo.py)
importfoo defbar(self,arg): """Asimpleexternalmethod.""" return'Arglength:%d'%foo.bar(arg)
很简单,不是吗?它定义了一个可以用Zope管理界面附加到任意文件夹的外部方法“bar”。要从该文件夹中的任何页中调用我们的扩展,我们只需简单地插入一个DTML变量引用,如下所示:
<dtml-varbar('Thisisatest')>
当用户查看我们的页时,DTML字段将被文本“Arglength:14”代替。我们就这样用C扩展了Zope。
Zope脚本:CliffNotes版
Zope脚本是Python2.3的一个想用来代替外部方法的新功能。外部方法能做到的,它都能做到,而且它能和安全性及管理系统更好地集成,在集成方面提供更多的灵活性,它还有很多对ZopeAPI中公开的全部Zope功能的访问。
一个脚本基本上就是一个短小的Python程序。它可以定义类或函数,但不是必须的。它被作为对象安装在Zope文件夹中,然后就可以把它当作DTML变量或调用(就像一个外部方法)来调用或者“从Web中”(在Zope中的意思就是它将被当作页来调用)调用它。当然,这意味着脚本可以像CGI程序那样生成对表单提交的响应,但却没有CGI的开销。确实是一个很棒的功能。此外,脚本有权访问被调用者或调用者对象(通过“context”对象)、对象所在的文件夹(通过“container”对象)和其他一些零碎信息。要获得更多关于脚本的知识,请参阅Zope手册(请参阅参考资料)中的“高级Zope脚本编制(AdvancedZopeScripting)”那一章。
您可能会错误地认为可以直接从脚本简单地导入foo并使用foo.bar(我知道我确实犯过这种错误)。但事实并非如此。由于安全性限制,只有Product可以被导入,而不是什么模块都可以。一般而言,Zope的设计者们认为任何脚本编制都需要访问文件系统,既然脚本对象是由Web使用Zope管理界面来管理,所以它们不是完全可信的。所以我打算就此打住,不给您展示示例脚本了,而是来讨论Product和基础类。
专注于Product
Product是扩展Zope的强大工具方法。从安装目录的级别来看,Product就是位于Zope目录下的“lib/python/Products”目录中的一个目录。在您自己的Zope安装目录中,您可以看到很多product示例,但本质上,最小的Product只由位于该目录的两个文件组成:一个可任意命名的代码文件和一个Zope在启动时调用来初始化Product的称为__init__.py的文件。(请注意:Zope只在启动时读取Product文件,这意味着为了测试,您必须能够停止和重新启动Zope进程)。本文只是尽量多提供一些您能通过使用ZopeProduct做到的事的提示。
要知道的是Product封装了一个或多个可从ZClass、脚本或直接从Web上的URL使用的类。(当然,在最后一种情况下,Product的实例被当作文件夹看待;那么URL的最后部分指定了将被调用的方法,该方法返回任意的HTML。)您不必一定要把Product当作“可添加的”对象来对待,虽然这是它的主要目的。要看一个优秀的、现实存在的示例,可以去看ZCatalog实现,它是标准Zope分发的一部分。那里您可以在__init__.py中看到一个非常简单的安装脚本,可以在ZCatalog.py中看到ZCatalog类,该类提供了很多发布方法。请注意Zope采用一种奇怪的约定来确定哪些方法可以通过Web访问―如果一个方法包含有一个doc字符串,那么该方法可通过Web访问;否则,就被认为是私有的。
无论如何,我们还是来看一个使用了C模块(我们在上面定义了它)的非常简单的Product。首先来看非常简单的__init__.py;请注意它只做了一件事,即告诉Zope我们正在安装的类的名称。更复杂的初始化脚本能做更多的事,包括声明由服务器维护的全局变量以及设置访问权限等等。欲了解更多详细信息,请参阅在线文档中的Zope开发者指南,也请研究您的Zope安装目录中现成的Product。您或许已经猜到了,我们的示例Product被称为“Foo”。这样您就将在lib/python/Products目录下创建一个Foo子目录。
清单4.基本的Product初始化脚本
importFoo definitialize(context): context.registerClass( Foo.Foo, permission='AddFoo', constructors=Foo.manage_addFoo )
现在请注意这个初始化脚本不仅导入了那个类,使它可被Zope的其它部件访问,而且还将该类注册成具有“可添加性”。context.registerClass调用通过首先命名我们所导入的类,然后指定可被用于添加实例的方法名称(这个方法必须显示一个管理页面,且该方法将自动与Zope管理界面集成)的名称来完成这项工作。酷。
我们来小结一下这个短小、简单的Product。它会把我们的foo.bar函数公开给脚本和ZClass,并且还有一个作为“可添加的”对象的小接口,这就是全部内容。
清单5.一个简单的ZopeProduct
importfoo classFoo(SimpleItem.Item): "AFooProduct" meta_type='foo' defbar(self,string): returnfoo.bar(string) def__init__(self,id): "Initializeaninstance" self.id=id defindex_html(self): "Basicviewofobject" return' Myidis%sanditslengthis%d. '%(self.id,foo.bar(self.id)) defmanage_addFoo(self,RESPONSE): "Managementhandlertoaddaninstancetoafolder." self._setObject('Foo_id',Foo('Foo_id')) RESPONSE.redirect('index_html')
这只是一个最简单的Product。不能绝对地说它是可能的Product中最小的一个,但已经很接近了。不过,它确实说明了Product的一些关键特征。首先,请注意“index_html”方法:它被调用来显示一个对象实例,这是通过构建HTML完成的。它实际上是一个页面。manage_addFoo方法是Zope对象管理的接口;我们在上面的__init__.py中引用了它。“__init__”方法初始化对象;实际上它必须做的全部工作就是记录实例的唯一标识符。
这个微型的Product不和Zope安全性进行交互操作。它不做很多管理工作。它没有交互功能。所以您可以给它添加很多东西(甚至连很有用的功能它也没有)。我希望这对您是一个很好的开始。
以后该做什么
对ZopeProduct的简单介绍已经告诉您如何把C语言函数从C代码变为Zope中可用的。要学会怎么写Product,您还得阅读更多文档(其中有很多仍在完善之中),坦率地说,还要研究已有的Product,看看它们是怎么做的。Zope模型有很强大的功能和很大的灵活性,它们都很值得探究。
我目前正在做集成C和Zope的大工程:集成我的工作流工具包(workflowtoolkit)。在本文发表之前,我希望能看到它的雏形。它已被列在下面的参考资料中,去看看吧;到您阅读本文时,应该已经能够从中找到一个扩展示例。祝我好运。