lua5.3 local局部变量实现
函数原型
我们平时在定义一个 lua 函数时,它内部实现对应的 c 实现是怎么样的,需要用到多少字段来记录一个函数的基本信息呢,接下来,我们看看 lua 函数原型的结构定义:
typedef struct Proto { CommonHeader; // 固定参数的数量 lu_byte numparams; /* number of fixed parameters */ // 是否有可变参数 lu_byte is_vararg; // 该函数需要的栈大小 lu_byte maxstacksize; /* number of registers needed by this function */ // upvalues数量 int sizeupvalues; /* size of 'upvalues' */ // 常量数量 int sizek; /* size of 'k' */ // 指令数量 int sizecode; // 行信息数量 int sizelineinfo; // 内嵌原型数量 int sizep; /* size of 'p' */ // 本地变量的数量 int sizelocvars; // 函数进入的行 int linedefined; /* debug information */ // 函数返回的行 int lastlinedefined; /* debug information */ // 常量数量 TValue *k; /* constants used by the function */ // 指令数组 Instruction *code; /* opcodes */ // 内嵌函数原型 struct Proto **p; /* functions defined inside the function */ // 行信息 int *lineinfo; /* map from opcodes to source lines (debug information) */ // 本地变量信息 LocVar *locvars; /* information about local variables (debug information) */ // Upvalue信息 Upvaldesc *upvalues; /* upvalue information */ // 使用该原型创建的最后闭包(缓存) struct LClosure *cache; /* last-created closure with this prototype */ // 源代码文件 TString *source; /* used for debug information */ // 灰对象列表,最后由g->gray串连起来 GCObject *gclist; } Proto;
局部变量解析阶段
函数的固定参数,可变参数,以及函数体里 local 定义的变量都属于函数局部变量。我们首先简单来了解下 local 定义的局部变量实现,从上面 Proto 结构体中,可以看到,局部变量记录用到的字段有:locvars,sizelocvars
,locvars 存储的是一个 LocVar 的数组,大小为 sizelocvars,主要作用是在调试时,输出堆栈信息。LocVar 对应的结构体如下:
/* ** Description of a local variable for function prototypes ** (used for debug information) */ typedef struct LocVar { // 变量名 TString *varname; // 变量的作用范围开始行号 int startpc; /* first point where variable is active */ // 变量的作用范围结束行号 int endpc; /* first point where variable is dead */ } LocVar;
在解析 lua 文件时,每次解析到一个函数的局部变量时,都会把这个变量添加进 locavars 中。可能大家会有疑问,如果一个函数内,有相同的局部变量, locvars 是存储一次这个变量名呢,还是会存储多个相同的变量名呢,答案是会存储多个相同变量名,我们看 LocVar 定义就知道,除了变量名外,它还要记录变量名的所在作用范围,起始行号,结束行号信息。
我们在 lua 中定义几个变量,简单分析下局部变量是如何解析存储:
local a1 = 12 local a2 = 5 do local b1 = 2 local a1 = 3 print(a1, a2, b1) end local b1 = 6 print(a1, b1) --[[ 输出结果: 3 5 2 12 6 ]]
先定义了两个局部变量 a1, a2,接着在 do...end 代码块里,又对 a1 进行重新定义赋值,但它的可见范围是在 6-7 行。在第 11 行,我们打印 a1 的结果是12,那么这些局部变量在 c 层是如何存储的,lua 又是怎么知道第 11 行要查找的 a1 变量是第 6 行里定义的 a1,还是第1行里定义的 a1 呢。
在解析 lua 代码到 do...end 时,Proto 的 locvars 存储如上图所示,存储了4个变量名以及它们的可见范围。可以看出,每定义一个 local 局部变量,locvars 都会存储起来,不管变量名有没有重复。在 lua 语法定义中,变量的查找,是从当前作用域开始,往代码的前面方向查找,如果找到第一个变量名相同,就表示找到了这个变量。
那当退出 do..end 代码块时,lua 又怎么知道 a1 是查找 locvars 数组里的第一个值,而不是第三个值呢。这就引申出 Dyndata->actvar
结构体了,它负责记录当前 Proto 里所有可见的局部变量的 locvars 数组下标。当解析 lua 文件到第 10 行时,已经退出了do...end作用域。 Dyndata->actvar->arr
数组变化如下:
我们可以看到,第 10 行,定义的 b1 局部变量,也被 locvars 按顺序存储起来了,但并没有因为退出了 do...end 而覆盖最后的 b1,a1,而 Dyndata->actvar->arr
数组发生了缩小,actvar->n
的个数从4个变成了3个。第3个元素存储的值是4,它其实指向了 locvars 里的下标4。那么解析到第10行后的代码,局部变量就只剩下可范围为1到12 行的 a1, a2,b1,它们在 locvars 对应的下标为 0,1,4。
我们还可以通过 luac -l -l xxx.lua 查看局部变量定义的情况,locals(5),这个 5 表示 Proto 存储了5个局部变量,后面变量名后的两行数字,分别表示在指令码code 中的起始行号,和结束行号,它们不是表示在文件中的行号,这个得注意下。
locals (5) for 00000000007a84c0: 0 a1 2 16 1 a2 3 16 2 b1 4 10 3 a1 5 10 4 b1 11 16 // luac.c 局部变量打印实现: static void PrintDebug(const Proto* f) { int i,n; ... n=f->sizelocvars; printf("locals (%d) for %p:\n",n,VOID(f)); for (i=0; i<n; i++) { printf("\t%d\t%s\t%d\t%d\n", i,getstr(f->locvars[i].varname),f->locvars[i].startpc+1,f->locvars[i].endpc+1); } ... }
局部变量运行阶段
在编译解析阶段,lua 函数里的代码,会被解析成一条条指令,存到 Proto->code 中,运行阶段,在 VM 虚拟机里执行指令对应的操作。对于局部变量设置值,读取变量值这些指令,看看 lua 是如何实现的。
local a1 = 12 local a2 = 5 do local b1 = 2 local a1 = 3 print(a1, a2, b1) end local b1 = 6 print(a1, b1) --[[ luac -l -l xxx.lua 0+ params, 8 slots, 1 upvalue, 5 locals, 6 constants, 0 functions 1 [1] LOADK 0 -1 ; 12 2 [2] LOADK 1 -2 ; 5 3 [5] LOADK 2 -3 ; 2 4 [6] LOADK 3 -4 ; 3 5 [7] GETTABUP 4 0 -5 ; _ENV "print" 6 [7] MOVE 5 3 7 [7] MOVE 6 1 8 [7] MOVE 7 2 9 [7] CALL 4 4 1 10 [10] LOADK 2 -6 ; 6 11 [11] GETTABUP 3 0 -5 ; _ENV "print" 12 [11] MOVE 4 0 13 [11] MOVE 5 2 14 [11] CALL 3 3 1 15 [11] RETURN 0 1 locals (5) for 001b7360: 0 a1 2 16 1 a2 3 16 2 b1 4 10 3 a1 5 10 4 b1 11 16 ]]
左边第1列,表示指令在 Proto->code 数组中的位置,为了方便阅读,打印的是数组下标+1,第2列表示指令在源码文件中的行号,第3列表示指令码,第4,第5,第6列(没有用到时,就会没打印出来)表示操作数,;后面是对指令码的简单描述。
通过第2列可以看到,局部变量设置值,只占一条指令。而第7行的 print 打印,占了5条指令。
locals (5) for 001b7360:
学过c语言的,都知道,局部变量是存放在栈上的。同样,我们的 lua 语言也不例外。在上面的指令列表中,我们有一个操作数是专门和栈打交道的,那就是第4列,它专门指向栈中的位置,在代码中叫 ra,第5,第6列的操作数(代码中叫 rb,rc),有可能是来自常量,闭包(后面介绍),或者新创建的 table 等其他地方,通过 rb,rc 操作数来和 ra 指向的栈进行交互,完成一系列指令操作,在 lua 中,c 和 lua 交互,也是要通过栈来传递数据的,栈是数据之间交互的桥梁(这个是重点)。比如把表加载到栈上,进行一些操作后,又放到某个地方。
对于上面的指令列表,可以先简单介绍下,后面再具体分析实现:
1 [1] LOADK 0 -1 ; 12 // 将常量12设置到 ra 指向栈偏移的0号位置,stack[ra+0]=12 2 [2] LOADK 1 -2 ; 5 // 将常量5设置到 ra 指向栈偏移的1号位置,stack[ra+1]=5 3 [5] LOADK 2 -3 ; 2 // 将常量2设置到 ra 指向栈偏移的2号位置,stack[ra+2]=2 4 [6] LOADK 3 -4 ; 3 // 将常量3设置到 ra 指向栈偏移的3号位置,stack[ra+3]=3 5 [7] GETTABUP 4 0 -5 ; _ENV "print" // 从全局表_ENV中获取print函数,并push到栈上,stack[ra+4]=print函数 6 [7] MOVE 5 3 // 把 ra + 3 位置的栈值设置到 ra + 5 的地方,stack[ra+5]=stack[ra+3]=3 7 [7] MOVE 6 1 // 将 ra + 1 位置的栈值设置到 ra + 6 的地方,stack[ra+6]=stack[ra+1]=5 8 [7] MOVE 7 2 // 将 ra + 7 位置的栈值设置到 ra + 2 的地方,stack[ra+7]=stack[ra+2]=2 9 [7] CALL 4 4 1 // 调用stack[ra+4]指向的函数,即print,参数为4-1=3个,返回值1-1=0个,print(3,5,2) 10 [10] LOADK 2 -6 ; 6 // 将常量6设置到 ra 指向栈偏移的2号位置,stack[ra+2]=6,之前的第3行设置的值被覆盖 11 [11] GETTABUP 3 0 -5 ; _ENV "print" // 从全局表_ENV中获取print函数,并push到栈上,stack[ra+3]=print函数 12 [11] MOVE 4 0 // 把 ra + 0 位置的栈值设置到 ra + 4 的地方,stack[ra+4]=stack[ra+0]=12 13 [11] MOVE 5 2 // 把 ra + 2 位置的栈值设置到 ra + 5 的地方,stack[ra+5]=stack[ra+2]=6 14 [11] CALL 3 3 1 // 调用stack[ra+3]指向的函数,即print,参数为3-1=2个,返回值1-1=0个,print(12,6) 15 [11] RETURN 0 1 // 函数返回,0个返回值
对 两个 print 函数打印前后,栈的数据分析如下图:
运行到代码最后,我们可以发现栈上还有三个值,对应着代码中的第一行 a1,第二行 a2,以及第十行 b1 三个局部变量。对于lua而言,整个文件加载时,它也会默认创建一个函数,我们暂时先叫 main 函数,它也会有一个 Proto 原型与之对应,可以理解为如下:
function main() -- xxx.lua 文件开始 local a1 = 12 local a2 = 5 do local b1 = 2 local a1 = 3 print(a1, a2, b1) end local b1 = 6 print(a1, b1) -- xxx.lua 文件结束 end
一般情况下,ra 指向 main 函数的下一个位置,作为第一个局部变量存放的地方,接着后面的局部变量依次存放。这就表明了,每个局部变量都是按顺序存放在栈上。栈上的值都是 TValue 类型,它可以存放任意数值(整数,浮点数,bool,table,字符串等),这就是我们平时为啥可以用任意数值赋值给同一个变量的原因。(至于为啥要说一般情况下,ra 指向 main 函数的下一个位置,那是因为 lua 还有一种可变参数的情况,比如 f(...),ra 就不是指向 main 的下一个位置了,后面再介绍)
小结
在介绍完局部变量在解析阶段和运行阶段做了哪些事情后,应该有了一个大概的了解了。在解析阶段,读取 lua 文件并解析每个字符,解析到的局部变量名存储在 Proto->locvars 中,而对变量的写值,读值,改值等操作,则是保存在指令数组 Proto->code 中,Proto->code 的操作数只记录了局部变量在 Proto->locvars 数组中的索引。在运行阶段,我们也只需要用到局部变量的索引值(对应了在栈中 ra 的偏移),去完成一系列和栈数据相关的操作。还有我们在 lua 代码中,定义了多少个变量,在 Proto->locvars 中也会相应的存储多少个变量名,即使是在函数里的 if...end,do...end,for...end 代码块定义的局部变量,也都会统统记录起来,并且生成对应的指令码。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通