lua 函数调用1 -- 闭包详解和C调用
这里,简单的记录一下lua中闭包的知识和C闭包调用 前提知识: 在lua api小记2中已经分析了lua中值的结构,是一个 TValue{value,tt}组合,如果有疑问,可以去看一下 ? 一些重要的数据结构? ??? lua中有两种闭包,c闭包和lua闭包 ??? 两种闭包的公共部分: ?????? #define ClosureHeader CommonHeader;???? lu_byte isC;??????? lua_byte nupvalues; GCObject* gclist;?????? struct Table env ????????????????????????????????????????????????????????????????? /*是否是C闭包*/???? /*upval的个数*/?????????????????????????????????? /* 闭包的env,set/getenv就是操纵的它 */ ? ??? C闭包的结构 ??????? struct CClosure{ ???????????? ClosureHeader; ???????????? lua_CFunction f; ???????????? TValue upvalue[1]; ??????? } ????? 结构比较简单,f是一个满足 int lua_func(lua_State*) 类型的c函数 ????? upvalue是创建C闭包时压入的upvalue,类型是TValue,可以得知,upvalue可以是任意的lua类型 ? ??? Lua闭包结构 ?????? struct LClosure{ ?????????? ClosureHeader; ?????????? strcut Proto* p; ?????????? UpVal* upvals[1]; ?????? } ????? Proto的结构比较复杂,这里先不做分析 ? ?? 统一的闭包结构,一个联合体,说明一个闭包要么是C闭包,要么是lua闭包,这个是用isC表识出来的. ?????? union Closure{ ??????????? CClosure c; ??????????? LClosure? l; ?????? } ? 纠结的闭包?????? 为什么大家叫闭包,不叫它函数,它看起来就是函数啊? 为什么要发明一个"闭包"这么一个听起来蛋疼的词呢? 我也纠结在这里好久了,大概快一年半了吧~~~=.=我比较笨~~~随着看源码,现在想通了,拿出一些的自己在研究过程中的心得[尽量的通俗易懂]: ?????? 1. c 语言中的函数的定义: 对功能的抽象块,这个大家没什么异议吧. ?????? 2. lua对函数做了扩展: ????????????? a. 可以把几个值和函数绑定在一起,这些值被称为upvalue. ????????????? ps:? 可能有人觉得c++的函数对象也可以把几个值和函数绑定起来啊,是这样的,但是这个问题就像是"在汇编中也可以实现面向对象呀"一样,lua从语言层面对upvalue提供了支持,就像c++/java从语言层面提供了对类,对象的支持一样,当然大大的解放了我们程序员的工作量,而且配上lua动态类型,更是让人轻松了不少. ????????????? b. 每个函数可以和一个env(环境)绑定. ????????????? ps:? 如果说上面的upvalue还能在c++中coding出来,那么env 上下文环境这种动态语言中特有的东西c++就没有明显的对应结构了吧? 可能有人觉得lua是c写的,通过coding也可以实现,好吧=.=,"能做和做"是两码事,就想你能步行从北京到上海,不表明你就必须要这么做. env是非常重要和有用的东西,它可以轻松创造出一个受限的环境,就是传说中的"沙盒",我说的更通俗一点就是"一个动态名字空间机制". 这个先暂时不分析. ? ?????? 好了,现在我们看到 ??????????? c?????? 函数??? { 功能抽象 } ??????????? lua??? 闭包???? {功能抽象,upvalue,env} ??????????? 重点: 闭包 == {功能抽象,env} ?? ?????? 看到这里,大家都明白了,如果把lua中的{功能抽象,env}也称为函数,不但容易引起大家的误解以为它就是和c函数一样,而且它确实不能很好的表达出lua函数的丰富内涵,闭包,"闭" 是指的它是一个object,一个看得见摸得着的东西,不可分割的整体(first class); "包" 指的是它包含了功能抽象,env. 这里一个很有趣的事实就是,{功能抽象,env}是很多动态语言的一个实现特征,比如lua,javascript都有实现这样的结构,它是先被实现出来,然后冠以"闭包"这样一个名称. 所以,你单单想去理解闭包这个词的话,基本是没有办法理解的,去网上查闭包,没用,你能查到的就是几个用闭包举出的例子,看完以后保证你的感觉是"这玩意挺神秘的,但是还是不懂什么是闭包",为什么不懂?? 因为它指的是一种实现结构特征,是为了实现动态语言中的函数first class和上下文概念而创造出来的. ?????? 宁可多说几句,只要对加深理解有好处就行,有这样两个个句子"我骑车去买点水果" "我用来闭包{功能抽象,env}实现动态语言中的函数first class和上下文概念",闭包和"骑车"都是你达到目地的一种手段,为了买水果你才想了"骑车"这样一个主意,并不是为了骑车而去买水果. 只把把眼睛盯在骑车上是不对的,它只是手段. ? ? 向lua中注册c函数的过程是通过lua_pushcclosure(L,f,n)函数实现的? ?????? 流程:? 1. 创建一个 sizeof(CClosure) + (n - 1) * sizeof(TValue)大小的内存,这段内存是 CClosure + TValue[n],并做gc簿记[这点太重要了,为什么lua要控制自己世界中的所有变量,就是因为它要做gc簿记来管理内存],? isC= 1 标示其是一个C闭包. ???????????? 2. c->f = f绑定c函数.??? ---------? 闭包.功能抽象 = f ???????????? 3. env = 当前闭包的env[这说明了被创建的闭包继承了创建它的闭包的环境].? ----------- 闭包.env = env ???????????? 4. 把栈上的n个元素赋值到c->upvalue[]数组中,顺序是越先入栈的值放在upvalue数组的越开始位置,c->nupvalues指定改闭包upvalue的个数.? ---------- 闭包.upvalue = upvalue ???????????? 5. 弹出栈上n个元素,并压入新建的Closure到栈顶. ?????? 整个流程是比较简单的,分配内存,填写属性,链入gc监控,绑定c函数,绑定upvalue,绑定env一个C闭包就ok了,请结合上面给的闭包的解释,很清楚了. ? 现在来解析这个C闭包被调用的过程[注意,这里只涉及C闭包的调用]? ?????? lua 闭包调用信息结构: ??????????? struct CallInfo { ??????? 从注释就可以看出来,这个结构是比较简单的,它的作用就是维护一个函数调用的有关信息,其实和c函数调用的栈帧是一样的,重要的信息base –> ebp,func –> 要调用的函数的栈index,savedpc –> eip,top,nresults和tailcalls没有明显的对应. ??????? 在lua初始化的时候,分配了一个CallInfo数组,并用L->base_ci指向该数组第一个元素,用L->end_ci指向该数组最后一个指针,用L->size_ci记录数组当前的大小,L->ci记录的是当前被调用的闭包的调用信息. ? ??????? 下面讲解一个c闭包的调用的过程: ??????? 情景: c 函数 int lua_test(lua_State* L){ ????????????????????????? int a = lua_tonumber(L,1); ????????????????????????? int b = lua_tonumber(L,2); ????????????????????????? a = a + b; ????????????????????????? lua_pushnumber(L,a); ???????????????? } ???????????????? 已经注册到了lua 中,形成了一个C闭包,起名为"test",下面去调用它 ???????????????? luaL_dostring(L,"c = test(3,4)") ? ???????? 1. 首先,我们把它翻译成对应的c api ???????????? ??????????????????????????????? 1. 最初的堆栈 ? ?????????????? lua_getglobal(L,“test”) ?????????????? lua_pushnumber(L,3) ?????????????? lua_pushnumber(L,4)?????? ???????????? ????????????????????????????? 2. 压入了函数和参数的堆栈 ? ?????????????? lua_call(L,2,1) ???????????? ?????????????????????????????? 3. 调用lua_test开始时的堆栈 ? ???????????? ?????????????????????????????? 4. 调用结束的堆栈 ? ?????????????? lua_setglobal(L,“c”)????? ???????????? ?????????????????????????????? 5. 取出调用结果的堆栈 ? ??????? 我们重点想要知道的是lua_call函数的过程 ??????????? 1. lua的一致性在这里再一次的让人震撼,不管是dostring,还是dofile,都会形成一个闭包,也就是说,闭包是lua中用来组织结构的基本构件,这个特点使得lua中的结构具有一致性,是一种简明而强大的概念. ??????????? 2. 根据1, a = test(3,4)其实是被组织成为一个闭包放在lua栈顶[方便期间,给这个lua闭包起名为bb],也就说dostring真正调用的是bb闭包,然后bb闭包执行时才调用的是test ? ???????? [保存当前信息到当前函数的CallInfo中] ??????????? 3. 在调用test的时刻,L->ci记载着bb闭包的调用信息,所以,先把下一个要执行的指令放在L->ci->savedpc中,以供从test返回后继续执行. ??????????? 4. 取栈上的test C闭包 cl,用 cl->isC == 1断定它的确是一个C闭包 ? ???????? [进入一个新的CallInfo,布置堆栈] ??????????? 5. 从L中新分配一个CallInfo ci来记录test的调用信息,并把它的值设置到L->ci,这表明一个新的函数调用开始了,这里还要指定test在栈中的位置,L->base = ci->base = ci->func+1,注意,这几个赋值很重要,导致的堆栈状态由图2转化到图3,从图中可以看出,L->base指向了第一个参数,ci->base也指向了第一个参数,所以在test中,我们调用lua_gettop函数返回的值就是2, 因为在调用它的时候,它的栈帧上只有2个元素,实现了lua向c语言中传参数. ? ??????? [调用实际的函数] ??????????? 6. 安排好堆栈,下面就是根据L->ci->func指向的栈上的闭包(及test的C闭包),找到对应的cl->c->f,并调用,就进入了c函数lua_test ? ??????? [获取返回值调整堆栈,返回原来的CallInfo] ??????????? 7. 根据lua_test的返回值,把test闭包和参数弹出栈,并把返回值压入并调整L->top ??????????? 8. 恢复 L->base,L->ci 和 L->savedpc,继续执行. ??????? 总结: 调用一个新的闭包时 1. 保存当前信息到当前函数的CallInfo中 2. 进入一个新的CallInfo,布置堆栈? 3. 调用实际的函数? 4. 获取返回值调整堆栈,返回原来的CallInfo ? ? 1. 创建lua虚拟机lua_State *lua_newstate (lua_Alloc f,void *ud) 创建一个新的独立的lua虚拟机. 参数指定了内存分配策略及其参数,让用户可以定制内存分配策略是十分有用的,比如在游戏服务器端使用lua,我做过一次统记lua在运行的时候会大量的分配大小小于128字节的内存块,在这样的环境下,使用lua原生的分配器就不太适合了,还好在服务器端,我们往往已经实现了memory pool,这时只需要写一个符合 lua_Alloc 原型的适配器,然后指定为lua的内存分配器就可以了,很灵活. 从lua的设计层面来说,lua只是内存分配器的用户,它只使用一个简单的接口来分配内存,而不去实现如何分配,毕竟内存分配不在lua的功能范围内,这样使的lua变的更加紧凑,它只是专注于实现lua本身,而不需要去关注内存分配策略这样的和lua本身无关的东西. 其实学习lua源代码不光是为了更好的掌握lua,也是为了学习lua中的体现出来的一些编程思想,lua是一个高度的一致性的,优雅的软件作品 失败返回null,多是因为内存分配失败了 该函数会创建栈 从该函数学习到的东西:? 1. 当你制作一个功能时,最好是理清该功能的核心概念和需求,然后去实现他们,功能要模块化,核心概念之间应该是概念一致的,联系紧密的[谈何容易,只能是尽可能的,随时提醒自己要有这样的想法]. ??????????????????????????????? 2. 不要因为功能的实现问题而将一个非该功能核心概念的东西加进来,反之应该把这些东西抽象化作为用户可配置的形式.[在实现时很容易发生"要用到某个功能了,就是实现它"这样的情况,这样并不好]就比如lua,它的核心概念就是lua虚拟机,而内存分配只是在实现lua虚拟机的过程中的要用到的一种东西,但它本身不在lua的核心概念里面,所以把它暴露出来,让用户自己去定制. ??????????????????????????????? 再说下去就是: 除了系统最核心的功能,其他的东西能用插件的形式暴露给用户,使其可配置可扩展. ? 关于这个函数,还要做更多的解释,比如我们看到的lua的绝大多数api的第一个参数都是lua_State* L,而这个L就是lua_newstate制造出来的,那么在分析源码的时候,当然要去看看lua_newstate到底是干了些什么,lua_State的结构又是什么,要了解这些内容,需要知道lua的内部组织结构,下面是一张很概括但能反映其结构的图 ? ? 可以看出来,在一个独立的lua虚拟机里,global_State是一个全局的结构,而lua_State可以有多个 值得说明的是,当调用lua_newstate的时候,主要的工作就是1. 创建和初始化global_State 2. 创建一个lua_State,下面来详细的讲解global_State的内容和作用. ? global_State一个lua虚拟机中只有一个,它管理着lua中全局唯一的信息,主要是以下功能 1. 内存分配策略及其参数,在调用lua_newstate的时候配置它们. 也可以通过lua_getallocf和lua_setallocf随时获取和修改它 2. 字符串的hashtable,lua中所有的字符串都会在该hashtable中注册. 3. gc相关的信息. 内存使用统计量. 4. panic,当无保护调用发生时,会调用该函数,默认是null,可以通过lua_atpanic配置. 5. 注册表,注册表是一个全局唯一的table. 6. 记录lua中元方法名称 和 基本类型的元表[注意,lua中table和userdata每个实例可以拥有自己的独特的元表--记录在table和userdata的mt字段,其他类型是每个类型共享一个元表--就是记录在这里]. 7. upvalue链表. 8. 主lua_State,一个lua虚拟机中,可以有多个lua_State,lua_newstate会创建出一个lua_State,并邦定到global_state的主lua_State上. global_State主要是管理lua虚拟机的全局环境. ? lua_State1. 要注意的是,和nil,string,table一样,lua_State也是lua中的一种基本类型,lua中的表示是TValue {value = lua_State,tt = LUA_TTHREAD} 2. lua_State的成员和功能 ??? a. 栈的管理,包括管理整个栈和当前函数使用的栈的情况. ??? b. CallInfo的管理,包括管理整个CallInfo数组和当前函数的CallInfo. ??? c. hook相关的,包括hookmask,hookcount,hook函数等. ??? d. 全局表l_gt,注意这个变量的命名,很好的表现了它其实只是在本lua_State范围内是全局唯一的的,和注册表不同,注册表是lua虚拟机范围内是全局唯一的. ???? e. gc的一些管理和当前栈中upvalue的管理. ???? f. 错误处理的支持. 3. 从lua_State的成员可以看出来,lua_State最主要的功能就是函数调用以及和c的通信. lua_State主要是管理一个lua虚拟机的执行环境,一个lua虚拟机可以有多个执行环境. ? lua_newstate函数的流程经过上面的分析,可以看出newstate = [new 一个 global_state] + [new 一个 lua_State],现在看一下它的流程,很简单 1. 新建一个global_state和一个lua_State. 2. 初始化,包括给g_s创建注册表,g_s中各个类型的元表的默认值全部置为0. 3. 给l_s创建全局表,预分配l_s的CallInfo和stack空间. 4. 其中涉及到了内存分配统统使用lua_newstate传进来的内存分配器分配. ? ? 2. 创建新lua执行环境lua_State *luaE_newthread (lua_State *L) 创建一个新的lua_State,预分配CallInfo和stack空间,并共享l_gt表,虽然每个lua_State都有自己的l_gt,但是这里是却将新建的lua_State的l_gt都指向主lua_State的l_gt. 注意,lua_State是lua运行的基础[CallInfo]和与c通信的基础[stack],在新的lua_State上操作不会影响到原来的lua_State:),这个是协程实现的基础. 这里顺便提一下协程,这里先引一段lua创始人的话:" 我们不信任基于抢占式内存共享的多线程技术. 在 HOPL 论文中,我们写道: "我们仍然认为,如果在连 a=a+1 都没有确定结果的语言中,无人可以写出正确的程序." 我们可以通过去掉抢占式这一点,或是不共享内存,就可以回避这个问题."协程的基础就是"去掉抢占式,但共享内存",这里的共享是在lua虚拟机的层面上的,而不是通常意义上的share memory,这里的共享内存直接就指的是不同线程[lua_State]之间,共享lua_State.l_gt全局表,全局表可以作为不同协程之间的通信环境,当然也可以用lua_xmove函数,协程的事先说到这里. ? 一个和多lua_State相关的函数是: 在同一个lua虚拟机里传递不同lua_State的值 void lua_xmove (lua_State *from,lua_State *to,int n) 把from栈上的前n个值弹出,并压入到to栈中. (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |