转自:http://www.devbean.net/2012/03/building-your-own-plugin-framework-3/
编程语言的支持
在前面的章节中,我们已经了解到,如果你能够使用编译器令应用程序和插件的虚表匹配,那么就可以保持 C++ 虚表级别的兼容性;或者你可以使用 C 级别的兼容性,然后就能使用不用的编译器去构建系统,但其限制在于你只能编写纯 C 应用。这样的话,你就不能够使用前面我们在例子 IActor 中看到的那种优雅的 C++ 接口。
纯 C 实现
在纯 C 的编程模型中,你只能使用 C 开发插件。当实现PF_CreateFunc
函数时,需要返回一个 C 对象同应用程序的 C 对象模型进行交互。
但是,我们知道,C 语言是一个过程语言,没有对象的概念。那么,刚刚提到的 C 对象和 C 对象模型是怎么回事呢?为了理解这一点,我们必须认识到,C 语言已经提供了足够多的抽象机制来实现对象、引入多态(这一点在我们的插件框架中尤其重要)以及支持面向对象风格的编程。事实上,原始的 C++ 编译器就是一个 C 编译器的前端。它将 C++ 代码翻译成 C 代码,然后直接输出给一个普通的 C 编译器(当然,我们说的是“原始的”C++ 编译器)。它的名字就叫 Cfront 这已经足够说明问题了。
核心技术是,使用包含函数指针的结构体。每个函数声明的第一个参数都应该是指向本结构体的指针。这个结构体也可能包含其他数据成员。也就是说,我们使用结构体模拟了 C++ 的类,提供了封装(在一个地方保存状态和行为)、继承(通过将父结构体作为第一个数据成员实现)和多态(通过设置不同的函数指针实现)。
C 不支持析构函数、函数和运算符的重载以及命名空间,所以在定义接口时,我们能够设置的选项极为有限。这可能有点因祸得福,因为那些掌握了 C++ 语言不同子集的人们都明白使用接口,而不一定了解析构函数、运算符重载那些机制。减少接口的语言结构上的限制,有助于简化接口,提高可用性。
在下面的章节中,我们将讨论面向对象的 C 语言设计。下面是我们的简单游戏的 C 对象模型的实现。如果你快速浏览一遍,你会发现它甚至支持集合类型和对象遍历器。
C
1 #ifndef C_OBJECT_MODEL 2 #define C_OBJECT_MODEL 3 4 #include <apr-1/apr.h> 5 6 #define MAX_STR 64 /* max string length of string fields */ 7 8 typedef struct C_ActorInfo_ 9 { 10 apr_uint32_t id; 11 apr_byte_t name[MAX_STR]; 12 apr_uint32_t location_x; 13 apr_uint32_t location_y; 14 apr_uint32_t health; 15 apr_uint32_t attack; 16 apr_uint32_t defense; 17 apr_uint32_t damage; 18 apr_uint32_t movement; 19 } C_ActorInfo; 20 21 typedef struct C_ActorInfoIteratorHandle_ { char c; } * C_ActorInfoIteratorHandle; 22 typedef struct C_ActorInfoIterator_ 23 { 24 void (*reset)(C_ActorInfoIteratorHandle handle); 25 C_ActorInfo * (*next)(C_ActorInfoIteratorHandle handle); 26 27 C_ActorInfoIteratorHandle handle; 28 } C_ActorInfoIterator; 29 30 typedef struct C_TurnHandle_ { char c; } * C_TurnHandle; 31 typedef struct C_Turn_ 32 { 33 C_ActorInfo * (*getSelfInfo)(C_TurnHandle handle); 34 C_ActorInfoIterator * (*getFriends)(C_TurnHandle handle); 35 C_ActorInfoIterator * (*getFoes)(C_TurnHandle handle); 36 37 void (*move)(C_TurnHandle handle, apr_uint32_t x, apr_uint32_t y); 38 void (*attack)(C_TurnHandle handle, apr_uint32_t id); 39 40 C_TurnHandle handle; 41 } C_Turn; 42 43 typedef struct C_ActorHandle_ { char c; } * C_ActorHandle; 44 typedef struct C_Actor_ 45 { 46 void (*getInitialInfo)(C_ActorHandle handle, C_ActorInfo * info); 47 void (*play)(C_ActorHandle handle, C_Turn * turn); 48 49 C_ActorHandle handle; 50 } C_Actor; 51 52 #endif
纯 C++ 实现
在纯 C++ 编程模型中,你需要使用 C++ 开发插件。插件的接口函数可以由 static 成员函数实现,或者是普通的 static、全局函数(毕竟,C++ 简单来说就是 C 的超集)。此时,我们的对象模型就可以是普通 C++ 对象模型。下面的代码是同一游戏的 C++ 对象模型的实现。它和前面的 C 对象模型几乎是一样的。
C++
1 #ifndef OBJECT_MODEL 2 #define OBJECT_MODEL 3 4 #include "c_object_model.h" 5 6 typedef C_ActorInfo ActorInfo; 7 8 struct IActorInfoIterator 9 { 10 virtual void reset() = 0; 11 virtual ActorInfo * next() = 0; 12 }; 13 14 struct ITurn 15 { 16 virtual ActorInfo * getSelfInfo() = 0; 17 virtual IActorInfoIterator * getFriends() = 0; 18 virtual IActorInfoIterator * getFoes() = 0; 19 20 virtual void move(apr_uint32_t x, apr_uint32_t y) = 0; 21 virtual void attack(apr_uint32_t id) = 0; 22 }; 23 24 struct IActor 25 { 26 virtual ~IActor() {} 27 virtual void getInitialInfo(ActorInfo * info) = 0; 28 virtual void play( ITurn * turnInfo) = 0; 29 }; 30 31 #endif
同时提供 C/C++
在同时提供 C/C++ 的编程模型中,我们既可以使用 C 语言开发插件,也可以使用 C++。当向系统注册插件时,我们必须指明到底是 C 对象还是 C++ 对象。当你创建一个平台时,这一技术无疑非常有用,因为你不应该限制第三方开发人员必须使用哪种语言。
我们的插件框架支持这种编程模型,但是真正的工作应当由应用程序去完成。每种对象类型都必须同时实现 C 接口和 C++ 接口。这意味着,你会有一个含有虚表的 C++ 类,还会有一个指向虚表中函数的指针的集合。这种机制并不那么重要,我们会在后面的游戏开发中具体演示。
注意,从插件开发者的角度来看,混合 C/C++ 模型不会增加任何额外的复杂度。插件开发者通常会使用 C 接口或者 C++ 接口来开发 C 或者 C++ 插件。
混合使用 C/C++
在混合使用 C/C++ 的编程模型中,我们可以使用 C++ 开发插件,但是需要基于 C 对象模型。这种机制需要引入 C++ 包装类,用于实现 C++ 对象模型,但是要包装成 C 对象。插件开发者需要自己处理这一层,在 C 和 C++ 之间来回翻译每一个调用、参数以及返回值。这意味着,你需要在应用程序对象模型之上做更多工作,但是通常这些工作都很直接。这样做的好处是,能够让插件开发者使用方便的 C++ 编程模型,同时拥有 C 级别的兼容性。我们会在后面的章节中再次回到这个问题上来。
语言对比
下面我们给出两种语言(C 和 C++)、两种部署方式(动态、静态)的对比。
C++ | C | |
静态 |
|
|
动态 |
|
|
我们讨论的目的是认识到,在同时支持 C/C++ 的模型中,如果使用 C++ 编写插件,系统将得益于,也会受限于 C++;如果使用 C 编写插件,系统则得益于,也会受限于 C。而在混合 C/C++ 模型中,系统仅受到 C 的影响,因为 C++ 那层完全隐藏在插件实现之下了。初看起来,这有点复杂,但是至少你有机会做出选择,而且插件系统允许你自己选择最合适的实现方式。这并不会强制你使用某一种编程模型,也不会要求去找一个最小公分母。我们只需看清二者的差别,选择最合适的方式。