纯 Java 实现的 Lua 5.4,垃圾回收交给 JVM。
javalua 是对 Lua 5.4 参考实现的逐模块移植。现有 Lua 5.4 程序可以原样运行: 完整语言语义、完整标准库、二进制 chunk、调试钩子,以及原生 C 模块。
- 完整的 Lua 5.4 语义 —— 手写词法分析器、语法分析器和代码生成器,以及
从
lvm.c移植的寄存器虚拟机。支持goto、变长参数、多返回值、整数/浮点 子类型,以及包括__close、__gc、__mode在内的全部元方法。 - 完整的标准库 ——
base、coroutine、package、string(含pack/unpack)、table、math、io、os、utf8,以及带钩子的debug。 - 内存交给 JVM —— 不移植
lgc.c。弱表基于WeakReference/ReferenceQueue,__gc用可复活的哨兵对象实现,字符串 驻留使用弱引用池。collectgarbage保留 Lua 的可观测行为,但实际回收交给 JVM。 - 协程不是线程 —— 每个协程是一条独立的 Lua 栈,
coroutine.yield通过LuaYield异常展开调用栈,等价于 C 的longjmp。可以穿过pcall、元方法和 迭代器 yield;跨 C 调用边界 yield 会像参考实现一样报错。 - 二进制 chunk ——
load、loadfile、dofile、require和string.dump读写标准 Lua 5.4 字节码。 - C API ——
luashim是一个小型 JNI 库,导出lua_*/luaL_*接口, 因此按 Lua 5.4 编译的真实 C 模块(lpeg、cjson、lfs、luabitop、mime、 luaposix 等)可以通过package.loadlib和require加载。它还补上 Java 没有对应物的那几个 libc 调用:setlocale(连带tonumber、tostring、string.format、io.read("n")都要走的那个 locale 小数点)、waitpid、FILE*。 - 需要 Java 21,单个 jar,无运行时依赖。
<dependency>
<groupId>com.github.esrrhs</groupId>
<artifactId>javalua</artifactId>
<version>1.0.0</version>
</dependency>import com.github.esrrhs.javalua.LuaLibs;
import com.github.esrrhs.javalua.LuaState;
import com.github.esrrhs.javalua.LuaValue;
import java.nio.file.Files;
import java.nio.file.Path;
LuaState L = new LuaState();
LuaLibs.openAll(L);
L.run("print('hello from Lua')");
LuaValue err = L.runProtected(
Files.readAllBytes(Path.of("script.lua")), "@script.lua");
if (err != null) {
System.err.println(err);
}
L.close();失败时 run 抛出 LuaError,runProtected 则返回错误值。每个 LuaState
拥有自己的全局表、注册表和协程,多个实例可以并行使用。
native/build.sh 把 native/luashim.c 编译到
src/main/resources/native/<os>-<arch>/,NativeBridge 会自动找到并加载。
编译需要 C 编译器和 JDK 头文件:
sh native/build.sh也可以在启动时用 -Djavalua.luashim=/path/to/libluashim.so 指定自定义
构建。加载 shim 后,require("lpeg") 和
package.loadlib(path, "luaopen_x") 的行为与 C Lua 一致。没有 shim 时,
package.loadlib 会返回
dynamic libraries not enabled; check your Lua installation。
发布到 Maven Central 的 jar 不带 shim(src/main/resources/native/ 被
gitignore,按机器现编),开箱用到的是纯 Java 那条路:io 走
RandomAccessFile 读写,缺的只是 C 模块加载。测试时可以两条路各自强制:
-Djavalua.nonative=true 假装 shim 装不起来。
正确性的标准是与真实的 Lua 5.4 二进制一致,而不是「看起来像」。针对参考 实现有四道闸门:
- 上游测试套件 —— Lua 官方的
lua-5.4.7-tests原样运行。 - 差分测试 —— 同一份脚本在两边执行,stdout 必须逐字节相同。覆盖语言 语义、标准库、错误消息、traceback 和字节码反汇编。
- 差分 fuzz —— 用确定性种子生成程序(包括协程模式和预编译 chunk
模式),与参考实现对比。种子数量可通过
-Dfuzz.seeds和-Dfuzz.coSeeds调整。 - 覆盖率 —— JaCoCo 行覆盖率,按两条
io路径合并后的数据统计, 不得低于 95%。
CI 分为三个 job:Build (JDK 21)、Conformance (against lua5.4) 和
Static analysis。其中 conformance job 会从源码构建 Lua 5.4.7、fkdump
字节码转储器和 luashim,再生成一个逗号小数点的 locale(pt_BR——没有它
那批 locale 用例全绿却什么都没测),然后把全量测试跑两遍:一遍带
shim,一遍加 -Djavalua.nonative=true 走纯 Java 的 io 路径,两条都要与
参考实现逐字节一致。每一遍之后紧跟一次 tools/check_ci_gates.py,某个闸门
类一条用例都没建时直接判失败;两遍的执行数据追加进同一个
jacoco.exec,95% 那条线最后对着合并后的文件卡一次。
下面只保留有意或平台无法对齐的偏离(其余应对齐 Lua 5.4.7):
- GC:内存交给 JVM(不移植
lgc.c)。弱表、__gc、collectgarbage尽量保留可观测形态,但collectgarbage("count")报的是 JVM 堆用量 (绝对值不同,相对变化仍有意义),且collectgarbage("step")的「本轮是否 结束」可能与 C 不同——没有可步进的增量 Lua 收集器。上游gc.lua/ 收集器参数类用例不做差分闸门。 - 超越函数(
math.sin等)与平台 libc 可能相差几个 ulp(JavaMath/fdlibm vs 参考实现链接的 libc)。 %p和 userdata/thread 的默认tostring输出 Java 身份标识,而不是 C 地址。- NaN 文本符号:C99 不规定 NaN 的符号。本实现按 IEEE 符号位对齐 glibc
(符号位置位打
-nan;正 NaN 在%+/%下打+nan/nan)。 Apple libc 仍可能对负 NaN 打无符号nan,差分测试两侧都会归一化。 部分数学库定义域错误的 NaN 载荷仍可能与 libm 不同;asin/acos已 收到与 glibc 相同的正 quiet NaN。 - 带 shim 时
os.setlocale会真的切换 libc 的 locale,而那是进程级 状态:同一个 JVM 里所有LuaState共享它,C 那边是一个进程一个状态。tonumber、tostring、string.format、io.write、io.read("n")用的 小数点(lua_getlocaledecpoint())以及os.date的 locale 名字(%A、%B、%c等,走 libcstrftime)都会跟着变。 - 没有 shim 时,
os.execute/io.popen:close对被信号杀死的进程只能给("exit", 128+n)(例如不排空就 close →("exit", 141)而不是("signal", 13)),os.setlocale只认nil/"C"/"POSIX",小数点恒为.,os.date名字类转换仍走英文表。 - 带 shim 时,
io.popen在子进程仍在写(sleep; echo、大块printf)就close与 C 一样稳定给signal 13。子进程已经写完再 close(极短的echo)时两边都会偶发exit 0——共有的时序窗口。os.clock已按进程 CPU 时间对齐 C 的clock();绝对值仍会因 JVM 启动开销与刚拉起的lua进程不同。
./mvnw test # 找不到 Lua 5.4 二进制时,差分测试会自动跳过
./mvnw package指定参考实现二进制即可启用全部闸门:
LUA54_BIN=/path/to/lua5.4 ./mvnw test查找顺序为 LUA54_BIN、target/lua54/src/lua,然后是 PATH 上的
lua5.4 或 lua(会校验版本确实是 5.4)。差分驱动把临时文件写在
target/rt/<pid>/ 下 —— 每个 JVM 一份,所以同时开两个 ./mvnw test
也不会读到对方的用例文件。可选环境变量:
| 变量 | 用途 |
|---|---|
LUA54_BIN |
参考解释器 |
LUAC54_BIN |
参考 luac |
FKDUMP_BIN |
针对参考实现编译的字节码转储器 |
LUA54_TESTS |
解压后的上游测试套件 |
LUA54_LIB |
参考实现的 liblua.a(C API 符号集对撞用;不设则依次试 target/lua54/src/liblua.a、/tmp/lua-5.4.7/src/liblua.a) |
本地构建参考实现:
curl -LO https://www.lua.org/ftp/lua-5.4.7.tar.gz
tar xzf lua-5.4.7.tar.gz
make -C lua-5.4.7 linux
LUA54_BIN=$PWD/lua-5.4.7/src/lua ./mvnw test| 路径 | 内容 |
|---|---|
src/main/java/com/github/esrrhs/javalua/ |
词法、语法、代码生成、虚拟机、状态、元方法、标准库 |
native/ |
luashim.c、生成的 C API stub、构建脚本 |
src/test/java/... |
单元、差分、fuzz 和原生扩展测试 |
benchmark/ |
本机的量级计时,不是 CI 闸门(理由见该目录 README) |
tools/ |
check_ci_gates.py:闸门类一条用例都没跑时直接判失败;local_conformance.sh:本地镜像 CI 的 conformance job(两条 io 路径各跑一遍、覆盖率合并后统计) |
Java 类与 C 源码一一对应:Lexer ↔ llex.c、Parser ↔ lparser.c、
Codegen ↔ lcode.c、Vm ↔ lvm.c、LuaState ↔ lstate.c/ldo.c、
Api ↔ lapi.c、LuaAux ↔ lauxlib.c,每个库文件对应一个类。
MIT