在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
欢迎转载,请保留出处:http://www.cnblogs.com/wellbye/
二、跨语言交互的实质 跨语言交互,也就是多语言混合编程,其实也是理解lua与c++交互的一个关键。 首先,是理解为什么要多语言混合使用,只用c++不行吗?答案是因为脚本语言语法糖多使用方便、沙盒式安全机制使系统更稳定、整体概念简单易学降低开发成本,等等……那么,只用脚本不行吗?那也是不行的,因为与系统api的接口、计算密集性模块的性能要求等是脚本语言不擅长的,这一部份仍然需要c/c++来完成。因此,为了综合各自的优势,就出现了混合编程的需要。 其次,是理解混编程序的大致运行流程,即在一个程序的生命周期里,哪些部份是c++写的,哪些部份是lua写的?哪里是交互接口的地方?以一个事件驱动型程序来说,程序启动、创建窗口及渲染器、事件循环分发等,都是在c++里做的,各种窗口消息、网络事件的接收分发也是c++做的,但是消息和事件的处理器有不少是在lua里的写的,这些脚本处理器会根据传入的参数及当前状态做出反应,包括改变脚本自身环境内的变量(这就影响了所谓的“当前状态”)、调用绑定的c函数从而修改了c/c++端的变量(从而影响了c++端的逻辑行为)。c++部分和lua部份的代码就这样交替往复的运行着,而彼此调用对方、传递信息的时候,也就是所谓的交互接口。 再次,是理解跨语言交互的大体技术。在一个语言里,怎么使用另一个语言的变量?调用另一个语言里的函数?这涉及到脚本语言的实现机制,一般来说都会有一个编译模块、一个虚拟机(执行)模块、一套类型实现及数据管理模块,通常还会有一个供外部操作的接口,如lua c api,这个接口让嵌入方得以操作脚本状态(如访问变量、调用函数、管理内存),所以在c/c++程序里,会通过这些c api将一些重要函数导出到脚本里供其调用,并在适当的时候触发调用脚本函数,交互也就由此而生了。 最后,总结一下,所有程序的本质功能都是处理数据,所有程序最终都是以机器码的形式被硬件CPU执行,从这两个角度去看,不同语言的代码并没有本质区别(脚本代码只是脚本虚拟机c模块的‘数据’而已),大家都是在处理数据,只是在不同时机处理数据的不同的部份,而所谓交互,就是在处理共享数据。
三、c++对象模型 说了半天脚本,现在该说另一半,c++了。但是还得先说一下更基础的c。相对于探讨c++与lua的交互来说,c与lua的交互则少有提及,因为那个实在是太简单了:c能够导出给lua的只有函数和常量,而导出函数恰恰是lua c api的标准功能,只要提供一个形式如下的c函数: typedef int (*lua_CFunction) (lua_State *L); 就可以将其注册到脚本里供lua调用,调用的参数可用各种lua c api从L中提取。但是这有一个问题,lua c api支持的外部c函数只能是具有以上格式(返回值及参数类型定义,又称函数签名)的函数,但我们通常写的(或者早就已经写好的)c函数都不是这样子,因为写成这样子就只能被lua调了,其它c代码就无法使用——虽然有不少绑定框架就是这么做的——但这不是我们的目的,我希望核心业务模块是脚本独立的,即纯粹按照c/c++使用的方式来编写,而额外的可选的脚本模块以一种侵入性最小的方式来将其粘合进其它语言。回到这个问题,由于我们正常的业务逻辑c函数,都不符合lua的要求,怎么办呢,那就是为每一个目标函数做一个包装函数,包装函数的签名符合lua_CFunction的格式,在每个包装函数内,从L中提取相应的参数再去调用目标函数,再将返回值送进L。举例来说,下面这个简单的c函数: int add(int x,int y) { return x+y; } 需要一个这样的包装函数: int C_add(lua_State* L) { int x = lua_tonumber(L,1); int y = lua_tonumber(L,2); int ret = add(x,y); lua_pushnumber(L,ret); return 1; } 为每一个c函数手工写这样的包装函数,实在太繁琐了。我们需要一种数据驱动的方法,将所有目标函数的信息填表,然后只手工写一个总控包装函数,在这个函数里,根据脚本调用的函数名,找到真正的目标函数指针,并从脚本中提取它需要的参数,拿它们去调用这个函数指针。关于函数信息表的生成和函数指针的动态调用,将在后文中介绍,在此先说明这个概念,即:实际稍大规模的工程项目中,完全手写包装是不可能的事(低效、易出错、缺乏变动一致性),需要借助函数指针这个有力工具,来完成高度概括、以不变应万变的自动绑定机制。
现在回到c++与lua的交互。一到c++里问题就复杂了,因为多了一个类和对象的概念,大部份时候,我们使用的是对象指针,调用的是其成员函数。这就出现了我之前提到的在群上屡见不鲜的问题——“我把一个c++对象导到lua里,怎么调用它的函数啊?” -_-#! 这属于还没有认真思考过c++的对象模型,不知道c++对象和它的成员函数之间是什么关系。可能误以为对象是个大箩筐,所有函数都装在它身上了,只要把它导出到脚本,脚本就可以像c++一样使用其所有功能。实际上c++对象只是个普通结构体,包含了所有数据成员,那函数存在哪呢?其实所有函数本身都存在代码段,跟对象是毫无关联的,因为对象作为数据,都是在数据段。但是在对象身上,会有一些指针字段,辗转链接指向与对象相关的特殊函数。所谓的特殊函数,就是虚函数,因为在引用函数时都是通过函数名,而在子类重载过父类虚函数时,就会出现多个相同名字的函数,因此必须通过对象身上的特殊指针来索引,才能取到正确的函数。普通函数因为不可能有重名,所以不需要在对象上做特殊关联,直接通过名字就可以引用到惟一版本。简单概括一下:对象是一个存在于数据段上的结构体,其字段除成员变量外,还有一些隐含的特殊指针字段,指向其类型所对应的虚函数表,表里的每一项是一个函数指针,指向代码段里的函数实现。值得注意的是,多重继承时,就会有多个虚表指针,它们存在对象这个结构体的不同位置。举例来说,一个简单的继承链: class base1{ virtual void b1_func1(); virtual void b1_func2(); } class base2{ virtual void b2_func1(); virtual void b2_func2(); } class derived : b1,b2 { int a; }
this--> --------------- 这里vtable是属于每个类的,该类的对象都会有一个指向此类vtable的指针。当在一个对象指针上调用其虚函数时: derived* obj = new derived(); obj->b2_func1(); 会先确定此虚函数所属的基类(base2)并根据其在基类列表中的顺序得知其虚表指针在对象结构体中的偏移,然后在obj指针上加上此偏移得到指向该对象的base2部份头部的地址,一方面需要通过此地址来索引base2的虚表,另一方面在给base2::b2_func1函数传递this参数时,需要的不是derived子类型的obj,而是偏移过后的base2头部地址。用伪代码来表示上述过程就是:
derived* obj = malloc(sizeof(derived)); call_derived_ctor(obj); //c++ new operator的两个步骤 offset = offsetof(base2,derived); //因为调用的b2_func1属于base2,所以需找到base2与derived的偏移 base2* b2_obj = (base2*)(((char*)obj)+offset); //得到obj的base2部位 fp_b2func1 = (void**)(*b2_obj)[indexof(b2_func1)]; //通过虚表及虚函数索引得到真正的函数地址 fp_b2func1(b2_obj); //调用函数,将base2部位地址当作this传入 注意第4行,根据目标函数期望的类型来调整对象指针,这是由c++编译器自动完成的,但是在与lua的交互过程中,c++对象指针在一次来回传递后类型信息已丧失,这时需要我们自己补上该操作。 至此应该很清楚了,所谓注册c++类到lua,包括两部份:一是把类成员函数的指针包装注册到lua,在脚本中拿到以userdata表示的对象指针后,其自身并没有“主动技能”,而是作为保存和传递的媒介,把它作为第一个参数,去调用包装后的成员函数;二是这个userdata的创建与销毁,对应着c++构造、析构函数的调用,它们的处理在本质上与一般成员函数并无差别,只是又多一些特殊部骤,如lua端对象表格的设置、userdata上挂gc函数等。 基本的概念都介绍完了,从下一章开始进入实践阶段,以我写的那个脚本粘合库为例,说明实现c++与lua交互的所有细节。
|
请发表评论