|
|
2 maanden geleden | |
|---|---|---|
| book | 3 maanden geleden | |
| image | 3 maanden geleden | |
| src | 3 maanden geleden | |
| .gitignore | 3 maanden geleden | |
| C++与Lua的交互.md | 2 maanden geleden | |
| README.md | 3 maanden geleden |
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,进而判断是执行循环体 还是 跳出循环
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 允许改变 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)
为了简化操作,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 版本之后被 移除
这里提供了两种替代方案
_ENV 重新绑定当前代码块环境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()
由于 lua 有自己的垃圾回收机制,大部分情况下不用关心对象的 GC
Weak 表的作用是告诉 GC 一个引用不应该阻止该对象被回收,它是个弱引用,可以被 GC 掉
如何定义一个 table 是 weak table 呢?
metatable 的 __mode 字符串包含小写的 k 表示这个 table 的 keys 都是 weak 的metatable 的 __mode 字符串包含小写的 v 表示这个 table 的 values 都是 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 a 的 keys 是 weak 的,{ a = 1} 没有对象引用,被 GC 掉了;{ b = }被 key 引用,因此没有被 GC
从 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;
}
luaL_newstate 创建全局状态对象 global_State 和主线程 lua_State、初始化堆栈、字符串表、GC、调用接口元数、创建注册表等luaL_opemlibs 遍历 linit.c中的库列表,依次执行luaL_requiref并将函数注册到库表中,将库表复制到全局_G[name]`luaL_doFile 来执行指定 lua 文件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 表示单个 线程/协程 的运行时对象,持有栈、调用帧、错误处理、钩子等
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 并记录触发频率 |
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]])
从 str_find_aux 函数中可以发现
const char *s = luaL_checklstring(L, 1, &ls) 这里的 s 就是 subject,即要搜索的字符串,因为它是函数参数栈中的第一个,所以第二个参数传入 1const char *p = luaL_checklstring(L, 2, &lp) 这里的 p 就是 pattern,即要匹配的子串,因为它是函数参数栈中的第二个,所以第三个参数传入 2CallInfo 是 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,即槽位的下一个
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_ 标识符来决定
#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.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 | 外部可见类型的计数 |
以下面这段代码为例
local f = function(a)
print(a)
end
变量 f 对应的 TValue 的 tt_ 是 LUA_TFUNCTION, value_.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 把所有这些类型放进一个 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指的是 被闭包捕获的外层局部变量的绑定(变量单元) ,是按引用共享的,不是值拷贝
typedef union StackValue {
TValue val;
struct {
TValuefields;
unsigned short delta;
} tbclist;
} StackValue;
StackValue 是 栈槽的专用表示 ,在 TValue 的基础上,额外为 待关闭变量(to-be-closed) 维护一条链
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_VSHRSTR 和 LUA_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 策略的不同
对于创建短字符串来说,会先从 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;
}
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 属性的作用,通过将 flags 与 1 << 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 有两个结构:数组段 和 哈希段