C++ API 设计 16 第十一章 脚本

第十一章 脚本

到本章为止,我主要关注的是API设计方面的通用内容,可以应用到所有的C++项目上。在涵盖了标准的API设计流程后,本书的剩余章节更多的是处理脚本和扩展性这些特别的主题。虽然不是所有的API都需要关心这些主题,但是它们正成为现代程序开发中更受欢迎的主题。因此,我认为对于一本全面介绍C++ API设计的书应该包括这些高级的主题。

相应地,本章处理的主题是脚本。也就是说,允许C++ API从脚本语言来访问,如:Python、 Ruby、Lua、Tcl或 Perl。我会解释为什么你需要这么做和你需要知道的一些问题,接着让你看看为这些语言创建绑定所需要的一些主流技术。

为了让本章更具实用性和启发性,我会详细介绍两种不同的脚本绑定技术,还会展示如何为两种不同的脚本语言创建绑定。特别地,我将提供如何利用Boost Python为C++ API创建Python绑定的详细内容,接着全面分析如何使用简化包装和接口生成器(Simplified Wrapper and Interface Generator,SWIG)来创建Ruby绑定。我之所以关注Python和Ruby,是因为这两个是当今正在使用的最受欢迎的脚本语言。就绑定技术而言,Boost Python和SWIG都是免费可用的开源解决方案,对绑定提供了广泛的控制。

11.1 添加脚本绑定

11.1.1 扩展对比嵌入

脚本绑定提供了一种从脚本语言访问C++ API的方式。这通常包括为C++类和函数创建包装代码,允许它们被导入到脚本语言,可以使用它的原生模块装载功能。例如,在Python中的import关键字;Ruby中的require或Perl中的use。

用脚本语言集成C++主要有两种策略

(1).扩展语言:在这种模型中,脚本绑定用来做为一种为脚本语言提供功能的模块。也就是:使用脚本语言编写代码的用户可以在他们自己的脚本中利用你的模块。你的模块看起来就像那个语言中的任何其它模块一样。例如,Python标准库中的expat和md5模块是在C中实现的,而不是在Python中实现

(2).嵌入到程序中:在这种情况下,最终用户的C++程序把一个脚本语言嵌入到它里面去。脚本绑定这时用来让最终用户为特定的程序编写脚本,调用程序的核心功能。举个Autodesk Maya 3D模块系统的例子,它有提供Python和Maya 嵌入语言(Maya Embedded Language,MEL)脚本;而Adobe Director多媒体创作平台嵌入的是Lingo脚本语言。

无论那种策略适合你的情况,定义和构建脚本绑定的过程都是一样的。唯一的不同就是谁拥有C++的main()函数。

11.1.2 脚本的优点

从脚本语言里访问API的原生代码提供了很多好处。这些好处既可以是直接对你自己的,如果你为你的C++ API提供了一个对脚本绑定的支持;或者是对你的用户,他们可以在你的纯C++ API上创建他们自己的脚本绑定。这里我将罗列出这些好处的一部分:

q跨平台:脚本语言是解释型的,这表示它们执行的是纯ASCII的源代码或者是平台无关的字节码。它们通常也会为特定平台的特性(如文件系统)提供它们自己的模块接口。因此,用脚本语言编写的代码可以在不修改的情况下运行在多个平台上。对于私有项目这也会被认为是一个缺点,因为脚本语言通常是采用源码形式发布

q快速开发:如果你要修改一个C++程序,那么你不得不重新编译和链接你的代码。对于大型的系统,这会是相当耗时的操作并影响工程师的生产率,因为他们得等待才能测试他们的程序。在脚本语言中,你只要编辑完源代码,接着就可以运行了,无需编译和链接操作。这允许你进行快速原型设计和测试新的修改,这常常可以大大提高工程师的效率和项目的速度。

q编写更少的代码:对于给定问题,对比C++,采用高级的脚本语言通常只要更少的代码就可以解决。脚本语言不需要显式的内存管理,比起C++的STL,它们有更大的可用标准库,而且它们常常在幕后处理复杂的概念,如引用计数。例如,下面的Ruby单行代码是让一个字符串返回其中所有唯一字母按照字母表顺序排序后的列表,如果这在C++中实现将会多出很多代码:

[代码 P330 第一段]

q基于脚本的程序:脚本语言的传统观点认为是用来解决小的命令行任务的,但是你必须用像C++那样最高效的语言来编写一个最终用户程序。然而,有种观点是你用C++编写对性能要求很高的核心部分,再为它们创建脚本绑定,接着用脚本语言来编写程序。按照MVC的说法,模型和视图都是用C++编写的,而控制器是通过脚本语言实现的。其中的关键点是你并不需要一个快速的编译语言来管理用户的输入,因为用户的输入频率是比较慢的。

在皮克斯的时候,我们实际上[l1]用这种方式重写了我们的内部动画工具集:用基于Python的主程序调用C++编写的高效模型和视图的扩展集。这享受到了这里列出的所有好处,如移除了很多程序逻辑修改要进行的编译-链接阶段,而且仍然我们的设计师提供了一个交互3D动画系统。

q为专家用户提供支持:往最终用户程序中添加脚本语言可以允许高级用户定制程序的功能,他们通过编写宏来执行重复的任务或GUI中没有暴露的任务。这可以做到不对新手用户牺牲软件的可用性,他们只会通过GUI来和程序进行交互。

q扩展性:除了让专家用户可以访问底层的功能,脚本接口可以用来让他们通过插件接口为程序添加全新的功能。这意味着程序的开发者不再负责解决每个用户的问题。用户将可以自行解决他们的问题。例如,火狐的浏览器允许使用JavaScript这个嵌入脚本语言来创建新的扩展。

q为易测性编写脚本:使用脚本语言编写代码有一个非常有价值的好处,就是你可以用那个语言来编写自动化测试。这个好处能够让你的QA团队也可以编写自动化测试,而不仅仅只能依赖黑盒测试。QA工程通常不会编写C++代码。然而,有很多有经验的白盒QA工程师会编写脚本语言代码。让QA团队参与编写自动化测试可以让测试范围覆盖得更广,让他们可以为更底层操作做出一定的贡献。

q富有表现力:语言学领域定义了语言相对论的原则(又称Sapir–Whorf假说),人们的语言会影响人们的思想和行为。当把这个概念应用到计算机科学领域,这意味着编程语言的表现力、灵活度和易用性会影响你所预想的各种解决方案。那是因为你无需为底层的问题分心,如内存管理或静态类型数据表现。这明显比先前的技术论据更具有主观性,但是它却毫不逊色。

11.1.3 语言兼容性问题

当在脚本语言中暴露一个C++ API时有个需要知道的重要问题:C++中的模式和用法不能直接映射到脚本语言中。因此,C++ API直接翻译成脚本语言所生成的代码看起来会不自然或不像那个原生的脚本语言。例如:

q命名约定:C++函数常常使用大写或小写的驼峰格式。也就是:GetName() 或getName()。而Python中约定(定义在PEP 8中)的方法名是使用蛇形格式。例如,get_name()。相似地,Ruby中的方法命名也应该使用蛇形格式。

qGetters/setters:在本书中,我主张你应该不在类中直接暴露数据成员。你应该总是提供getter/setter方法来访问那些成员。然而,很多脚本语言允许你使用这个语法访问成员变量的同时强制那个访问要通过getter/setter方法。实际上,在Ruby中:这是你从类外部访问成员变量的唯一方式。这种代替C++风格的代码

[代码 P332 第一段]

你可以这样更为简单地编写,这仍然是利用底层的getter/setter方法:

[代码 P332 第二段]

q迭代:绝大部分脚本语言支持迭代的基本概念,可以定位序列中的元素。然而,这个概念的实现通常和STL中的实现是不一致的。例如,C++有五种迭代类别(向前的、双向的、随机访问、输入和输出),而Python只有一种迭代类别(向前的)。让一个C++对象在脚本语言中也是可迭代的,就需要特别注意,以使其适应该语言的语义,如对于Python,添加一个__iter__()方法。

q运算符:你已经知道C++支持几种运算符,如:operator+、operator+=和operator[ ]。这些常常可以直接翻译成脚本语言中同等的语法,如C++的流运算符“<<”对应Ruby中的to_s()方法(返回对象的字符串表示)。然而,目标语言可能支持额外的C++中不支持的运算符,如Ruby的平凡运算符(**)和返回除法中的商和余数(div mod)。

q容器:STL中提供的容器类,如:std::vector、std::set和std::map。这些是静态类型类模板,只能接受相同类型的对象。比较一下,很多脚本语言都支持动态类型,支持容器包含不同类型的元素。在脚本语言中使用这些灵活的类型来传送数据是很常见的。例如,C++方法中更适合接收几个非常量引用参数,这和脚本语言中的表现形式不同,请看下面的例子:

[代码 P332 第三段]

所有的这些意味着要编写好的脚本绑定就需要进行一定的手动调整。完全采用自动化技术创建绑定的API会在脚本语言中觉得不自然。例如,PyObjC程序Python中为Objective-C对象提供一座桥梁[l2],但是可能会导致Python中繁琐的结构,如把方法命名成setValue_()。相比之下,通过你手动精心编写的脚本中对外暴露的函数可以为你生成更高质量的结果。

11.1.4 跨越语言屏障

语言屏障是指C++满足脚本语言的边界。为对象进行脚本绑定时要小心脚本语言中到相关的C++代码的前向方法调用。然而, 默认情况下通常不会有C++代码调用脚本语言。这是因为C++ API并非特别设计成某个脚本语言进行交互,它并不知道脚本的运行环境。

例如,有一个带有虚方法的C++类在Python中重写。而C++代码并不知道Python已经重写了它的虚方法。这是有帮助的,因为C++虚表是在编译时静态创建的,无法适应Python中在运行时动态添加方法。有些绑定技术提供了额外的功能来实现这种跨语言多态。我会在本章稍后的Boost Python和SWIG讨论如何实现这个。

另一个需要知道的问题是C++代码是否使用了内部事件或通知系统。如果有的话,需要添加一些额外的机制到所有的C++触发事件以跨越语言边界到脚本代码内部。例如,Qt和Boost提供了一种信号和槽(signal/slot)系统,当另一个C++对象修改状态时,C++代码可以注册并接收通知。然而,允许脚本接收这些事件就需要你编写显式代码来拦截C++事件并让事件跨越边界,送达给脚本对象。

最后,C++代码中的异常也需要和脚本代码进行通信。例如,未捕获的C++异常可能被语言屏障所捕获,接着被翻译成脚本语言的异常类型。

11.2 绑定脚本技术

有多种技术可以用来生成脚本语言调用C++代码的包装。[l3]每种都有各自的优缺点。一些是支持语言中立(language- neutral)技术,也就是支持很多脚本语言(如COM或CORBA),一些是C/C++所特有的,但是也支持为很多语言(如SWIG)创建绑定,一些只为单个语言(如Boost Python)提供C++绑定,还有其它的为特定的API提供C++绑定(如Pivy Python是为Open Inventor C++工具包提供绑定)。

这里我会罗列出几种这些技术,接着本章的剩余部分我会关注其中两种的更多细节。我选择关注的是可移植的C++特定解决方案,而不是更加通用和重量级进程间通信模型,如COM或CORBA。为了更具实用价值,我会讲述一个绑定技术,让你通过编程(Boost Python)来定义脚本绑定,还有一个使用接口定义文件为绑定生成代码(SWIG)。

所有的脚本绑定技术从本质上都是基于适配器设计模式创建的。也就是说,它提供了一个API到另一个API的一对一映射,同时会把数据类型翻译成它们最适合的基本形式,或许还使用更符合习惯的命名约定。认清这个事实意味着你应该知道API包装设计模式(如代理和适配器模式)的基本问题。最需要关注的是要随着时间推移保持这两个API的同步。你将会看到,Boost Python和SWIG都需要在改进C++ API时保持冗余文件的同步,如Boost中的额外C++文件和SWIG中的单独接口。当支持一个脚本API时,这常常成为最大的维护开销。

11.2.1 Boost Python

Boost Python(也写作boost::python或Boost.Python)是一个C++库,允许C++ API与Python进行互操作。它是优秀的Boost库的一部分,你可以在http://www.boost.org/找到它。通过Boost Python,你可以在C++代码中编程创建绑定,接着可以链接绑定到Python和Boost Python库。这种生成的动态库就可以直接导入到Python中。

Boost Python支持下面的包装C++ API的功能和特性:

qC++引用和指针

q把C++异常转换成Python

qC++的默认参数和Python关键字参数

q在C++中操作Python对象

q把C++的迭代器导成Python的迭代器

qPython文档字符串

q全局注册类型强制(Globally Registered Type Coercions

11.2.2 SWIG

SWIG是个帮助使用C或者C++编写的软件与其它各种高级编程语言创建绑定的开发工具。支持的脚本语言包括Perl、PHP、Python、Tcl和Ruby。还包括非脚本语言,如:C#、Common Lisp、Java、Lua、Modula-3、OCAML、Octave和R。

SWIG的核心设计概念就是接口文件,通常使用的是.i文件扩展名。这个文件用来为使用C/C++语法来定义一个给定的通用模块的绑定。下面是SWIG接口文件的一般格式:

[代码 P334 第一段]

SWIG程序可以读取这个接口文件并为特定的语言生成绑定。接着,这些绑定就可以编译进一个共享库中,这样就可以被脚本语言所加载。要查看更多关于SWIG的信息,请查阅http://www.swig.org/

11.2.3 Python-SIP

SIP这个工具可以用来为Python创建C和C++绑定。它原先是为PyQt包所创建的,是为诺基亚的Qt工具包提供Python绑定。因此,Python-SIP支持Qt的信号/槽机制。然而,这个工具也可以用来为所有的C++ API创建绑定。

SIP的工作方式很像SWIG,不过它所支持的语言没有SWIG那么多。SIP的接口规范文件支持大部分C/C++语法,它的命令使用和SWIG(如:标记使用%符号打头)相似的语法,不过它支持不同的命令风格和集合来定制绑定。这里有一个简单的Python-SIP接口规范的例子。

[代码 P335 第一段]

11.2.4 COM自动化

组件对象模型(Component Object Model,COM)是一个二进制接口标准,允许对象之间通过进程间通信的方式来交互。COM指定了定义良好的接口,允许软件组件重用和链接到一起,以构建最终用户程序。这个技术是由微软在1993发明的,直到今天仍然在使用,主要是应用在Windows平台,不过现在微软鼓励使用.NET和SOAP。

COM包含了大量的技术,但是这里我们关注的是COM自动化,也叫做OLE自动化或简称自动化。这包括自动化对象(也叫做ActiveX对象)可以被脚本语言所访问并反复执行任务或从脚本控制应用程序。这支持很多种语言,如:VisualBasic、Jscript、Perl、Python、Ruby和微软的.NET语言。

一个COM对象由通用唯一识别码(Universally Unique ID,UUID,也写作UUIDs)标识,通过接口暴露它的功能。所有的COM对象都支持IUnknown接口方法:AddRef()、Release()和QueryInterface()。COM自动化对象同时实现了IDispatch接口,包括Invoke()方法触发对象内的命名函数。

为接口暴露的对象模型使用接口描述语言(interface description language,IDL)进行描述。IDL是一种软件组件接口的语言中立的描述,通常存在扩展名为.idl的文件中。IDL描述在Windows中可以使用MIDL.EXE编译器转换成各种形式。生成的文件包括COM对象的代理DLL代码和一个描述对象模型的类型库。下面的例子显示的是微软的IDL语法:

[代码 P336 第一段]

还有一个框架叫做跨平台组件对象模型(Cross-Platform Component Object Model,XPCOM)。这是由Mozilla开发的开源项目,他们的很多程序都有使用,包括火狐浏览器。XPCOM遵循COM类似的设计,不过它们的组件兼容或可互换的。

11.2.5 CORBA

通用对象请求代理体系结构(Common Object Request Broker Architecture,CORBA)是一个工业标准,允许软件组件之间不受地点和制造商的影响,能够相互通信。在这点上,这和COM是很相似的:两种技术都是用来解决不同来源的对象间的通信,也都是利用语言中立的IDL格式来描述每个对象的接口。

CORBA是跨平台的,通过几种开源实现并对UNIX平台提供了强有力的支持。它是由对象管理组织(Object Management Group,和管理UML模型语言是同一个组织)在1991年定义的。CORBA提供很多语言绑定,包括:Python、Perl、Ruby、Smalltalk、JavaScript、Tcl和CORBA脚本语言(IDLscript)。它也支持多继承接口,而COM只支持单继承。

就脚本而言,CORBA并不像COM那样需要一个特定的自动化接口。默认情况下,所有的CORBA对象都是通过动态调用接口(Dynamic Invocation Interface)脚本化的,这是让脚本语言可以动态地决定对象的接口。做为一个从脚本语言访问CORBA对象的例子,这里有个IDL描述样例,看看它是如何映射到Ruby语言的。

[代码 P337 第一段]

11.3 用Boost Python添加Python绑定

本章的剩余部分将专注于让你理解如何为C++ API创建绑定。开始我先介绍如何使用Boost Python库创建Python绑定。

Python是由Guido van Rossum在1991年设计的开源动态类型语言。Python是强类型的,自动内存管理和引用引用计数。它配备了一个庞大的标准库,包括的模型如:os、sys、re、difflib、 codecs、datetime、math、gzip、csv、socket、json和xml等。Python有个和C/C++迥异的地方是:它是用缩进来定义模块边界的,而C/C++是使用花括号。Python的CPython实现是最为常见的,不过还有其它的主要实现,如Jython(使用Java编写的),还有Iron Python(面向.NET框架)。要想了解更多Python语言的细节,请参见:http://www.python.org/

前面已经提到过,Boost Python是用来通过编程的方式来定义Python绑定,接着就可以被编译成由Python直接加载的动态库。图11.1给出了这个的基本流程。

[图 P337 第一张]

图11.1

使用Boost Python创建C++ API的Python绑定的流程。白色框代表文件,阴影框代表命令。

11.3.1 绑定Boost Python

很多Boost包完全是通过头文件实现的,使用的是模板和内联函数,因此你只需要确保在把Boost目录添加到编译器中时要包含搜索路径。然而,使用Boost Python需要你编译和链接boost_python库,因此你需要知道如何编译Boost。

编译Boost库的推荐方式是利用bjam程序,位于Jam编译系统的Perform->Perforce[l4]子菜单下。因此,你首先需要下载bjam。预编译的可执行文件可以在http://www.boost.org/下载到,可在多个平台上运行。

在UNIX变种平台上编译boost库,如Linux或Mac,步骤如下所示:

[代码 P338 第一段]

<toolset>字符串是用来定义编译器,例如:“gcc”、“darwin”、“msvc”或“intel”。

如果你的机子上安装有多个版本的Python,你可以在bjam配置文件中指定要使用的版本,这个配置文件是user-config.bjam,你应该在主目录中创建。你可以在Boost.Build的用户手册中查找到更过bjam配置的细节,你要往user-config.bjam文件中添加下面那样的实体:

[代码 P338 第二段]

在Windows系统上,你得通过命令行(只要运行bootstrap.bat,代替boostrap.sh)来执行相似的步骤。然而,你可以从http://www.boostpro.com/下载预编译的boost库。如果你使用的是预编译的库,你需要确保你用来编译脚本的Python版本和用来编译BoostPro库的版本应该是一样的。

11.3.2 用Boost Python包装C++ API

先来看看一个简单的C++ API,接着将暴露给Python。我使用的是电话簿的例子,可以存储多个联系人的电话号码。这给我们已给可管理的、却不繁琐的例子,整章都使用这个例子。下面是电话簿API的公共定义。

[代码 P338 第三段]

要注意的是这可以让我们演示很多功能,如包装多个类、STL容器的使用和多个构造函数。除了直接映射C++的成员函数到Python的方法,我也会借此机会演示创建Python属性。

Person类本质上只是一个数据容器:它只包含访问底层数据成员的getter/setter方法。这些都是不错的转化成Python属性的候选者。Python属性的行为和使用getter/setter方法管理对象访问的普通对象是相似的(也包含一个删除方法用来销毁对象)。这样做可以更直观地访问类的成员,你想要的行为就是像一个简单的数据成员那样,同时也为你提供了控制获取和设置值的逻辑。

现在已经给定了C++ API,接着让我们看看如何使用boost::python来指定Python绑定。通常你可以创建一个单独的.cpp文件来为给定的模块指定绑定,出于方便可以使用和模块一样的文件名,外加一个_wrap后缀。也就是说,我在本例中使用的是phonebook_wrap.cpp。这个包装文件指定了你要暴露的类和在那些类中可用的方法。下面的boost::python代码是关于包装phonebook.h API所必须的。

[代码 P339 第二段]

请留意Person类,我定义了两个属性,我提供了C++ getter/setter函数来控制访问那些属性(如果只提供一个getter方法,那么属性就是只读的了)。对于PhoneBook类,我定义了标准的方法:size()、add_person()、remove_person()和find_person()。我还明确地指定了find_person()返回指针值。

接着,你就可以把phonebook.cpp和phonebook_wrap.cpp编译成动态库。这将涉及到对Python和boost::python编译头文件,还有链接两者的库。这在Mac和Linux上的结果是phonebook.so,而在Windows上是phonebook.dll。(注意:Python无法识别Mac上的.dylib扩展名)。例如,在Linux上:

[代码 P340 第二段]

提示

要确保用来编译脚本的Python版本和用来编译BoostPro库的版本应该是一样的。

现在,你就可以使用import关键字直接把动态库装载到Python中。这里有一些装载C++库的Python样例代码,还演示了Person和Phonebook对象的创建。注意访问Person对象属性的语法,例如:p.name,与之形成对比的是PhoneBook成员的方法调用语法,例如:book.add_person()。

[代码 P340 第三段]

11.3.3 构造函数

在我们的phonebook_wrap.cpp文件中,我没有明确地为Person或PhoneBook类指定构造函数。在这种情况下,Boost Python为每个类暴露默认的构造函数,这就是为什么我可以这样写:

[代码 P341 第一段]

不过,要注意的是在C++ API中,Person类有两个构造函数,一个是默认的构造函数,另一个是带有一个字符串参数的构造函数:

[代码 P341 第二段]

你可以为Person类修改包装代码,在类中指定单个构造函数,这样就可以让Boost Python暴露这些构造函数,接着使用.def()语法列出更多的构造函数。

[代码 P341 第三段]

现在你就可以从Python创建Person对象,并可使用那两个构造函数。

[代码 P341 第四段]

11.3.4 扩展Python API

你也可以往Python API中添加原先不存在于C++ API中的方法。这通常是用来定义一些标准的Python对象方法,如__str__()返回一个可读的对象版本或__eq()用来测试相等。

下面的例子,我修改了phonebook_wrap.cpp文件,加入了一个静态*函数,用来输出Person对象的值。接着我使用这个函数在Python中定义Person.__str__()方法。

[代码 P342 第一段]

这演示了往一个类中添加新方法的功能。不过,在这个特殊的例子中,Boost Python还提供了一种可选的方式来为__str__()函数指定更符合语言习惯的风格。你可以为Person定义operator<<,让Boost为__str__()方法使用这个操作符。例如:

[代码 P342 第二段]

有了这个Person.__str__()的定义,现在你可以按照下面的方式来编写代码(在Python解释器提示符中输入,>>>):

[代码 P343 第二段]

当我谈论扩展Python API时,我会注意到Python的动态性意味着你可以在运行时往一个类中添加新的方法。这并不是Boost Python的特性,而是Python语言本身的一个核心功能。例如,你在Python层可以这么定义__str__()方法:

[代码 P343 第三段]

这将会输出如下文本:

Name: Martin

Home: (123) 456-7890

11.3.5 C++的继承

C++和Python都支持多继承,Boost Python可以轻松地暴露任何C++类的所有基类。我将演示这是如何完成的,把Person类转换成一个基类(例如,提供一个虚析构函数)并添加一个叫PersonWithCell的派生类,增加一个指定手机号码的功能。这并不是一个特别好的设计,不过在本例中可以很好地阐明我们的议题。

[代码 P344 第一段]

接着,你就可以像下面那样更新包装文件,在Python中表示这种继承层级:

[代码 P344 第二段]

现在,你可以从Python中创建PersonWithCell对象:

[代码 P345 第一段]

11.3.6 跨语言多态

你可以在Python中创建继承自C++类(该类已经通过Boost Python对外暴露)的类。例如,下面的Python程序演示的是如何直接在Python中创建PersonWithCell类,并且仍然可以把这个类的实例添加到PhoneBook。

[代码 P45 第二段]

当然,PyPersonWithCell的cell_number属性只能从Python调用,C++并不知道这个动态添加到派生类的新方法。

还值得注意的是:即使是在Python中重载的C++虚函数在默认情况下也不能从C++处调用。然而,如果跨语言多态对你的API很重要的话,那么Boost Python有提供一种实现这个的方法。这是通过定义一个包装类实现的,多重继承自C++类并和Boost Python的包装类模板捆绑在一起。接着,这个包装类会检测在Python中是否有为给定的虚函数定义重载,如果有定义的话就调用那个方法。例如,给定一个叫Base的C++类,有一个虚方法,你可以这样创建包装类:

[代码 P346 第一段]

接着,你可以这样暴露这个Base类:

[代码 P346 第二段]

11.3.7 支持迭代

Boost Python也允许你创建Python迭代器,这是基于在C++ API中定义的STL迭代接口。这让你能够在Python中创建可以在容器中迭代元素的对象。例如,你可以往PhoneBook类中添加begin()和end()方法,提供对STL迭代器的访问,这样就可以遍历电话簿中的所有联系人。

[代码 P346 第三段]

有了这些新增的方法,你就可以扩展这个包装,为PhoneBook类指定__iter__()方法,就是为对象返回一个迭代器的Python的方式。

[代码 P347 第二段]

现在,你就可以编写在PhoneBook对象中迭代所有联系人的代码:

[代码 P347 第三段]

11.3.8 整合全部

在接下来的章节,我将把所有介绍过的这些内容全部整合起来,下面是phonebook.h头文件的完整定义,还有phonebook_wrap.cpp的boost::python包装。

[代码 P347 第四段]

11.4 用SWIG添加Ruby绑定

下面的章节让我们看看为C++ API创建脚本绑定的另一个例子。我将使用一个简化过的包装和接口生成器,[l5]我将使用这个实用程序来创建Ruby语言绑定。

Ruby是一个开源动态类型脚本语言,是由日本人*(外号Matz)在1995年发布的。Ruby语言受到Perl和Smalltalk的影响,特别强调易用性。在Ruby中,一切都是对象,即使是在C++中单独对待的内建基本类型,如int、float和bool。Ruby是非常流行的脚本语言,特别是在日本,比Python还流行,因为是在那里发明的。想要了解更多Ruby语言的信息,请参见:http://www.ruby-lang.org/

SWIG是靠读取接口文件中的绑定信息运作的,并生成C++代码来指定绑定。这些生成的代码可以编译成动态链接库,直接被Ruby加载。图11.2演示了这种基本流程。需要注意的是:SWIG支持多种脚本语言。我将使用它来创建Ruby绑定,不过它也可以一样容易地用来创建Python、Perl或其它几种语言的绑定。

[图 P350 第一张]

图11.2

使用SWIG创建C++ API的Ruby绑定的流程。白盒表示文件,灰盒表示命令。

11.4.1 用SWIG包装C++ API

我将采用Python例子中一样的电话簿API例子,接着将演示如何使用SWIG为这个接口创建Ruby绑定。电话簿的C++ API如下所示:

[代码 P350 第一段]

现在,让我们看看一个基本的SWIG接口文件是如何把C++ API暴露给Ruby的。

[代码 P351 第一段]

你可以看到:接口文件和phonebook.h的头文件非常相似。实际上,SWIG可以直接解析绝大多数C++语法。如果C++头文件很简单的话,那么你甚至可以使用SWIG的%include指令简单地通知它直接读取C++的头文件。我这里并不这么做,以便你可以直接控制哪些要暴露给Ruby,哪些不暴露给Ruby。

现在,有了已给初始的接口文件,你就可以让SWIG读取这个文件,并为所有指定的C++类和方法生成Ruby绑定。这将创建一个phonebook_wrap.cxx文件,可以和C++代码一起编译,生成一个动态链接库。例如,在Linux中的步骤是这样子的:

[代码 P351 第二段]

11.4.2 调整Ruby API

尝试Ruby绑定的第一步是相当简单的。为了让API对Ruby程序员看起来更自然,你需要解决几个问题。首先,Ruby方法的是命名是采用蛇形模式来代替驼峰模式,也就是用add_person()来代替AddPerson()。SWIG通过在脚本API中重命名符号来支持这个,也就是使用%rename命令。例如,你可以往接口文件中添加下面的代码行,通知SWIG重命名PhoneBook的方法。

[代码 P352 第一段]

最新的SWIG版本还支持一个-autorename命令行选项,可以自动执行这个操作。预计这个选项最终将默认启用。

其次,Ruby也有一个和Python属性相似的概念,可以用来方便地访问数据成员。实际上更优雅,Ruby中的所有实例变量都是私有的,因此必须通过getter/setter才能访问。%rename语法也可以用来实现这种功能。例如:

[代码 P352 第二段]

最后,你应该注意到我往PhoneBook C++类中添加了一个额外的IsEmpty()方法。如果还没有联系人添加到电话簿中,那么这个方法就会返回true。我添加这个方法的原因是为了演示如何把一个C++的成员函数暴露成一个Ruby查询方法。这是一个返回布尔值的方法,按照惯例,它以一个问号结束。因此,我喜欢C++IsEmpty()函数在Ruby中显示成empty?。这既可以在SWIG中用%predicate指令实现,也可以使用%rename。

[代码 P352 第三段]

经过对接口文件这些修正,我们的Ruby API感觉更自然了。如果你要在接口文件中返回SWIG并重新编译phonebook动态库,那么你就可以把它直接导入Ruby,编写如下代码:

[代码 P352 第四段]

要注意的是使用p.name的getter和p.name的setter,还有采用蛇形模式的方法名:add_person()。

11.4.3 构造函数

Person类有两个构造函数:一个是不带参数的默认构造函数,另一个是带有std::string参数的非默认构造函数。使用SWIG,你只需在接口文件中包含那些构造函数的声明,它就会自动在Ruby中创建相关的构造函数。也就是说,有了先前的接口文件,你就已经可以这么做:

[代码 P353 第二段]

通常说来,Ruby中的方法重载不如在C++中那么灵活。例如,SWIG无法消除映射到Ruby中相同类型的重载函数之间的歧义。例如,一个构造函数接收一个short参数,另一个接收int或者一个构造函数接收一个对象的指针,另一个接收相同类型的引用。SWIG并未提供一种方式来处理这些,让你能够忽略给定的重载方法(使用%ignore)或者重命名其中的一个方法(使用%rename)。

11.4.4 扩展Ruby API

SWIG让你可以扩展C++ API的功能。例如,往类中添加一个只能在Ruby API中使用的方法。这可以用SWIG的%extend指令实现。为了演示这个,我将往Ruby版本的Person类中添加一个to_s()方法。这是一个标准的Ruby方法,用来返回表示对象的可读信息,相当于Python中的__str__()方法。

[代码 P353 第三段]

对Person绑定使用这个新的定义,你就可以编写下面的Ruby代码:

[代码 P354 第二段]

puts p行将使用to_s()方法输出Person对象。在本例中,将输出如下结果:

Martin: (123) 456-7890

11.4.5 C++的继承

有了刚刚给定的那些构造函数,在使用SWIG时,你无需为表示继承做些什么特别的操作。你只要使用标准的C++语法在接口文件中声明类。例如,你可以往API中添加下面的PersonWithCell类:

[代码 P354 第三段]

接着,你就可以更新SWIG接口文件:

[代码 P355 第二段]

现在,你就可以从Ruby中访问这个C++派生类:

[代码 P356 第二段]

Ruby只支持单继承,支持附加的mixin类。当然,C++是支持多继承的。因此,在默认情况下,SWIG只会考虑派生类的基类列表的第一个基类:任何其它基类中的成员函数都不会被继承。然而,SWIG的近期版本支持一个可选的-minherit命令行选项,在使用Ruby mixin时模拟多重继承(不过,在这种情况下,Ruby中的类就不再有真正的基类)。

11.4.6跨语言多态

在默认情况下,如果你在Ruby中重载一个虚函数,那么你就不能从C++中调用这个Ruby方法。不过,SWIG提供一种方法,让你可以通过使用它的“引向器”(director)特性来使用这种类型的跨语言多态。当你启用类中的引向器,SWIG就生成一个继承自C++类的新包装类,也就是SWIG的引向器类。引向器类存储一个指向底层Ruby对象的指针,负责决定一个函数调用是否应该指向到Ruby的重载方法或默认的C++实现。这和Boost Python支持的跨语言多态都是采用相似的方法。然而,SWIG会在幕后创建包装类:你所要做的就是指定你要创建哪个类的引向器,并使用%module指令启用引向器特性。例如,下面更新过的接口文件将为所有的类开启跨语言多态:

[代码 P356 第三段]

11.4.7 整合全部

我通过多次迭代改进简单的例子,这是为了每次添加增加的内容。因此,在本章节快要结束时,我将给出完整的C++头文件和SWIG的接口文件,方便你参考。首先,下面是C++ API:

[代码 P357 第一段]

接着是最终的SWIG接口(.i)文件:

[代码 P358 第二段] 

 更通顺

 the PyObjC utility provides a bridge for Objective-C objects in Python

 Various technologies can be used to generate the wrappers that allow a scripting language to call down

into your C++ code.

 原书勘误 P338

 更通顺

Power by YOZOSOFT

C++ API 设计 16 第十一章 脚本,布布扣,bubuko.com

C++ API 设计 16 第十一章 脚本

上一篇:C++ API 设计 14 第九章 文档


下一篇:C++ API 设计 15 第十章 测试