# Lua ## 初识 Lua ### 循环 与 迭代 #### 普通循环 ```lua for i = 0, 7, 2 do print(i) end ``` > 上述代码的输出是:0,2,4,6 普通循环 写法,定义 i 的初始值为 0,第二个 100 为 i 的终值,第三个 2 为每次循环的步长 #### 闭包函数迭代器 ```lua function list_iter (t) local i = 0 local n = table.getn(t) return function () i = i + 1 if i <= n then return t[i] end end end t = {10, 20, 30} for element in list_iter(t) do print(element) end ``` 关于上面的代码,虽然 `list_iter` 返回的是函数,但是对于这种情况来说,lua 的执行逻辑是:每次循环时都执行一次函数,直到函数的返回值为 `nil` 为止 由于闭包的存在,`list_iter`` 函数中的局部变量 i 并不会被释放,因为仍有函数对象持有该对象,知道函数对象被释放的时候才会释放,因此闭包每次执行 i 都会加一,直到遍历完整个 table 之后,循环结束,闭包函数对象被释放 闭包函数迭代器支持状态传递 ```lua local _var = 10; local _state = 2; function list_iter (t) local i = 0 local n = #t return function (state, var) print("state = " .. state .. " var = " .. var) i = i + 1 if i <= n then return t[i] end end, _state, _var end local t = {4, 5, 6} for index in list_iter(t) do print(index) end ``` 输出内容是 ```bash state = 2 var = 10 4 state = 2 var = 4 5 state = 2 var = 5 6 state = 2 var = 6 ``` 执行逻辑大概是 iter, index, state = list_iter(t),得到 index = 10, state = 2 每次循环时执行 index = iter(state , index) 之后判断 index 是否是 nil,进而判断是执行循环体 还是 跳出循环 #### lua 提供的 pairs 和 ipairs ipairs函数专门用于遍历数组(即键是从1开始的连续整数的表)。它会按照1,2,3...的顺序遍历表,直到遇到nil值就停止 ```lua local t = { [2] = "Tue", [3] = "Wet", ["Mon"] = 1, ["Tue"] = 2, ["Wet"] = 3 } print("ipairs -------------- ") for k, v in ipairs(t) do print(k .. " " .. v) end print("pairs -------------- ") for k, v in pairs(t) do print(k .. " " .. v) end ``` 输出内容如下 ```bash ipairs -------------- pairs -------------- 2 Tue 3 Wet Tue 2 Mon 1 Wet 3 ``` 注意,使用 ipairs 并没有执行循环体,因为 ipairs 是从 1 开始查找,按照 1、2、3 ... 进行索引遍历,由于 t 没有索引为 1 的 index,循环在一开始就结束了 ### 携程 ```cpp -- 创建协程 co = coroutine.create(function() -- 协程函数体 print("协程开始") coroutine.yield("暂停点") -- 挂起协程 print("协程恢复") return "完成" end) -- 恢复执行 -- 第一次恢复 status = true value = 暂停点 status, value = coroutine.resume(co) print(((status and "TRUE") or "FALSE") .. " " .. value) -- 第二次恢复 status = true value = 完成 status, value = coroutine.resume(co) print(((status and "TRUE") or "FALSE") .. " " .. value) -- 第三次恢复 status = false value = cannot resume dead coroutine status, value = coroutine.resume(co) print(((status and "TRUE") or "FALSE") .. " " .. value) -- 其他API -- coroutine.status(co) -- 获取协程状态:running, suspended, dead, normal -- coroutine.wrap() -- 创建协程并返回函数包装器 -- coroutine.yield() -- 挂起当前正在执行的协程 ``` ### Metatables 和 Metamethods Metatables 允许改变 table 的行为,比如两个 table 相加,默认情况下是错误的,但是通过 setmetatble 设置 metatable 并实现 __add 函数即可实现两个 table 的相加操作 lua 提供以下几种 metamethods - 访问控制: - `__index(t, k)` 读 `t[k]` 且原键为 `nil` 时调用 - `__newindex(t, k, v)` 写 `t[k] = v` 且原键为 `nil` 时调用 - `__metatable`(保护) - `__mode`(弱值示例) - 可调用与字符串: - `__call`(构造/克隆) - `__tostring(t)` 字符串上下文时触发 - 序列与拼接: - `__len(a)` 在 `#t` 时触发 - `__concat(a, b)` 通常 `a .. b` 时触发 - 算术: - `__unm(a)` 返回同类型结果,对应 -a - `__add(a, b)`(加) - `__sub(a, b)`(减) - `__mul(a, b)`(乘) - `__div(a, b)`(除) - `__idiv(a, b)` - `__mod(a, b)`(余) - `__pow(a, b)`(幂) - 位运算(Lua 5.3+): - `__band` - `__bor` - `__bxor` - `__bnot` - `__shl` - `__shr` - 比较: - `__eq(a, b)`(相等) - `__lt(a, b)`(小于) - `__le(a, b)`(小于等于) - 迭代器(Lua 5.3 支持): - `__pairs` - `__ipairs` ```lua -- Lua -- 基础结构 local ST = {} ST.v = 10 -- 元表 local MT = {} -- 辅助:类型判定与取值/构造 local function isST(x) return type(x) == "table" and getmetatable(x) == MT end local function val(x) local tx = type(x) if tx == "number" then return x end if tx == "table" and isST(x) then return rawget(x, "v") end error("非法操作数:" .. tostring(x)) end local function new(v) return setmetatable({ v = v }, MT) end -- 访问控制 function MT.__index(self, k) if k == "val" then -- 别名:读取 self.val 等同 self.v return rawget(self, "v") end return rawget(self, k) -- 其他键按原值(nil 时返回 nil) end function MT.__newindex(self, k, v) if k == "v" then -- 仅允许写 v rawset(self, "v", v) else error(("只读字段:%s"):format(tostring(k))) end end -- 可调用:克隆或重置 v,返回新实例 function MT.__call(self, nv) if nv == nil then nv = rawget(self, "v") end return new(nv) end -- tostring 与拼接 function MT.__tostring(self) return ("ST(v=%s)"):format(tostring(rawget(self, "v"))) end function MT.__concat(a, b) local function S(x) return isST(x) and tostring(x) or tostring(x) end return S(a) .. S(b) end -- 长度:返回 v(整数化由调用方决定) function MT.__len(self) return rawget(self, "v") end -- 算术 function MT.__unm(a) return new(-val(a)) end function MT.__add(a, b) return new(val(a) + val(b)) end function MT.__sub(a, b) return new(val(a) - val(b)) end function MT.__mul(a, b) return new(val(a) * val(b)) end function MT.__div(a, b) return new(val(a) / val(b)) end function MT.__idiv(a, b) return new(val(a) // val(b)) end function MT.__mod(a, b) return new(val(a) % val(b)) end function MT.__pow(a, b) return new(val(a) ^ val(b)) end -- 按位(Lua 5.3+) function MT.__band(a, b) return new(val(a) & val(b)) end function MT.__bor(a, b) return new(val(a) | val(b)) end function MT.__bxor(a, b) return new(val(a) ~ val(b)) end function MT.__bnot(a) return new(~val(a)) end function MT.__shl(a, b) return new(val(a) << val(b)) end function MT.__shr(a, b) return new(val(a) >> val(b)) end -- 比较 function MT.__eq(a, b) return val(a) == val(b) end function MT.__lt(a, b) return val(a) < val(b) end function MT.__le(a, b) return val(a) <= val(b) end -- 迭代(Lua 5.2/5.3 支持) function MT.__pairs(self) return next, self, nil end function MT.__ipairs(self) local function iter(_, i) i = i + 1 local v = rawget(self, "v") or 0 if i <= v then return i, i end end return iter, self, 0 end -- 资源关闭(Lua 5.4) function MT.__close(self, err) rawset(self, "_closed", true) end -- 保护与命名 -- MT.__metatable = "locked" MT.__name = "ST" -- MT.__mode = "v" -- 可选:弱值 -- 绑定元表到 ST(让 ST 自身成为实例) setmetatable(ST, MT) ``` 测试代码输出 ```bash local st1 = new(10); local st2 = new(20); print(getmetatable(st1)) print(getmetatable(st2)) print(st1 + st2) print(st1 - st2) print(st1 * st2) print(st1 / st2) print(st1 & st2) ``` ### 环境 #### 全局变量 environment 为了简化操作,Lua 将环境本身存储在一个全局变量 `_G` 中 > _G._G 等价于 _G ```lua for n in pairs(_G) do print(n) end print(_G == _G._G) ``` `_G` 本身也是一个 `table`,可以像操作 `table` 一样操作它 ```lua local declaredNames = {} function declare (name, initval) rawset(_G, name, initval) declaredNames[name] = true end setmetatable(_G, { __newindex = function (t, n, v) if not declaredNames[n] then error("attempt to write to undeclared var. "..n, 2) else rawset(t, n, v) -- do the actual set end end, __index = function (_, n) if not declaredNames[n] then error("attempt to read undeclared var. "..n, 2) else return nil end end, }) ``` 像上面这样,给 `_G` 设置 `metatable`,当想要获取空对象是提示错误 > `rawset` 可以不触发 `metamethod` 来给 `table` 设置值 #### 非全局的环境 全局环境的问题就是一个小小的修改,可能会影响所有的程序 Lua 5.0 之后提供 setfenv 函数来改变一个函数的环境 setfenv 接受函数和新的环境作为参数,还接受一个数字表示栈顶的活动函数,比如 1 表示当前函数,2 表示调用当前函数的函数 ```lua a = 1 setfenv(1, {_G = _G}) -- 此时修改环境 全局变量中有 a 但是局部变量中没有 a 了 _G.print(a) --> nil 环境修改,当前函数中没有局部变量 a _G.print(_G.a) --> 1 ``` 不过 setfenv 和 getfenv 在 Lua 5.2 版本之后被 移除 这里提供了两种替代方案 1. 使用局部 `_ENV` 重新绑定当前代码块环境 2. 使用 `load(chunk, chunkname, mode, env)` 在指定环境中执行 字符串/文件 ```lua a = 1 -- 仍在默认全局环境里 do local _ENV = { _G = _G } -- 将当前块的环境改为仅含 _G 的表 _G.print(a) -- => nil(因为当前块环境里没有 a) _G.print(_G.a) -- => 1(从真正的全局表里读到 a) end ``` > 使用 _ENV 临时修改执行环境 ```lua a = 1 local env = { _G = _G } -- 自定义环境(不含 a) local chunk = [[ _G.print(a) -- => nil _G.print(_G.a) -- => 1 ]] local f = load(chunk, "demo", "t", env) f() ``` > 使用 load 让代码在指定环境中运行 ### 面向对象 ```lua local St = {v = 10} function St.printf(self) print(self.v) end St.printf(St); ``` 类似 `printf` 这样每次调用都要把自己这个对象传入的写法,非常麻烦,而且一点也不 `OOP`,所以 lua 提供 **:** 来代替 . ```lua local St = { v = 10, printf2 = function(self) print(self.v); end } function St:printf() print(self.v); end St:printf(); St.printf2(St); ``` > 使用 **:** 来 定义 函数 和 调用 函数 #### 类 参考 JS 中实现 class 的方案,使用原型链的方式来实现继承 ```cpp local St = { v = 10 } function St:printf() print(self.v); end function St:Add(val) self.v = self.v + val; end function St:new (o) o = o or {} -- create object if user does not provide one setmetatable(o, self) self.__index = self return o end local s1 = St:new() s1:Add(100); print(s1.v); print(St.v); ``` - 由于 `s1` 没有名为 `Add` 的函数,会通过 `metatable` 查找,进而使用 `St.Add` 函数 - 在调用 `Add` 函数时,由于 `s1` 没有 `v` 属性,会通过 `metatable` 找到 `St.v`,并通过 `self. v = self.v + val` 语句给 `s1` 添加 `v` 属性,并赋值 > 由于赋值时是给 `s1` 赋值,所以并没有修改 `St.v` 的值 #### 继承 ```lua local StSon = St:new(); function StSon:Mul(val) self.v = self.v * val; end function StSon:new(o) o = o or {} setmetatable(o, StSon) self.__index = self return o end ``` 用例 ```lua local sts1 = StSon:new() sts1:Add(100); sts1:Mul(2); sts1:printf() ``` ### Weak 表 由于 lua 有自己的垃圾回收机制,大部分情况下不用关心对象的 GC `Weak` 表的作用是告诉 GC 一个引用不应该阻止该对象被回收,它是个弱引用,可以被 GC 掉 如何定义一个 `table` 是 `weak table` 呢? - `metatable` 的 `__mode` 字符串包含小写的 `k` 表示这个 `table` 的 `keys` 都是 `weak` 的 - `metatable` 的 `__mode` 字符串包含小写的 `v` 表示这个 `table` 的 `values` 都是 `weak` 的 ```lua a = {} b = {} setmetatable(a, b) b.__mode = "k" -- 现在 table a 的 keys 都是 weak 的 key = { a = 1} -- 创建 key a[key] = 1 key = { b = 2} -- 重新赋值 key a[key] = 2 collectgarbage() -- 强制垃圾回收 for k, v in pairs(a) do print(v) end --> 2 ``` 上述代码,此时只会输出 2 ,不会输出 1 和 2 这是因为 `table` `a` 的 `keys` 是 `weak` 的,`{ a = 1}` 没有对象引用,被 GC 掉了;`{ b = } `被 `key` 引用,因此没有被 GC ## Lua 源码 从 C++ 中启动 Lua 虚拟机并指定对应的 lua 文件的基本流程如下 ```cpp #include #include int main() { lua_State* L = luaL_newstate(); // 使用luaL_newstate()代替lua_open() // 加载标准库 luaL_openlibs(L); // 执行Lua文件 luaL_dofile(L, "test/test.lua"); lua_close(L); return 0; } ``` 1. 通过 `luaL_newstate` 创建全局状态对象 `global_State` 和主线程 `lua_State`、初始化堆栈、字符串表、GC、调用接口元数、创建注册表等 2. 通过 `luaL_opemlibs` 遍历 `linit.c`` 中的库列表,依次执行 `luaL_requiref` 并将函数注册到库表中,将库表复制到全局 `_G[name]` 3. 调用 `luaL_doFile` 来执行指定 lua 文件 ### 常用结构体 #### global_State `global_State` 是一个 Lua 虚拟机实例的 **全局运行时** ,被所有线程 `lua_State` 共享;负责内存分配、字符串驻留、GC(增量/分代)、类型默认元表、注册表、panic/warn、主线程与打开 `upvalue` 的线程链等 绝大多数字段仅内部使用;开发者通过公开 API(`lua_newstate`、`lua_gc`、`lua_setwarnf`、`lua_atpanic`、`LUA_REGISTRYINDEX`、`debug.*` 等)间接影响或观察其效果 | 属性名 | 作用 | | --- | --- | | `frealloc/ud` | 自定义分配器及其上下文;影响所有对象分配/释放 | | `strt` | 字符串驻留哈希表 | | `l_registry` | 注册表(全局)表,跨库共享状态 | | `nilvalue` | 单例 nil 值 | | `seed` | 哈希随机种子,抵御恶意碰撞 | | `twups` | 含“打开的 upvalue”的线程链。示例:主线程创建闭包,协程使用该闭包,线程将被挂到 twups,确保 upvalue 生命周期正确 | | `panic` | 未受保护错误时的回调(终止前调用)。例如 `lua_atpanic`` 注册,避免直接触发以免中止进程 | | `mainthread` | 主线程指针 | | `memerrmsg` | 分配内存失败时的错误消息字符串 | | `tmname[TM_N]` | 内置元方法名称表,用于将枚举 TM_* 映射到字符串名 | | `mt[LUA_NUMTYPES]` | 各基础类型的默认元表指针数组(nil/boolean/lightuserdata/number/string/table/function/userdata/thread) | | `strcache[STRCACHE_N][STRCACHE_M]` | API 层的短字符串缓存(最近/热路径缓存),加速 lua_pushstring 等重复驻留的查找 | | `warnf/ud_warn` | 警告回调及其上下文,lua_setwarnf(L, mywarn, ud) 注册;在解析/运行期间调用 lua_warning(L, "message", tocont) 触发非致命提示(如弃用特性或潜在问题) | 除了上述这些属性之外,剩下的属性几乎都是跟 GC 相关的 | GC 相关的属性 | 作用 | | --- | --- | | `currentwhite/gcstate/gckind/gcstopem/gcstp/gcemergency/gcpause/gcstepmul/gcstepsize` | GC 三色标记当前白色集合、GC 状态机阶段、GC 模式(增量/分代)、应急收集阻断、是否暂停 GC、是否处于应急、暂停参数、步进速度与粒度 | | `allgc` | 所有可回收对象总链 | | `sweepgc` | 当前清扫指针 | | `finobj/tobefnz` | 带 __gc 的对象与待执行 finalizer 的队列。示例:创建带 __gc 的 userdata,然后 lua_gc(L, LUA_GCCOLLECT) | | `gray/grayagain` | 增量/原子阶段需遍历的“灰色”对象 | | `weak/ephemeron/allweak` | 弱表(弱值/弱键/全弱)集合。示例:__mode='v'/'k' 的表在 GC 后被清理 | | `fixedgc` | 固定对象(如保留字符串)不被回收 | | `survival/old1/reallyold/firstold1/finobjsur/finobjold1/finobjrold` | 分代 GC 的分龄链表与对应 finalizer 队列 | #### lua_State `lua_State` 表示单个 **线程/协程** 的运行时对象,持有栈、调用帧、错误处理、钩子等 > C API 里的 L 指针就是它 | 属性 | 数据类型 | 作用 | 例子 | | --- | --- | --- | --- | | `CommonHeader` | | GC 标头(类型、标志、颜色)。仅内部使用 | 通过 lua_gc 展示可达性与回收 | | `status` | lu_byte | 线程状态 | lua_status(L) 可得;协程 yield 后为 LUA_YIELD,可用于协程切换 | | `allowhook` | lu_byte | 是否允许触发 hook(内部门闸)。不可直接访问 | 通过 lua_sethook 验证在某些阶段不触发 | | `nci` | unsigned | CallInfo 节点数量(调用深度)。不可直接读取 | 用深度递归观察错误“C stack overflow” | | `top` | StkIdRel | 栈顶第一个空槽 | push/pop 后记录可见索引与有效值边界 | | `l_G` | global_State | 指向 global_State。不可直接访问 | 通过 LUA_REGISTRYINDEX 操作注册表以体现全局状态 | | `ci` | CallInfo | 当前调用帧信息。不可直接访问 | 用 debug.getinfo 观察当前函数、栈层级 | | `stack_last` | StkIdRel | 栈尾(最后元素 + 1) | lua_checkstack 扩栈与失败处理 | | `stack` | StkIdRel | 栈基址(当前帧底) | 正负索引读取、验证越界行为 | | `openupval` | UpVal | 打开的 upvalue 链表 | 闭包捕获局部变量并跨协程使用,展示“打开的 upvalue” | | `tbclist` | StkIdRel | 待关闭变量链表 | Lua 5.4 的 变量与 __close 元方法 | | `gclist` | GCObject | 等待处理的 GC 对象链 | userdata 带 __gc,调用 lua_gc(L, LUA_GCCOLLECT) 观察析构 | | `twups` | lua_State* | 持有打开 upvalue 的线程链 | 主线程与协程共享 upvalue,GC 阶段仍保持活性 | | `errorJmp` | lua_longjmp* | C 层异常恢复点 | lua_pcall 捕获 lua_error | | `base_ci` | CallInfo | 首层 CallInfo(C 调 Lua 的入口帧)。不可直接访问 | 从 C 执行 Lua chunk 观察栈基变化 | | `hook` | lua_Hook | 当前调试钩子函数指针 | lua_sethook 注册并记录事件 | | `errfunc` | ptrdiff_t | 当前错误处理函数(栈索引) | lua_pcall 带消息处理器 | | `nCcalls` | l_uint32 | 嵌套的(不可让步|C)调用计数 | C 函数递归并观察是否阻止 yield | | `oldpc` | int | 最后一次跟踪的 PC(内部) | 指令/行号钩子中观察行为 | | `basehookcount/hookcount/hookmask` | intl_signalT | 钩子计数与掩码 | 设置 LUA_MASKCOUNT/LUA_MASKLINE/LUA_MASKCALL 并记录触发频率 | #### CallInfo ```cpp static int str_find (lua_State *L) { return str_find_aux(L, 1); } static int str_find_aux (lua_State *L, int find) { size_t ls, lp; const char *s = luaL_checklstring(L, 1, &ls); const char *p = luaL_checklstring(L, 2, &lp); // some code else ..... } LUALIB_API const char *luaL_checklstring (lua_State *L, int arg, size_t *len) { const char *s = lua_tolstring(L, arg, len); if (l_unlikely(!s)) tag_error(L, arg, LUA_TSTRING); return s; } LUA_API const char *lua_tolstring (lua_State *L, int idx, size_t *len) { TValue *o; lua_lock(L); o = index2value(L, idx); // some code else ... } static TValue *index2value (lua_State *L, int idx) { CallInfo *ci = L->ci; if (idx > 0) { StkId o = ci->func.p + idx; api_check(L, idx <= ci->top.p - (ci->func.p + 1), "unacceptable index"); if (o >= L->top.p) return &G(L)->nilvalue; else return s2v(o); } // some code else ... } ``` 以 `str_find` 为例,该函数是用来查找匹配字符串的,在 lua 中原型是 `string.find(s, p [, init [, plain]])` - s,subject, 表示要搜索的字符串 - p,pattern,模式/子串 - init, 起始位置,默认为 1,允许使用负数表示相对尾部 - plain,布尔值,true 表示禁用模式语法,做字面量匹配 从 `str_find_aux` 函数中可以发现 - `const char *s = luaL_checklstring(L, 1, &ls)` 这里的 s 就是 subject,即要搜索的字符串,因为它是函数参数栈中的第一个,所以第二个参数传入 1 - `const char *p = luaL_checklstring(L, 2, &lp)` 这里的 p 就是 pattern,即要匹配的子串,因为它是函数参数栈中的第二个,所以第三个参数传入 2 `CallInfo` 是 Lua 虚拟机对 **一次函数调用** 的栈帧描述,链成双向链表形成调用栈;每个活跃函数(Lua/C)对应一个 CallInfo > 这里 **帧** 指的就是一次函数调用的活动记录,说白了就是一次函数调用 `CallInfo` 限制本帧可用栈顶、记录当前执行位置、变参信息、yield/继续执行的上下文、受保护调用的错误处理器、返回值期望、调试钩子传输的值范围,以及各种状态位(Lua/C、tail call、正在关闭 tbc 变量等) | CallInfo 的属性 | 作用 | | --- | --- | | func | 本帧函数在栈中的相对索引(StkIdRel);用于定位函数闭包/closure | | top | 本帧允许使用的栈顶(StkIdRel);避免越界干扰其他帧 | | *previous, *next | 调用链前后指针;形成双向链表。可快速切换当前活动帧、回退返回 | | u.l.savedpc | (仅 lua 函数) 当前字节码指令地址,用于恢复继续执行 | | u.l.trap | (仅 lua 函数) 线路/技术 跟踪标志,用于在执行到下一条指令或计数阈值时强制执行调式狗子,比如 `debug.sethook(h, "l")` | | u.l.nextraargs | (仅 lua 函数) 变参函数的 **额外参数** 数量,比如 `function f(a, ...) end` 此时调用 `f(1, 2, 3, 4)` 那么 `nextraargs` 值为 3 | | u.c.k | (仅 C 函数)继续函数(continuation),用于可让出/可恢复的 C 调用 | | u.c.old_errfunc | (仅 C 函数)受保护调用中的旧错误处理索引,用于在 pcall 链嵌套时恢复前一个错误处理器 | | u.c.ctx | 继续函数的上下文值(lua_KContext,C 指针或整数) | | u2 | 不太懂 | | nresults | 本次调用期望返回值的数量, VM 用它在返回时裁剪或补齐结果,比如 `lua_call(L, f, 1)` 值保留一个返回值,其余丢弃 | | callstatus | 帧状态位的集合(位标志),比如 `CIST_C` 表示这是 C函数、`CIST_TAIL` 表示尾调用 等 | > 属性 u 是一个 union,其包含 `struct l` 表示 lua 函数调用 和 `struct c` 表示 c 函数调用 两个结构体 lua 线程的栈为一段连续的槽位 (`TValue` 数组),所有帧共享同一个物理栈。每个调用帧用 CallInfo 限定自身可用的槽位范围 `CallInfo.top` 表示本帧可使用的最大允许槽位上限,用于限制本帧内的堆栈/寄存器访问 其实在 `lua_State` 中也有一个 `top`,其表示当前线程 **已用栈** 的下一个空位 那么此时回到 `index2value` 函数 > `index2value` 这里 2 就是 two,与 to 同音,译为得到 index 对应的 value ```cpp static TValue *index2value (lua_State *L, int idx) { CallInfo *ci = L->ci; if (idx > 0) { StkId o = ci->func.p + idx; api_check(L, idx <= ci->top.p - (ci->func.p + 1), "unacceptable index"); if (o >= L->top.p) return &G(L)->nilvalue; else return s2v(o); } // some code else } ``` 从 `ci->func.p + idx` 可以看出,`CallInfo.func.p` 表示当前帧函数在栈中的相对索引,那么函数的第一个参数就是 `CallInfo.func.p + 1` > 这里的 `CallInfo.func.p` 是一个 `StackValue *`,对其 `+1` 表示指针向后移动 1 个 `StackValue`,即槽位的下一个 #### Value、TValue、StackValue ##### Value 和 TValue ```cpp typedef union Value { struct GCObject *gc; /* 指向可回收对象(字符串、表、Lua/C 闭包、完整 userdata、线程等) */ void *p; /* 轻量 userdata 的裸指针(不受 GC 管理) */ lua_CFunction f; /* 轻量 C 函数指针(lua_CFunction) */ lua_Integer i; /* 整型数(lua_Integer) */ lua_Number n; /* 浮点数(lua_Number) */ lu_byte ub; /* 未用,占位避免未初始化告警 */ } Value; ``` `Value` 是 `Lua` 内部 **值载体** 的联合体,用于存放具体数据的位表示,但是其真正的类型是什么是由 `TValue` 的 `tt_` 标识符来决定 ```cpp #define TValuefields Value value_; lu_byte tt_ typedef struct TValue { TValuefields; } TValue; ``` 从上面代码可知 `TValue` 包含两个属性,一个是 `value_` 存储具体的值,一个是 `tt_` 表示值类型 ```cpp // ttisstring(o) 判断 TValue o 是否是 string #define ttisstring(o) checktype((o), LUA_TSTRING) #define checktype(o,t) (ttype(o) == (t)) #define ttype(o) (novariant(rawtt(o))) #define novariant(t) ((t) & 0x0F) #define rawtt(o) ((o)->tt_) ``` 上述代码从上往下,本质上就是判断 `o->tt_` 与 `LUA_STRING` 是否相等,以此来判断 `o` 是否是 `string` | 属性宏 | 值 | 对应类型 | | --- | --- | --- | | LUA_TNONE | 0 | 无类型,用于栈索引无效或无返回值位置,非值本身 | | LUA_TNIL | 1 | nil,无值 | | LUA_TBOOLEAN | 2 | 布尔值 | | LUA_TLIGHTUSERDATA | 3 | 轻量 `userdata`;裸 c 指针,不受 GC 管理,对应 `Value.p` | | LUA_TNUMBER | 4 | 数值的外部类型,内部有整数与浮点,对应 `Value.i` 和 `Value.n` | | LUA_TSTRING | 5 | 字符串,GC管理对象,载荷为 Value.gc 指向 `TString` | | LUA_TTABLE | 6 | 表,GC管理对象,载荷为 Value.gc 指向 `Table` | | LUA_TFUNCTION | 7 | 函数的外部类型,内部有三种 lua 闭包 Value.gc 指向 LClosure; C 闭包 Value.gc 指向 CClosure;轻量 C 函数 Value.f (非 GC) | | LUA_TUSERDATA | 8 | 完整 userdata;GC 管理, 载荷为 Value.gc 指向 Udata | | LUA_TTHREAD | 9 | 线程/coroutine;GC 管理,载荷为 value.gc 指向 lua_State | | LUA_NUMTYPES | 10 | 外部可见类型的计数 | 以下面这段代码为例 ```lua local f = function(a) print(a) end ``` 变量 `f` 对应的 `TValue` 的 `tt_` 是 `LUA_TFUNCTION`, `value_.gc` 指向 `LClosure` ```cpp #define CommonHeader struct GCObject *next; lu_byte tt; lu_byte marked typedef struct GCObject { CommonHeader; } GCObject; typedef struct Udata { CommonHeader; // some proprety else ... } Udata; typedef struct TString { CommonHeader; // some property else ... } TString; #define ClosureHeader \ CommonHeader; lu_byte nupvalues; GCObject *gclist typedef struct LClosure { ClosureHeader; // some property else ... } LClosure; ``` 从上述代码不难发现,所有被 GC 管理的对象都有一个 `CommonHeader` 宏定义的属性 这个 `CommonHeader` 的作用是什么?从 `gco2ts` 可以看出作用 ```cpp union GCUnion { GCObject gc; /* common header */ struct TString ts; struct Udata u; union Closure cl; struct Table h; struct Proto p; struct lua_State th; /* thread */ struct UpVal upv; }; #define cast_u(o) cast(union GCUnion *, (o)) #define gco2ts(o) check_exp(novariant((o)->tt) == LUA_TSTRING, &((cast_u(o))->ts)) ``` `GCUnion` 是 **所有可回收对象的联合体** ,用于安全地将具体对象指针重解释为 `GCObject*` 所有可回收对象(`TString`/`Udata`/`Closure`/`Table`/`Proto`/`Thread`/`UpVal`)在结构体起始处都含有 `CommonHeader` 该头部的二进制布局一致 `GCUnion` 把所有这些类型放进一个 `union`;`union` 的所有成员起始地址相同,因此 `&((GCUnion*)ptr)->gc` 与 `(GCObject*)ptr` 等价(指向同一内存起点) | GCObject 的属性 | 作用 | | --- | --- | | next | 把同类对象(或工作队列)串成单向链表,用于 GC/调度 | | tt | 对象外部类型标签(字符串/表/函数/线程/完整 userdata/原型/UpVal 等),供运行时快速判断类型与分派处理 | | marked | GC 标志位(颜色与代龄等),用于增量/分代 GC 的标记-清扫与回收策略 | | ClosureHeader 定义属性 | 作用 | | --- | --- | | CommonHeader | 不做介绍 | | nupvalues | 记录闭包捕获的 `upvalue` 的个数,在 `CClosure` 中决定 `TValue` 数组大小,在 `LClosure` 中决定 `UpVal` 数组大小,在 GC 扫描时依次遍历闭包持有的 `upvalues` | | gclist | 增量/分代 GC 的 **工作队列指针** ,用于把对象临时串到灰色队列 | > `upvalue` 指的是 **被闭包捕获的外层局部变量的绑定(变量单元)** ,是按引用共享的,不是值拷贝 ##### StackValue ```cpp typedef union StackValue { TValue val; struct { TValuefields; unsigned short delta; } tbclist; } StackValue; ``` `StackValue` 是 **栈槽的专用表示** ,在 `TValue` 的基础上,额外为 **待关闭变量**(to-be-closed) 维护一条链 #### TString ```cpp typedef struct TString { CommonHeader; lu_byte extra; // 词法保留字标记(例如 "and"、"local" 等),用于词法分析快速判定;非保留字为 0 lu_byte shrlen; // 短字符串的长度;若为长字符串则置为 0xFF unsigned int hash; // 32 位哈希值;短字符串在创建时计算;长字符串常在需要时延迟计算 union { size_t lnglen; // 长字符串的长度 struct TString *hnext; // 短字符串在全局字符串表中同桶的链表“下一项”指针 } u; char contents[1]; // 变长数据区起点 } TString; ``` 这里的 `contents` 是 **可变尾部数组** 的老派写法。Lua 在创建 TString 时会按长度多分配内存:对象的总大小为 header 大小 + 实际字符串字节数 + 1(用于结尾的 '\0') 也就是说 `contents` 只是变长区域的起始地址,访问不会月结,实际分配的块比结构声明更大 ![](Image/001.png) ```cpp #define sizelstring(l) (offsetof(TString, contents) + ((l) + 1) * sizeof(char)) // 获取真实长度 totalsize = sizelstring(l); // 通过真实长度创建对象 o = luaC_newobj(L, tag, totalsize); ``` lua 将 字符串 区分为 **长字符串** 和 **短字符串**,分别走不同的创建过程 区分一个字符串是长字符串还是短字符串,通过长度比较,大于 `LUAI_MAXSHORTLEN` 的是长字符串 > `LUAI_MAXSHORTLEN` 的值为 40 ```cpp TString *luaS_newlstr (lua_State *L, const char *str, size_t l) { if (l <= LUAI_MAXSHORTLEN) /* short string? */ return internshrstr(L, str, l); else { ts = luaS_createlngstrobj(L, l); // some code else ... return ts; } } ``` 将字符串分为短/长两类,可在常见短字符串上启用驻留(interning)与快速比较,避免为不常复用的长字符串付出昂贵代价 从比较两个 TString 是否相等,可以看出长字符串和段字符串的比较不同 基于 `LUA_TSTRING` 通过位运算,更新出 `LUA_VSHRSTR` 和 `LUA_VLNGSTR` 两个 `tag`,其实也就是 `tt_` ```cpp #define makevariant(t,v) ((t) | ((v) << 4)) #define LUA_VSHRSTR makevariant(LUA_TSTRING, 0) /* short strings */ #define LUA_VLNGSTR makevariant(LUA_TSTRING, 1) /* long strings */ ``` 如果是短字符串,那么比较相同的方法直接比较两个 TString 的地址是否相同 ```cpp #define eqshrstr(a,b) check_exp((a)->tt == LUA_VSHRSTR, (a) == (b)) ``` 如果是长字符串,则通过 `luaS_eqlngstr` 函数进行比较 ```cpp int luaS_eqlngstr (TString *a, TString *b) { size_t len = a->u.lnglen; lua_assert(a->tt == LUA_VLNGSTR && b->tt == LUA_VLNGSTR); return (a == b) || /* 相同的地址 */ ((len == b->u.lnglen) && /* 与判断长度是否相同,因为后面是逐个比较比较费,先提前判断一下 */ (memcmp(getlngstr(a), getlngstr(b), len) == 0)); /* 逐字符串比较 */ } ``` 除了比较的差别之外, 还有一些其他的区别 - `hash` 的计算时机 - 短字符串创建即计算 - 长字符只在需要计算时计算 - GC 策略的不同 - 短字符串驻留、唯一,复用高,减少重复分配与 GC 压力;死短串可在 GC 时从字符串表清理 - 长字符串不驻留,避免字符串表膨胀与插入/查找开销,减少大对象长期保活 对于创建短字符串来说,会先从 `global_State` 中查找是否存在存在 hash 相同的短串,如果存在则更新其 GC 状态,并直接返回 ```cpp global_State *g = G(L); stringtable *tb = &g->strt; TString **list = &tb->hash[lmod(h, tb->size)]; for (ts = *list; ts != NULL; ts = ts->u.hnext) { if (l == ts->shrlen && (memcmp(str, getshrstr(ts), l * sizeof(char)) == 0)) { if (isdead(g, ts)) changewhite(ts); return ts; } } ``` > 为避免 `hash` 冲突的出现,使用 `TString**` 表示 `TString*` 数组,将所有 `hash` 相同的 `TString` 都放在同一个桶中 如果没有找到,则创建一个新的 `TString`,无论是长字符串还是短字符串,创建都是通过 `createstrobj` 函数 ```cpp static TString *createstrobj (lua_State *L, size_t l, int tag, unsigned int h) { TString *ts; GCObject *o; size_t totalsize; /* total size of TString object */ totalsize = sizelstring(l); o = luaC_newobj(L, tag, totalsize); ts = gco2ts(o); ts->hash = h; ts->extra = 0; getstr(ts)[l] = '\0'; /* ending 0 */ return ts; } ``` #### Table ```cpp typedef struct Table { CommonHeader; lu_byte flags; // 1<

flags |= cast_byte(1u<