cocos2d-x支持多种脚本引擎的绑定,例如支持lua(通过lua或luajit)、javascript(通过SpiderMonkey脚本引擎),分别对应libluacocos2d和libjscocos2d两个工程,每个工程里分别对应大量的自动绑定和手动绑定代码。如果需要增加一些引擎功能需要绑定到脚本的话,两个工程都需要修改代码,非常不便于维护。假如希望使用其他脚本引擎的话(例如google红红火火的V8,或者ms的chakra),那得多开几个工程,每个工程都需要实现几乎一样,但是又不一样的代码。现在我提出一种思想,来解决这类问题。
现有的脚本引擎多如牛毛,而不同企业间可能有不同的技术积累,希望选择不一样的脚本引擎,有的python蟒蛇派,有的是lua派,有的是ruby派,还有JavaScript、lua等等等等,所以,JavaScript和Lua不应该成为二选一。
而这些脚本引擎之间,有很多的共同点,例如都是弱类型语言,都是支持那么几种简单类型,都是使用GC机制来回收等等。
脚本引擎统一化,是把多个脚本引擎(lua、JavaScriptCore、chakra、v8、spidermonkey等等)通过同一套抽象接口进行封装,引擎的脚本绑定代码都通过抽象接口来编写。通过选择编译不同的实现层来实现脚本引擎间的切换,而不是每个脚本引擎都要写不一样的脚本绑定代码,这样能大大简化脚本绑定层的维护成本,并能保证所有脚本引擎的接口的绝对一致性,并让用户轻松选择使用哪个脚本引擎,而不限定于必须使用官方选择的引擎。
脚本引擎抽象层API的定义非常关键,概括起来我认为他必须符合以下要求:
1、抽象层的接口必须在各个脚本引擎之间都可以实现,例如:“创建字符串”这个接口,是任意脚本引擎都能实现。
2、抽象层在绑定脚本的过程中是足够用的,例如:我们需要导出一个C++的类,还需要导出类里的函数,还有特殊的结构体,甚至包含lambda表达式,这些都需要考虑进去抽象层定义的需求里。
3、抽象层还必须足够的薄,薄到运行时根本感觉不到他的存在。需要使用“宏”或者inline函数的方式来给这个抽象层减肥,坚决不使用C++类的方式来加厚他。
4、抽象层在使用的过程中必须足够简单,简单到就好像是一个单独的脚本引擎API一样,这些API看上去是大统一的,并且是脚本语言无关的。
在定义抽象层的过程中,我参考了很多别人的方案,最终,我使用 了这么个方案:
1、使用C inline函数来定义API接口函数的声明,各个引擎的实现部分分别实现这些函数
2、定义一些基本类型,基本类型有各个脚本引擎来最终定义,但名字和意义都是统一的,初步定义了这些类型,每个类型在不同脚本引擎中对应的类型分别如下表:
统一类型 | 描述 | Lua | JavaScript | JavaScriptCore | V8 | chakra |
USValue | 表示脚本中的任意类型 | 任意类型
| Object | JSValueRef | v8::Local<v8::Value> | JsValueRef |
USObject | 脚本对象,对象可以由key、value组成,可以拥有继承结构 | table | Object | JSObjectRef | v8::Local<v8::Object> | JsValueRef |
USFunction | 脚本函数 | function | Function | JSObjectRef | v8::Local<v8::Function> | JsValueRef |
USArray | 数组类型,下标从0开始 | table | Array | JSObjectRef | v8::Local<v8::Array> | JsValueRef |
USMap | 键-值配对的map类型 | table | Map | JSObjectRef | v8::Local<v8::Object> | JsValueRef |
USSet | 值作为键也作为值得列表,值不能重复 | table | Set | JSObjectRef | v8::Local<v8::Object> | JsValueRef |
USNumber | 数值类型 | number | Number | JSValueRef | v8::Local<v8::Number> | JsValueRef |
USBoolean | bool类型 | boolean | Boolean | JSValueRef | v8::Local<v8::Boolean> | JsValueRef |
USString | 字符串类型 | string | String | JSValueRef | v8::Local<v8::String> | JsValueRef |
USBuffer | 内存块缓存对象 | string | Int8Array | JSValueRef | v8::Local<v8::Int8Array> | JsValueRef |
USConstructor | 对象实例的构造器,用来导出C++类 | table | Object | JSValueRef | v8::Local<v8::Object> | JsValueRef |
3、定义一批API,用以对以上定义的基本类型进行创建、调用、修改等操作,例如创建的过程API定义成这种形式:
1 // 创建Null值
2 inline USValue createUSNull();
3 // 创建Undefined值
4 inline USValue createUSUndefined();
5 // 创建普通的对象
6 inline USObject createUSObject();
7 // 创建数组
8 inline USArray createUSArray(int length = 0);
9 // 创建Map
10 inline USMap createUSMap();
11 // 创建Set
12 inline USSet createUSSet();
13 // 创建字符串
14 inline USString createUSString(const char *str, int length = -1);
15 // 创建Buffer,将会拷贝数据到Buffer中,脚本引擎负责销毁
16 inline USBuffer createUSBuffer(const char *buffer, size_t size);
17 // 创建一个脚本函数,函数调用时会回调到callback,并带上data
18 USFunction createUSFunction(USFunctionCallback callback, void *data = nullptr, const char *name = nullptr);
19 // 创建数字
20 inline USNumber createUSNumber(double number);
21 // 创建bool
22 inline USBoolean createUSBoolean(bool value);
23
24 // 创建对象构造器
25 USConstructor USClassCreateConstructor(const USClass &cls);
抽象层定义好后,需要经过大量的努力,才能在各个脚本引擎间的最终实现。当最终实现完毕后,就可以下一步工作:
1、把cocos2d-x对于自动绑定代码的template类进行修改,修改成使用统一脚本引擎的API
2、把cocos2d-x对于手动绑定的代码如法炮制
3、使用不同的引擎实现来编译
经过这样如法炮制之后,最终cocos2d-x只剩下一套脚本绑定的工程,而通过选择不同的底层脚本引擎,却可以编译出完全不一样的脚本引擎版本。
经过cocos2d-x github社区的努力,最终应该会出现不同的fork,如python、ruby等等各种语言出现各种绑定版本,而这种绑定版本的出现,只需实现抽象层API的基本API即可。
至此cocos2d-x的脚本引擎统一即可完成大业。
但,事情还没完,我在下一篇中,将会讲到抽象API的详细定义,敬请期待下一篇。
如果本文对你的开发有所帮助,并且你手头恰好有零钱。
不如打赏我一杯咖啡,鼓励我继续分享优秀的文章。