lua-源码带读-04-字符串系统-闭包与上值
Lua 5.4 源码带读 第4篇:字符串系统、闭包与上值
本篇目标
深入理解 Lua 的字符串管理(驻留、短串/长串区分)、闭包机制(Lua闭包 vs C闭包)和上值(Upvalue)的工作原理。
前置知识
- 阅读过第1-3篇
- 了解字符串驻留(interning)的概念
- 了解闭包的概念
1. 字符串系统:lstring.c
1.1 两种字符串
Lua 5.4 区分两种字符串:
| 类型 | 宏 | 长度限制 | 特点 |
|---|---|---|---|
| 短字符串 | LUA_VSHRSTR | ≤40字节 | 驻留(interned),相同内容共享 |
| 长字符串 | LUA_VLNGSTR | >40字节 | 不驻留,独立存储 |
1.2 TString 结构体
// lobject.h
typedef struct TString {
CommonHeader; // next + tt + marked
lu_byte extra; // 额外信息(如不可哈希标记)
size_t shrlen; // 短字符串长度
unsigned int hash; // 哈希值(用于驻留)
union {
size_t lnglen; // 长字符串长度
struct TString *hnext; // 哈希链(短字符串驻留用)
} u;
char contents[1]; // 实际字符数据(柔性数组)
} TString;
1.3 短字符串驻留
// lstring.c
static TString *internshrstr(lua_State *L, const char *str, size_t l) {
global_State *g = G(L);
// 计算哈希值
unsigned int h = luaS_hash(str, l, g->seed);
// 查找驻留表
TString **p = &g->strt.hash[h & (g->strt.size - 1)];
while (*p != NULL) {
TString *ts = *p;
if (ts->shrlen == l && (memcmp(str, getstr(ts), l) == 0)) {
// 找到相同内容的字符串,返回已有的
return ts;
}
p = &ts->u.hnext; // 沿哈希链查找
}
// 未找到,创建新字符串
TString *ts = createstrobj(L, l, LUA_VSHRSTR, h);
memcpy(getstr(ts), str, l);
ts->u.hnext = *p;
*p = ts; // 插入哈希链
g->strt.nuse++;
// 检查是否需要扩容
if (g->strt.nuse > cast(unsigned int, g->strt.size) && g->strt.size <= MAX_INT/2)
luaS_resize(L, g->strt.size * 2);
return ts;
}
1.4 驻留表
// lstate.h
typedef struct stringtable {
TString **hash; // 哈希桶数组
lu_int32 nuse; // 当前使用数
lu_int32 size; // 桶数(2的幂次)
} stringtable;
驻留的好处:
- 字符串比较变成指针比较(O(1))
- 节省内存(相同内容的短字符串只存储一份)
1.5 长字符串
// lstring.c
static TString *createstrobj(lua_State *L, size_t l, int tag, unsigned int h) {
TString *ts;
size_t totalsize = sizestring(l);
ts = cast(TString, luaC_newobj(L, tag, totalsize));
ts->hash = h;
ts->extra = 0;
if (tag == LUA_VSHRSTR) {
ts->shrlen = l;
} else {
ts->u.lnglen = l;
}
return ts;
}
长字符串不驻留,但仍然可以通过 luaS_new 短路优化转为短字符串。
2. 闭包与上值:lfunc.c
2.1 Closure 结构体
// lobject.h
typedef union Closure {
CClosure c; // C闭包
LClosure l; // Lua闭包
} Closure;
typedef struct CClosure {
CommonHeader;
lua_CFunction f; // C函数指针
int nupvalues; // 上值数量
// 上值数组(紧随其后)
} CClosure;
typedef struct LClosure {
CommonHeader;
Proto *p; // 函数原型(字节码、常量表等)
UpVal *upvals; // 上值数组
size_t nupvalues; // 上值数量
} LClosure;
2.2 Lua 闭包 vs C 闭包
| 特性 | Lua闭包 | C闭包 |
|---|---|---|
| 包含字节码 | 是(Proto) | 否 |
| 上值数量 | 0 到任意 | 0 到任意 |
| 创建方式 | 闭包表达式 | lua_pushcclosure |
| 访问外部变量 | 通过UpVal | 通过lua_upvalueindex |
2.3 UpVal(上值)
上值是闭包访问外部局部变量的机制。
// lfunc.h
struct UpVal {
CommonHeader;
TValue *v; // 指向栈上的变量(变量未关闭时)
union {
struct { // 打开的上值
struct UpVal *next;
struct UpVal **prev;
} open;
TValue value; // 关闭后的值存储
} u;
};
2.4 打开 vs 关闭的上值
打开状态:v 指向栈上的变量(函数返回前)
- 多个闭包可以共享同一个打开的上值
- 通过
openupval链表管理
关闭状态:v 指向 u.value(函数返回后)
- 变量已离开栈,值保存在 UpVal 结构体中
- 从
openupval链表中移除
2.5 上值关闭
// lfunc.c
void luaF_close(lua_State *L, StkId level) {
UpVal *uv;
// 从栈顶向下查找打开的上值
while (L->openupval != NULL &&
(uv = L->openupval) >= level) {
lua_assert(!upisopen(uv));
// 从链表中移除
*uv->u.open.prev = uv->u.open.next;
if (uv->u.open.next != NULL)
uv->u.open.next->u.open.prev = uv->u.open.prev;
// 关闭上值(复制栈上的值到UpVal结构体)
setobj(L, &uv->u.value, uv->v);
uv->v = &uv->u.value; // v指向自身
}
}
2.6 创建闭包
// lfunc.c
LClosure *luaF_newLclosure(lua_State *L, int n) {
GCObject *o = luaC_newobj(L, LUA_VLCL, sizeLclosure(n));
LClosure *c = gco2lcl(o);
c->p = NULL;
c->nupvalues = cast_byte(n);
while (n--) c->upvals[n] = NULL;
return c;
}
// lvm.c - OP_CLOSURE 指令
case OP_CLOSURE: {
Proto *p = cl->p->p[GETARG_Bx(i)];
LClosure *ncl = getcached(p, cl->upvals, base);
if (ncl == NULL)
ncl = luaF_newLclosure(L, p->nupvalues);
setclLvalue(L, ra, ncl);
ncl->upvals[0] = &cl->upvals[GETARG_Bx(i)];
// ... 连接其他上值
vmbreak;
}
3. C API:lapi.c
3.1 栈式交互模型
所有C API操作都通过lua_State的栈进行:
C代码 -> push值到栈 -> 调用Lua -> 从栈上取返回值
Lua -> 参数压栈 -> 调用C函数 -> C函数从栈上取参数
3.2 虚拟索引
// lua.h
#define LUA_REGISTRYINDEX (-1001000) // 全局注册表伪索引
#define LUA_ENVIRONINDEX (-1001001) // 环境伪索引
#define LUA_GLOBALSINDEX (-1001002) // 全局表伪索引
// 正数:从栈底向上(1=栈底)
// 负数:从栈顶向下(-1=栈顶)
3.3 栈操作API
// lua.h
void lua_pushnil(lua_State *L);
void lua_pushnumber(lua_State *L, lua_Number n);
void lua_pushinteger(lua_State *L, lua_Integer n);
void lua_pushstring(lua_State *L, const char *s);
void lua_pushboolean(lua_State *L, int b);
void lua_pushcfunction(lua_State *L, lua_CFunction f);
void lua_pushcclosure(lua_State *L, lua_CFunction f, int n);
void lua_pushlightuserdata(lua_State *L, void *p);
void lua_pushthread(lua_State *L);
3.4 值获取
lua_Number lua_tonumberx(lua_State *L, int idx, int *isnum);
lua_Integer lua_tointegerx(lua_State *L, int idx, int *isnum);
int lua_toboolean(lua_State *L, int idx);
const char *lua_tolstring(lua_State *L, int idx, size_t *len);
int lua_type(lua_State *L, int idx);
3.5 表操作
void lua_settable(lua_State *L, int idx);
void lua_gettable(lua_State *L, int idx);
void lua_setfield(lua_State *L, int idx, const char *k);
void lua_getfield(lua_State *L, int idx, const char *k);
3.6 错误处理
// lua_error - 抛出错误(类似longjmp)
// lua_pcall - 受保护调用(错误不会崩溃程序)
int lua_pcallk(lua_State *L, int nargs, int nresults, int errfunc, lua_KContext ctx, lua_KFunction k);
4. 本篇小结
| 概念 | 要点 |
|---|---|
| 字符串 | 短串(≤40字节)驻留,长串(>40字节)独立存储 |
| 短串比较 | 指针比较(O(1)),通过哈希链实现驻留 |
| Lua闭包 | 包含Proto + UpVal数组 |
| C闭包 | 包含C函数指针 + 上值 |
| 上值 | 打开状态指向栈,关闭状态保存值 |
| C API | 栈式交互,虚拟索引,受保护调用 |
思考题
- 短字符串驻留表为什么使用哈希链而非开放寻址?这有什么优势?
- 如果一个字符串被用作表键,它何时从短串转为长串?
- 为什么C闭包和Lua闭包共享同一个Closure联合体?
- 上值的"打开"和"关闭"分别发生在什么时候?
- lua_pcallk 和 lua_pcall 有什么区别?k参数用于什么?
思考题解答
1. 短字符串驻留表为什么使用哈希链?
哈希链 vs 开放寻址:
| 特性 | 哈希链 | 开放寻址 |
|---|---|---|
| 冲突处理 | 链表连接冲突节点 | 探测下一个空位 |
| 动态扩容 | 简单(重新分配桶) | 复杂(需要rehash所有元素) |
| 删除操作 | 简单(链表断开) | 复杂(需要墓碑标记) |
| 内存局部性 | 较差(链表跳转) | 较好(连续内存) |
选择哈希链的原因:
- 字符串驻留表频繁增长(新字符串不断创建),开放寻址的rehash成本高
- 字符串对象较大,开放寻址需要移动整个对象,哈希链只需移动指针
- 短字符串不会被删除(驻留到程序结束),不需要删除操作的优化
hnext指针已经存在于 TString 结构体中(union的一部分),不额外占用内存
2. 字符串何时从短串转为长串?
不会转换。短串和长串是创建时就确定的:
// lstring.c
TString *luaS_newlstr(lua_State *L, const char *str, size_t l) {
if (l <= LUAI_MAXSHORTLEN)
return internshrstr(L, str, l); // 短串,驻留
else
return luaS_newLstr(L, str, l); // 长串,不驻留
}
一个字符串对象一旦创建为短串或长串,不会改变类型。即使短串内容被频繁使用,也不会变成长串。
注意:# 运算符对短串和长串的行为一致,但短串的长度在 shrlen 字段(lu_byte,最大255),长串在 lnglen 字段(size_t,无限制)。实际上 LUAI_MAXSHORTLEN 是40字节,所以40字节以下的字符串都是短串。
3. 为什么C闭包和Lua闭包共享Closure联合体?
-
类型系统统一:在Lua中,
type(f)返回"function"而不区分Lua函数和C函数。联合体让两者共享同一个类型标签LUA_TFUNCTION。 -
可以互相调用:
f()不需要区分f是Lua闭包还是C闭包,VM通过tt_字段的变体位区分调用路径。 -
上值机制统一:C闭包和Lua闭包都可以拥有上值,通过统一的
Closure类型可以共享上值管理代码。 -
内存效率:联合体只占两者中较大者的空间,不需要为两种闭包分配不同大小的结构。
union Closure {
CClosure c; // C闭包:函数指针 + nupvalues + 上值数组
LClosure l; // Lua闭包:Proto + upvals指针
};
4. 上值的"打开"和"关闭"分别发生在什么时候?
打开状态(变量在栈上活跃时):
- 进入包含外部变量引用的函数时
- 多个闭包共享同一个打开的上值
v指向栈上的变量位置
关闭状态(函数返回时):
- 函数执行
return或块结束时 - 通过
luaF_close()将栈上的值复制到 UpVal 结构体 - 从
openupval链表中移除 v指向u.value(值保存在UpVal中)
local x = 1
local f = function()
return x -- x被闭包引用
end
-- f() 返回后,x的上值被关闭,值1保存在UpVal中
为什么需要关闭:函数返回后,对应的栈帧被销毁,但闭包仍然需要访问变量的值。关闭操作将值从栈上"抢救"到堆上的UpVal结构体中。
5. lua_pcallk 和 lua_pcall 的区别
// lua_pcall 是 lua_pcallk 的简化版本
#define lua_pcall(L,n,r,f) lua_pcallk(L, n, r, f, 0, NULL)
// lua_pcallk 的额外参数:
// ctx: continuation上下文
// k: continuation函数(lua_KFunction)
k参数的用途:实现跨C调用边界的yield(5.4新增)。
在Lua 5.1中,C函数不能在被调用时yield。5.4通过continuation机制允许:
// 一个可以被中断的C函数
static int my_cfunc(lua_State *L) {
// 执行部分工作
// 然后调用lua_yieldk
return lua_yieldk(L, 1, ctx, my_continuation);
}
// continuation函数:在resume时被调用
static int my_continuation(lua_State *L, int status, lua_KContext ctx) {
// 继续未完成的工作
}
如果k为NULL,则行为与lua_pcall完全相同——不允许yield。这保持了向后兼容性。

浙公网安备 33010602011771号