博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
lua虚拟机--lua函数调用
阅读量:4166 次
发布时间:2019-05-26

本文共 3162 字,大约阅读时间需要 10 分钟。

前言

本文介绍lua虚拟机中是如何实现lua函数调用的,不涉及C函数的调用。下面通过栈帧结构以及lua调用栈组织形式来了解lua虚拟机中函数调用的过程。

数据栈
函数调用字节码

对以下的lua函数调用代码:

function wen(a,b)    print(a,b)enda = 1b = 2wen(a,b)

lua编译器会生成如下形式字节码:

CLOSURE     0 0 ; 0xb3c630    SETTABUP    0 -1 0  ; _ENV "wen"    SETTABUP    0 -2 -3 ; _ENV "a" 1    SETTABUP    0 -4 -5 ; _ENV "b" 2    GETTABUP    0 0 -1  ; _ENV "wen"    GETTABUP    1 0 -2  ; _ENV "a"    GETTABUP    2 0 -4  ; _ENV "b"    CALL        0 3 1    RETURN      0 1

这里忽略了wen函数对应的字节码。

可以看到,在执行CALL指令之前,lua会执行以下的三条指令做准备:

GETTABUP    0 0 -1  ; _ENV "wen"    GETTABUP    1 0 -2  ; _ENV "a"    GETTABUP    2 0 -4  ; _ENV "b"

这三条指令分别是将wen对应的闭包结构放入R(0)中,然后将全局变量a和b的值分别放入R(1),R(2)中。其中R(K)表示base+K,亦即当前栈帧中索引为K的内存位置。

下面我们看一下调用CALL指令后,lua是如何构建新函数的栈帧的。

CALL指令

CALL指令具有如下形式:

CALL A B C

可以用伪代码表示为:

R(A), ... ,R(A+C-2) := R(A)(R(A+1), ... ,R(A+B-1))

也就是,R(A)存储函数的Closure结构,R(A)以上存储函数参数,参数个数为B-1。函数的返回结果放在R(A)以及R(A)往上的位置,返回个数为C-1。这里为了节约内存,返回参数覆盖了原来CLosure以及函数参数的位置。我们这里先不关心函数的返回。

下面看一下虚拟机中的实现。

虚拟机实现

CALL指令的实现位于OP_CALL分支:

vmcase(OP_CALL,

虚拟机首先根据指令内容获取函数的返回参数个数,以及根据函数参数调整当前栈结构:

int b = GETARG_B(i);int nresults = GETARG_C(i) - 1;if (b != 0) L->top = ra+b;  /* else previous instruction set top */

这里L是一个lua_state变量,它维护一个lua执行流的所有状态,其中L->stack维护该执行流的栈基地址,L->top指向栈中第一个可用的位置。这里根据已经放入栈中的函数参数调整L->top指针,使他始终指向栈的可用位置,避免覆盖掉函数参数。

下面调用luaD_precall函数:

if (luaD_precall(L, ra, nresults)) {  /* C function? */   if (nresults >= 0) L->top = ci->top;  /* adjust results */       base = ci->u.l.base;}

该函数将会构建栈帧,同时判断所调用函数类型。这里我们只关注lua函数栈帧的构建过程。

下面进入该函数:

int luaD_precall (lua_State *L, StkId func, int nresults) {  lua_CFunction f;  CallInfo *ci;  int n;  /* number of arguments (Lua) or returns (C) */  ptrdiff_t funcr = savestack(L, func);

参数中,func指向栈中存放要调用的函数闭包的位置。

CallInfo是一个维护被调用闭包的相关状态的结构,包括被调用闭包的栈帧在L->stack中的位置区间,闭包的执行字节码等等。

下面是lua函数栈帧构建的核心逻辑:

case LUA_TLCL: {  /* Lua function: prepare its call */

首先是根据函数闭包获取函数的元信息:

Proto *p = clLvalue(func)->p;

Proto 中包含了在编译期得到的函数信息,包括,该函数最大需要使用的栈空间(maxstacksize),函数的形式参数个数等等(numparams)。lua函数支持实参数目小于形参数目的调用,其他没有赋值的形参将默认为nil:

for (; n < p->numparams; n++)   setnilvalue(L->top++);

在定长参数情况下,lua将根据已经完整填入函数参数后的栈指针来设置栈帧位置:

if (!p->is_vararg) {        func = restorestack(L, funcr);        base = func + 1;}

base就是这个将被调用的函数的栈帧的基地址。当然,函数执行过程中除了参数需要用到栈空间外,局部变量也需要存储在栈中,因此L->top还需要进行进一步的调节。主要是根据proto的maxstacksize来调整,保证该函数只使用L->top以下,L->base以上的栈空间。

以下就是构建之后的栈帧结构示意图:

这里写图片描述

CallInfo维护了这个即将调用的函数的所有信息。

调用栈

在C函数调用过程中,会在执行被调用的函数前将调用函数的执行地址放入栈中,这样被调用的函数返回时才可以继续原来的位置执行。

但是从上面分析lua函数调用过程中发现,lua栈中并没有任何涉及调用的主函数的信息,存储的都是参数,局部变量等信息。这样的话,在被调用函数结束时,如何回到原来的函数执行位置呢?

lua实现机制是将数据栈和调用栈进行分离。前面说过CallInfo维护了被调用函数的所有信息,因此借助这个结构就可以返回到原来的函数中。基于此,lua执行流状态机lua_state维护了一个CallInfo链表,其中L->ci始终指向当前执行的函数对应的CallInfo结构。

每次调用一个函数都会在执行链表中添加一个CallInfo项:

ci = next_ci(L);

这个宏会将新CallInfo放到链表末端,因此当下次执行到这个函数的RETURN返回语句时,只需要执行L->ci = L->ci->previous就可以返回到调用函数中。

以上所有预备工作完成之后,就可以执行这个新的CallInfo了:

else {  /* Lua function */      ci = L->ci;      ci->callstatus |= CIST_REENTRY;//调用的是lua函数      goto newframe;  /* restart luaV_execute over new Lua function */        }

可以看到,只需要将ci指向L->ci即可(L->ci在luaD_precall中已经被更新过了)。

总结

阿根廷队惊险晋级真是太让人开心了,煤球王终于还是有机会用自己的天才成为国家英雄获得无上荣耀。所以说,什么时候都不能放弃,即使只剩下十分钟,也要拼尽全力。愿你的明天充满光辉。

你可能感兴趣的文章
android中SharedPreferences的简单例子
查看>>
android中使用TextView来显示某个网址的内容,使用<ScrollView>来生成下拉列表框
查看>>
andorid里关于wifi的分析
查看>>
Spring MVC和Struts2的比较
查看>>
Hibernate和IBatis对比
查看>>
Spring MVC 教程,快速入门,深入分析
查看>>
Android 的source (需安装 git repo)
查看>>
LOCAL_PRELINK_MODULE和prelink-linux-arm.map
查看>>
Ubuntu Navicat for MySQL安装以及破解方案
查看>>
Class.forName( )你搞懂了吗?——转
查看>>
jarFile
查看>>
EJB与JAVA BEAN_J2EE的异步消息机制
查看>>
数学等于号是=那三个横杠是什么符
查看>>
HTTP协议详解
查看>>
java多线程中的join方法详解
查看>>
idea添加gradle模块报错The project is already registered
查看>>
在C++中如何实现模板函数的外部调用
查看>>
HTML5学习之——HTML 5 拖放
查看>>
HTML5学习之——HTML 5 Canvas vs. SVG
查看>>
HTML5学习之——HTML 5 应用程序缓存
查看>>