lua-源码带读-01-语言设计与源码结构
Lua 5.4 源码带读 第1篇:语言设计与源码结构
本篇目标
理解 Lua 5.4 的设计哲学,建立全部源文件的索引,深入理解类型系统和 lua_State —— 这是阅读后续所有篇的基础。
前置知识
- C 语言基础:指针、结构体、联合体、宏定义、typedef
- 函数指针的基本用法
- 对"嵌入式脚本语言"有基本概念
1. Lua 是什么
Lua 是一个嵌入式脚本语言,由巴西 PUC-Rio 大学的 Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo 设计。核心特点:
- 极小:整个解释器 + 标准库约 20,000 行 C 代码(一个熟练程序员几天就能通读)
- 嵌入式:设计目标是被 C/C++ 程序嵌入,而非独立运行
- 动态类型:变量没有类型,值才有类型
- 自动内存管理:垃圾回收器自动管理内存
- 寄存器式虚拟机:字节码执行模型(比栈式VM高效)
- 表(Table):唯一的复合数据结构,数组、字典、对象、模块都用表实现
- 协程:对称协程(symmetric coroutine),可实现迭代器、状态机、协作式多任务
2. 源码目录结构
Lua 的源码结构极其简单——所有文件都在 src/ 目录下平铺,没有任何子目录:
lua-source/
├── Makefile
├── README
└── src/
├── lua.h ← 公共 API 头文件
├── luaconf.h ← 所有可配置宏
├── lauxlib.h ← 辅助库 API
├── lualib.h ← 标准库加载函数
├── lua.c ← 独立解释器程序(读取stdin或文件执行)
├── luac.c ← 独立编译器程序(编译为字节码)
├── l*.c / l*.h ← 核心实现(约30个文件)
└── Makefile
没有目录嵌套,没有复杂的构建系统。这正是 Lua 的设计哲学:简单就是力量。
3. 源文件完整索引
3.1 公共头文件(外部使用者需要关注的)
| 文件 | 行数 | 职责 |
|---|---|---|
lua.h |
523 | 核心 API 声明:类型定义、栈操作、加载/调用、GC 控制、协程、调试 |
luaconf.h |
802 | 所有可配置项:数值类型、路径、导出符号、内存分配器 |
lauxlib.h |
~250 | 辅助库:在核心 API 之上封装的便捷函数 |
lualib.h |
~50 | 标准库加载函数:luaL_openlibs() |
3.2 类型定义(最重要的头文件)
| 文件 | 职责 |
|---|---|
lobject.h |
所有 Lua 对象的类型定义:Value、TValue、TString、Table、Proto、Closure、UpVal |
llimits.h |
内部宏和基本类型别名(lu_byte、lu_mem 等) |
lopcodes.h |
字节码操作码定义(83条指令) |
lopnames.h |
操作码名称字符串(用于调试输出) |
ljumptab.h |
虚拟机跳转表(用于高效指令分发) |
3.3 核心实现
| 文件 | 职责 |
|---|---|
lapi.c |
C API 实现——桥接外部 C 代码与 Lua 内部 |
llex.c |
词法分析器(lexer)——源代码 → Token 流 |
lparser.c |
递归下降语法解析器——Token 流 → 字节码 |
lcode.c |
代码生成器——在解析过程中直接产生字节码 |
lvm.c |
虚拟机主循环——执行字节码的引擎 |
ldo.c |
栈和调用处理——函数调用、pcall、yield、错误处理 |
lgc.c |
垃圾回收器——三色标记 + 增量 + 分代(5.4新特性) |
ltable.c |
表(Table)实现——数组部分 + 哈希部分 |
lstate.c |
全局状态和线程(协程)状态 |
lstring.c |
字符串驻留(interning)——短字符串共享,长字符串直接存储 |
lfunc.c |
闭包(Closure)和上值(Upvalue)管理 |
ltm.c |
元方法(Metamethod)访问和分发 |
lzio.c |
缓冲输入流抽象——供词法分析器和加载器使用 |
lobject.c |
通用对象工具函数(字符串转数值、格式化等) |
lmem.c |
内存管理接口——封装用户提供的分配函数 |
lctype.c |
字符类型判断(letter/digit等,Lua内部用) |
3.4 标准库
| 文件 | 职责 |
|---|---|
lbaselib.c |
基础库:print, type, pcall, error, assert, select, collectgarbage 等 |
lstrlib.c |
字符串库:string.format, string.match, string.gmatch, string.gsub 等 |
ltablib.c |
表操作库:table.insert, table.remove, table.sort, table.concat |
lmathlib.c |
数学库:math.random, math.floor, 位运算等 |
liolib.c |
I/O 库:io.open, io.read, io.write 等 |
loslib.c |
OS 库:os.time, os.execute 等 |
lcorolib.c |
协程库:coroutine.create, coroutine.resume, coroutine.yield |
ldblib.c |
调试库:debug.getinfo, debug.getlocal, debug.sethook 等 |
loadlib.c |
动态库加载:require 的底层实现(package.loadlib) |
lutf8lib.c |
UTF-8 支持库 |
linit.c |
构建标准库列表:luaL_openlibs() |
4. luaconf.h —— 所有可配置项
luaconf.h 是 Lua 中唯一需要根据平台修改的文件。关键配置项:
4.1 数值类型
// luaconf.h 中的关键配置
#if LUAI_IS_32INT
typedef int lua_Integer; // 默认32位整数
#else
typedef long long lua_Integer; // 可选64位整数
#endif
#if !defined(LUA_FLOAT_TYPE)
#define LUA_FLOAT_DOUBLE // 默认双精度浮点
#endif
Lua 5.4 的重大改进:整数和浮点数是两种不同的内部类型(5.3+)。这避免了 0 和 0.0 的比较问题。
4.2 内存分配器
// luaconf.h: 默认内存分配器
#define l_allocarius(p, o, n, u) ((void)(u), l_alloc(p, o, n))
用户可以通过 lua_setalloc 替换分配器,这是嵌入式场景的关键特性。
4.3 调试开关
#define LUAI_USE_ASSERT 0 // 启用断言检查
#define LUAI_USE_APICHECK 0 // 启用API参数检查
5. 核心类型系统(基于 lobject.h)
Lua 的类型系统是理解整个源码的关键。所有 Lua 值的底层表示都围绕两个核心类型:Value 和 TValue。
5.1 Value 联合体——所有值的容器
// lobject.h:49-57
typedef union Value {
struct GCObject *gc; /* 可回收对象(string、table、function 等) */
void *p; /* 轻量用户数据(light userdata) */
lua_CFunction f; /* 轻量 C 函数 */
lua_Integer i; /* 整数值 */
lua_Number n; /* 浮点值 */
lu_byte ub; /* 未使用,但避免编译器警告 */
} Value;
这是一个 联合体(union),所有成员共享同一块内存。同一时刻只有一个成员有效,具体哪个有效由类型标签决定。
联合体的好处:
- 内存效率:所有值只占 8 字节(64位系统)
- 类型安全:通过 tt_ 标签访问正确的成员
- 无装箱开销:不像 Java 有 Integer/Double 包装类
5.2 TValue——值 + 类型标签
// lobject.h:65-69
typedef struct TValue {
Value value_; /* 实际的值 */
lu_byte tt_; /* 类型标签(type tag) */
} TValue;
TValue 是 Lua 内部表示所有值的基础结构。每个 Lua 值在内部都是一个 TValue。
5.3 类型标签的位布局
// lobject.h:38-42
// bits 0-3: 实际类型(LUA_T* 常量)
// bits 4-5: 变体位(variant bits)
// bit 6: 是否可回收(collectable)
// bit 7: 未使用
标签位的含义:
- 低 4 位:9 种基本类型之一(nil/boolean/lightuserdata/number/string/table/function/userdata/thread)
- 4-5 位:同一类型内的变体
- 第 6 位:标记该值是否需要垃圾回收
5.4 九种基本类型
// lua.h:63-73
#define LUA_TNIL 0
#define LUA_TBOOLEAN 1
#define LUA_TLIGHTUSERDATA 2
#define LUA_TNUMBER 3
#define LUA_TSTRING 4
#define LUA_TTABLE 5
#define LUA_TFUNCTION 6
#define LUA_TUSERDATA 7
#define LUA_TTHREAD 8
5.5 变体详解
| 基本类型 | 变体 | 值 | 含义 |
|---|---|---|---|
| LUA_TNUMBER | LUA_VNUMINT | 3<<4 | 64位有符号整数 |
| LUA_TNUMBER | LUA_VNUMFLT | 4<<4 | 双精度浮点 |
| LUA_TSTRING | LUA_VSHRSTR | 2<<4 | 短字符串(≤40字节,驻留) |
| LUA_TSTRING | LUA_VLNGSTR | 3<<4 | 长字符串(>40字节) |
| LUA_TFUNCTION | LUA_VLCL | 1<<4 | Lua函数(闭包) |
| LUA_TFUNCTION | LUA_VLCF | 2<<4 | C函数(无闭包) |
| LUA_TFUNCTION | LUA_VCCF | 3<<4 | C闭包(带upvalue) |
5.6 类型检查宏
// lobject.h 中的类型检查宏
#define ttisnumber(o) checktype((o), LUA_TNUMBER)
#define ttisinteger(o) ((o)->tt_ == LUA_VNUMINT)
#define ttisfloat(o) ((o)->tt_ == LUA_VNUMFLT)
#define ttisstring(o) checktype((o), LUA_TSTRING)
#define ttisshrstring(o) ((o)->tt_ == LUA_VSHRSTR)
#define ttisfullstring(o) ((o)->tt_ == LUA_VLNGSTR)
#define ttistable(o) checktype((o), LUA_TTABLE)
#define ttisfunction(o) checktype((o), LUA_TFUNCTION)
#define ttisnil(o) ((o)->tt_ == LUA_TNIL)
#define ttisboolean(o) ((o)->tt_ == LUA_TBOOLEAN)
#define ttisclosure(o) ((o)->tt_ >= LUA_VLCL) // 函数类型都算closure
checktype 检查低4位是否匹配:
#define checktype(o,t) ((o)->tt_ & 0x0F) == (t))
5.7 GC 对象公共头部
所有需要垃圾回收的对象共享一个公共头部:
// lobject.h:288
#define CommonHeader struct GCObject *next; lu_byte tt; lu_byte marked
typedef struct GCObject {
CommonHeader; // next + tt + marked
} GCObject;
next:指向下一个GC对象(GC用链表遍历所有对象)tt:类型标签(与 TValue 的 tt_ 相同)marked:GC标记(白色/灰色/黑色)
需要GC管理的对象:TString、Table、LClosure、CClosure、UpVal、lua_State、Proto
5.8 常见对象结构
Proto(函数原型):
typedef struct Proto {
CommonHeader;
TValue *k; // 常量表(编译时确定的值)
Instruction *code; // 字节码数组
int sizek; // 常量数
int sizecode; // 指令数
int numparams; // 固定参数数
int is_vararg; // 是否为可变参数函数
int maxstacksize; // 最大栈需求
// ... 调试信息、行号信息等
} Proto;
TString(字符串):
typedef struct TString {
CommonHeader;
lu_byte extra; // 额外信息(如不可哈希标记)
size_t shrlen; // 短字符串长度
unsigned int hash; // 哈希值(用于驻留)
union {
size_t lnglen; // 长字符串长度
struct TString *hnext; // 哈希链(用于驻留)
} u;
char contents[1]; // 实际字符数据(柔性数组)
} TString;
UpVal(上值):
struct UpVal {
CommonHeader;
TValue *v; // 指向栈上的变量(变量未关闭时)
// 或指向自身value(变量已关闭时)
union {
struct UpVal *next; // 打开的上值链表
// 关闭时:value 存储实际值
} u;
TValue value; // 关闭后的值存储
};
6. lua_State——Lua 的心脏
// lua.h:57
typedef struct lua_State lua_State;
lua_State 是 Lua 的核心结构体。每个独立的 Lua 虚拟机实例就是一个 lua_State,每个协程也是一个独立的 lua_State。
// lstate.h 中的完整定义
struct lua_State {
CommonHeader; // GC头部
unsigned short nci; // 嵌套C调用数
lu_byte status; // 线程状态(OK/YIELD/ERROR)
StkId oldpc; // 上一个PC(用于调试)
StkId stack; // 值栈底
StkId stack_last; // 栈的最后一个安全位置
StkId top; // 栈顶指针(下一个可用位置)
global_State *l_G; // 全局状态
CallInfo *ci; // 当前调用信息
StkId registry; // 注册表基址
StkId errorJmp; // 错误恢复点(longjmp目标)
CallInfo base_ci; // 初始调用信息
// ... 统计信息、hook信息等
};
6.1 CallInfo——调用栈帧
// lstate.h
typedef struct CallInfo {
StkId func; // 函数在栈上的位置
StkId u.l.base; // 当前函数的基址(base指针)
const Instruction *savedpc; // 保存的PC(协程切换用)
unsigned short ncall; // 调用深度计数
// ...
} CallInfo;
所有 CallInfo 通过链表连接,形成函数调用链。
6.2 global_State——全局状态
// lstate.h
typedef struct global_State {
void *ud; // 用户数据(传给分配器)
lua_Alloc alloc; // 内存分配函数
l_mem frealloc; // 重新分配函数
lu_byte currentwhite; // 当前GC白色
GCObject *allgc; // 所有GC对象链表
GCObject *finobj; // 待终结对象链表
TValue l_registry; // 注册表
TValue nilvalue; // nil值的全局实例
unsigned int seed; // 随机种子(哈希用)
// ... GC状态、字符串驻留表、计数器等
} global_State;
7. 三级头文件架构
luaconf.h ← 所有可配置的宏
↓
lua.h ← 公共 API 和类型定义
↓
lauxlib.h ← 辅助库(在 lua.h 之上封装)
lualib.h ← 标准库加载
每一层都依赖上一层。外部用户通常只需要:
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
内部实现文件则需要额外包含:
#include "lua.h"
#include "llimits.h"
#include "lobject.h"
#include "lstate.h"
// ...
8. lua.c 和 luac.c——独立程序
Lua 提供两个独立程序:
lua.c(解释器):
- 读取 stdin 或文件中的 Lua 代码
- 直接编译并执行
- 交互式 REPL 模式
luac.c(编译器):
- 将 Lua 源代码编译为字节码(.luac 文件)
- 可以反编译查看生成的字节码
- 用于预编译和性能优化
两者共享 lua.h 公共 API,核心实现都在 l*.c 文件中。
9. 本篇小结
| 概念 | 要点 |
|---|---|
| Lua 的定位 | 嵌入式脚本语言,不是独立程序 |
| 源码结构 | 全部在 src/ 下平铺,~20,000 行 |
| 核心类型 | Value(联合体)+ tt_(类型标签)= TValue |
| lua_State | 每个虚拟机实例,包含栈、调用链、全局表 |
| GC 对象 | string/table/closure/upvalue/userdata/thread 都通过 GC 管理 |
| luaconf.h | 唯一需要平台修改的文件 |
| Proto | 函数原型,包含常量表、字节码、参数信息 |
| CallInfo | 调用栈帧,链表连接 |
思考题
Value用联合体而非结构体有什么好处?如果用结构体会怎样?- 为什么类型标签用
lu_byte(8位)?只用 4 位存类型就够了,为什么需要变体位和 GC 位? - Lua 的短字符串会驻留(interning),即相同内容的短字符串共享同一个
TString对象。这有什么好处?有什么限制? - 为什么 Lua 的表只有一个复合数据结构(Table),而不用数组、链表、哈希表等多种结构?
global_State中的seed字段有什么用途?为什么需要随机种子?- Proto 结构体中的
is_vararg字段如何影响虚拟机的行为?
思考题解答
1. Value 用联合体而非结构体有什么好处?
联合体(union)的好处:
- 内存效率:所有成员共享同一块内存,一个 Value 只占 8 字节(64位系统上最大的成员 lua_Number/void* 的大小)。如果用结构体,需要为每个成员分配独立空间,至少 8+8+8+8+8=40 字节。
- 无类型膨胀:不需要为每种类型定义单独的包装类(不像 Java 有 Integer、Double、String 等装箱类)。
- 与 GC 配合:联合体中的
gc指针可以直接用于 GC 链表管理,不需要类型转换。 - 指针兼容:
void *p和GCObject *gc在大多数平台上大小相同,内存布局紧凑。
如果用结构体:
- 内存膨胀 5-8 倍,嵌入式场景无法接受
- 大量未使用的字段浪费 cache line
- 类型判断需要检查哪个字段有效,反而更复杂
代价:联合体要求程序员(或编译器)确保同一时刻只访问一个成员,通过类型标签(tt_)保证安全性。
2. 类型标签为什么用 8 位?
8位(lu_byte)的设计考量:
- 低4位(bits 0-3):存储9种基本类型(0-8),4位最多16种,足够
- 变体位(bits 4-5):同一类型内区分不同变体,例如:
LUA_TNUMBER内区分整数(LUA_VNUMINT)和浮点(LUA_VNUMFLT)LUA_TSTRING内区分短串(LUA_VSHRSTR)和长串(LUA_VLNGSTR)LUA_TFUNCTION内区分Lua闭包(LUA_VLCL)、C函数(LUA_VLCF)、C闭包(LUA_VCCF)
- bit 6(collectable):标记该值是否需要GC管理。非GC对象(nil、boolean、整数、浮点)不需要GC跟踪,设置此位为0可以跳过GC扫描
- bit 7:保留未用,为未来扩展留空间
变体位的好处:
- 快速类型检查:
ttisinteger(o)只需一次比较o->tt_ == LUA_VNUMINT - GC优化:collectable位让GC跳过不需要回收的对象
- 类型安全:
ttisclosure(o)用o->tt_ >= LUA_VLCL一次性检查所有函数变体
3. 短字符串驻留的好处和限制
好处:
- O(1) 字符串比较:相同内容的字符串只有一份,比较变成指针比较(
ts1 == ts2),不需要逐字符比较 - 节省内存:
"hello"出现100次只存储一份 - 哈希值缓存:TString的
hash字段在创建时计算好,后续哈希表操作直接使用 - 加速表查找:字符串键的哈希值已预计算,
luaH_getshortstr直接使用
限制:
- 长度限制:只有 ≤40 字节的字符串才驻留(
STRCACHEMAX),超过的作为长字符串独立存储 - 短串不会被回收:驻留表中的短字符串不会被GC回收(直到整个字符串驻留表重建),可能导致内存泄漏
- 驻留表扩容成本:字符串数量增长时需要rehash整个驻留表,这是一个O(n)操作
- 全局共享风险:所有lua_State共享同一个字符串驻留表,多线程场景需要考虑线程安全
- 动态内容:每次
tostring(x)生成新的短串,不会与已有的自动合并(内容相同但生成方式不同时)
设计权衡:40字节的阈值是经验值——短字符串通常作为表键、错误消息、标识符使用,驻留收益大;长字符串(如大段文本)使用频率低且复制成本高,不适合驻留。
4. 为什么只有 Table 一种复合数据结构?
这是 Lua 的核心设计哲学——极简主义:
- 实现简单:只需实现一个 Table 的C代码,而不需要数组、链表、哈希表、有序映射等多种数据结构
- 统一接口:所有操作(下标、迭代、长度、插入删除)都通过同一套API
- 双区设计覆盖大多数场景:
- 数组部分处理正整数键(
{1,2,3}) - 哈希部分处理其他键(
{a=1, b=2}) - 自动根据使用模式调整两个区域的大小
- 数组部分处理正整数键(
- 元表扩展性:通过元表可以为Table添加任何行为,不需要其他数据结构
- 嵌入式友好:内存分配只需一次(Table结构体+数组+哈希),减少碎片
代价:
- 无法高效实现特定数据结构(如有序映射需要额外库)
- 稀疏数组浪费空间(
{[1000]=1}会创建1000个空槽) - 没有原生的平衡二叉树、跳表等高效结构
Lua的哲学是:一个足够灵活的数据结构(Table)+ 元表机制 = 足够解决所有问题。其他数据结构可以通过C库或Lua库实现。
5. seed 字段的用途
global_State 中的 seed 是一个随机种子,主要用途:
- 字符串哈希:
luaS_hash(str, len, seed)用 seed 作为哈希算法的初始值,防止哈希碰撞攻击(HashDoS) - 表哈希:Table 的哈希部分也使用 seed 计算键的哈希值
- 防止DoS:如果没有随机种子,攻击者可以构造大量碰撞的字符串键,使哈希表退化为O(n)查找(类似Java HashMap在Java 8之前的漏洞)
- 每次启动不同:seed 在
lua_newstate时生成,基于时间等信息,每次运行不同
// lstate.c
g->seed = luaG_numbergenversion(G);
// 或基于随机源
这在Web应用等暴露给外部输入的场景中尤为重要——攻击者无法预测seed,也就无法构造碰撞攻击。
6. is_vararg 字段的影响
Proto 中的 is_vararg 字段控制虚拟机如何处理可变参数函数:
| 值 | 含义 | VM行为 |
|---|---|---|
| 0 | 固定参数函数 | 参数直接放在 base 到 base + numparams 之间 |
| 1 (VARARG_HASARG) | 有固定参数的可变参数函数 | 多余参数放在栈顶 |
| 2 (VARARG_ISVARARG) | 纯可变参数函数 | 参数通过 VARARGPREP 调整 |
VM行为差异:
// ldo.c - luaD_precall
if (p->is_vararg) {
// 可变参数函数:需要调整参数
base = adjust_varargs(L, p, narg);
// 多余参数复制到栈顶
} else {
// 固定参数函数:base = func + 1
base = func + 1;
}
VARARGPREP 指令(5.4新增)在可变参数函数入口处执行,将固定参数复制到新的栈帧,为可变参数腾出空间。
is_vararg 还影响 OP_VARARG 指令的行为:当 C==0 时,使用实际的可变参数数量并设置 L->top;当 C>0 时,只取指定数量的参数。

浙公网安备 33010602011771号