Ingen beskrivning

NiceTry12138 7624eaa98c feat: 添加 C++ 中通过 lua_State 控制 lua 的运行 2 månader sedan
book bfc021c289 feat: 添加 lua pdf 3 månader sedan
image 7264208891 feat: 添加 lua 源码结构体解析 3 månader sedan
src 7264208891 feat: 添加 lua 源码结构体解析 3 månader sedan
.gitignore d7112dfdc3 feat: 添加 lua 源码编译工程 3 månader sedan
C++与Lua的交互.md 7624eaa98c feat: 添加 C++ 中通过 lua_State 控制 lua 的运行 2 månader sedan
README.md 7264208891 feat: 添加 lua 源码结构体解析 3 månader sedan

README.md

Lua

初识 Lua

循环 与 迭代

普通循环

for i = 0, 7, 2 do
    print(i)
end

上述代码的输出是:0,2,4,6

普通循环 写法,定义 i 的初始值为 0,第二个 100 为 i 的终值,第三个 2 为每次循环的步长

闭包函数迭代器

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 之后,循环结束,闭包函数对象被释放

闭包函数迭代器支持状态传递

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

输出内容是

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值就停止

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

输出内容如下

ipairs -------------- 
pairs -------------- 
2 Tue
3 Wet
Tue 2
Mon 1
Wet 3

注意,使用 ipairs 并没有执行循环体,因为 ipairs 是从 1 开始查找,按照 1、2、3 ... 进行索引遍历,由于 t 没有索引为 1 的 index,循环在一开始就结束了

携程

-- 创建协程
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

-- 基础结构
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)

测试代码输出

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

for n in pairs(_G) do 
  print(n) 
end

print(_G == _G._G)

_G 本身也是一个 table,可以像操作 table 一样操作它

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 表示调用当前函数的函数

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) 在指定环境中执行 字符串/文件
a = 1 -- 仍在默认全局环境里 

do 
  local _ENV = { _G = _G } -- 将当前块的环境改为仅含 _G 的表 
  _G.print(a) -- => nil(因为当前块环境里没有 a) 
  _G.print(_G.a) -- => 1(从真正的全局表里读到 a) 
end

使用 _ENV 临时修改执行环境

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 让代码在指定环境中运行

面向对象

local St = {v = 10}
function St.printf(self)
    print(self.v)
end
St.printf(St);

类似 printf 这样每次调用都要把自己这个对象传入的写法,非常麻烦,而且一点也不 OOP,所以 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 的方案,使用原型链的方式来实现继承

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 的值

继承

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

用例

local sts1 = StSon:new()

sts1:Add(100);
sts1:Mul(2);

sts1:printf()

Weak 表

由于 lua 有自己的垃圾回收机制,大部分情况下不用关心对象的 GC

Weak 表的作用是告诉 GC 一个引用不应该阻止该对象被回收,它是个弱引用,可以被 GC 掉

如何定义一个 tableweak table 呢?

  • metatable__mode 字符串包含小写的 k 表示这个 tablekeys 都是 weak
  • metatable__mode 字符串包含小写的 v 表示这个 tablevalues 都是 weak
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 akeysweak 的,{ a = 1} 没有对象引用,被 GC 掉了;{ b = }key 引用,因此没有被 GC

Lua 源码

从 C++ 中启动 Lua 虚拟机并指定对应的 lua 文件的基本流程如下

#include <iostream>
#include <lua.hpp>

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_newstatelua_gclua_setwarnflua_atpanicLUA_REGISTRYINDEXdebug.* 等)间接影响或观察其效果

属性名 作用
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)调用计数
oldpc int 最后一次跟踪的 PC(内部) 指令/行号钩子中观察行为
basehookcount/hookcount/hookmask intl_signalT 钩子计数与掩码 设置 LUA_MASKCOUNT/LUA_MASKLINE/LUA_MASKCALL 并记录触发频率

CallInfo

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

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
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;

ValueLua 内部 值载体 的联合体,用于存放具体数据的位表示,但是其真正的类型是什么是由 TValuett_ 标识符来决定

#define TValuefields	Value value_; lu_byte tt_

typedef struct TValue {
  TValuefields;
} TValue;

从上面代码可知 TValue 包含两个属性,一个是 value_ 存储具体的值,一个是 tt_ 表示值类型

// 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.iValue.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 外部可见类型的计数

以下面这段代码为例

local f = function(a)
  print(a)
end

变量 f 对应的 TValuett_LUA_TFUNCTIONvalue_.gc 指向 LClosure

#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 可以看出作用

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 把所有这些类型放进一个 unionunion 的所有成员起始地址相同,因此 &((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
typedef union StackValue {
  TValue val;
  struct {
    TValuefields;
    unsigned short delta;
  } tbclist;
} StackValue;

StackValue栈槽的专用表示 ,在 TValue 的基础上,额外为 待关闭变量(to-be-closed) 维护一条链

TString

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 只是变长区域的起始地址,访问不会月结,实际分配的块比结构声明更大

#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

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_VSHRSTRLUA_VLNGSTR 两个 tag,其实也就是 tt_

#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 的地址是否相同

#define eqshrstr(a,b)	check_exp((a)->tt == LUA_VSHRSTR, (a) == (b))

如果是长字符串,则通过 luaS_eqlngstr 函数进行比较

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 状态,并直接返回

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 函数

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

typedef struct Table {
  CommonHeader;
  lu_byte flags;        // 1<<p 标记该 Table 有没有设置 meta method
  lu_byte lsizenode;    // 节点数组大小的对数, sizenode = 2^(lsizenode)
  unsigned int alimit;  // 数组段(array part)的 边界上限
  TValue *array;        // 数组段指针,存放 1..alimit 之间的整数键对应的值
  Node *node;           // 哈希段节点数组的起始指针;空表时指向 dummynode
  Node *lastfree;       // 指向哈希段中“尚可能为空”的最后位置,便于从后往前找空位
  struct Table *metatable;
  GCObject *gclist;
} Table;

以下面代码为例,可以理解 flags 属性的作用,通过将 flags1 << TMS 进行位运算比较来判断是否实现了某些 metaMethod

// meta method 枚举
typedef enum {
  TM_INDEX,
  TM_NEWINDEX,
  TM_GC,
  TM_MODE,
  ...
};

const TValue *luaT_gettm (Table *events, TMS event, TString *ename) {
  const TValue *tm = luaH_getshortstr(events, ename);
  lua_assert(event <= TM_EQ);
  if (notm(tm)) {  /* no tag method? */
    events->flags |= cast_byte(1u<<event);  /* cache this fact */
    return NULL;
  }
  else return tm;
}

关于 Table 有两个结构:数组段 和 哈希段

  • 数组段,只存放整数键,且大多是稠密区间,从 1 开始到 limit
  • 哈希段,存放非整数或 稀疏/大整数键,哈希冲突用 节点数组 + 单链表 实现