AIGC标识 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 栈式交互,虚拟索引,受保护调用

思考题

  1. 短字符串驻留表为什么使用哈希链而非开放寻址?这有什么优势?
  2. 如果一个字符串被用作表键,它何时从短串转为长串?
  3. 为什么C闭包和Lua闭包共享同一个Closure联合体?
  4. 上值的"打开"和"关闭"分别发生在什么时候?
  5. lua_pcallk 和 lua_pcall 有什么区别?k参数用于什么?

思考题解答

1. 短字符串驻留表为什么使用哈希链?

哈希链 vs 开放寻址

特性 哈希链 开放寻址
冲突处理 链表连接冲突节点 探测下一个空位
动态扩容 简单(重新分配桶) 复杂(需要rehash所有元素)
删除操作 简单(链表断开) 复杂(需要墓碑标记)
内存局部性 较差(链表跳转) 较好(连续内存)

选择哈希链的原因

  1. 字符串驻留表频繁增长(新字符串不断创建),开放寻址的rehash成本高
  2. 字符串对象较大,开放寻址需要移动整个对象,哈希链只需移动指针
  3. 短字符串不会被删除(驻留到程序结束),不需要删除操作的优化
  4. 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联合体?

  1. 类型系统统一:在Lua中,type(f) 返回 "function" 而不区分Lua函数和C函数。联合体让两者共享同一个类型标签 LUA_TFUNCTION

  2. 可以互相调用f() 不需要区分f是Lua闭包还是C闭包,VM通过 tt_ 字段的变体位区分调用路径。

  3. 上值机制统一:C闭包和Lua闭包都可以拥有上值,通过统一的 Closure 类型可以共享上值管理代码。

  4. 内存效率:联合体只占两者中较大者的空间,不需要为两种闭包分配不同大小的结构。

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。这保持了向后兼容性。

posted @ 2026-09-04 17:26  IcarusLee  阅读(2)  评论(0)    收藏  举报