从零写一个多后端编译器:Java 实现字节码、分层 JIT、GC 和 GraalVM Polyglot 跨语言互操作
UniVM 是一个编译器实验项目:多前端 → 统一字节码 → 解释器 + 分层 JIT → C / C++ / Go / JAR 多后端。
M1–M6 全部验证通过,JIT 加速 2.47x,OSR 1.97x,并已在 GraalVM Polyglot 上跑通跨语言互操作的第一步。
一、为什么做这个项目
编译器是计算机科学里最容易被"敬而远之"的方向之一。教科书讲龙书、讲 LLVM IR、讲 SSA,但真正动手写一个能跑、能优化、能多端输出的编译器,中间有大量工程细节没人告诉你。
UniVM 的出发点是:用一套最小的架构,把编译器的完整链路走一遍。
它的设计模型借鉴 JVM / GraalVM:
每种语言写一个前端 → 全部落到同一套栈式字节码 → 一个 VM(解释器 + JIT)→ 多个后端输出。
不存在"一个编译器编译所有语言"。JVM 能跑 Java / Kotlin / Scala,靠的就是"多前端 + 单字节码 + 单 VM"。
目前 M1–M6 已经全部打通并多端验证一致,M7 在推进 GraalVM Polyglot 跨语言互操作。
二、整体架构
text
.uni / .py 源码
↓ Lexer + Parser(按扩展名选前端)
AST
↓ TypeChecker(INT / ARRAY 检查)
Module
↓ Codegen(栈式指令 + 结构化控制节点)
字节码
↓
解释器 (Vm) ←→ 分层 JIT (Jit) ←→ Heap + GC
↓
C / C++ / Go / JAR 四种后端
核心文件都在 src/ 下:
模块 文件
.uni 前端 Lexer.java Parser.java
Python 前端 LexerPy.java ParserPy.java
AST Ast.java
类型检查 TypeChecker.java
字节码生成 Codegen.java
解释器 Vm.java
分层 JIT Jit.java
堆 + GC Heap.java ArrayTypes.java
后端 BackendC.java BackendCpp.java BackendGo.java BackendJava.java
整个项目只有 17 个 Java 文件,127 KB 源码。不依赖任何第三方库。
三、多前端:同一套 AST
UniVM 有两种前端:
.uni 前端:类 C 语法,递归下降解析,支持 let、赋值、print、if/elif/else、while、func、return、表达式语句、数组。
Python 前端:缩进块、def、elif,产出同一套 AST。
关键点是:前端只负责把源码变成 AST,不关心后端。 两种前端落到同一个 Module,之后所有阶段完全复用。
M5 验证里,py_demo.py 经 Python 前端编译后,解释器 / jar / Go 三个后端输出完全一致:
text
7 120 3628800 55 3 2 1 5050
四、分层 JIT:2.47x 加速
Jit.java 实现了一个简单的分层 JIT:
Tier 0:解释器,逐条执行字节码。
Tier 1:当某个函数被调用超过阈值(1000 次)时,编译成一份"扁平化"的表示,跳过解释器循环。
OSR(On-Stack Replacement):对长循环,不等到函数被再次调用,直接在循环回边达到阈值时替换栈上帧。
M4 验证里,jit.uni 里的 work 函数被调用 3001 次,第 1000 次触发编译:
模式 耗时
解释器 2914 ms
Tier 1 JIT 1179 ms
加速比 2.47x
M4b 验证 OSR,osr.uni 函数只调用 1 次,但回边执行 200 万次:
模式 耗时
解释器 325 ms
OSR 165 ms
加速比 1.97x
这两个数据不算惊艳——真正的 JVM C2 编译器加速比往往是 10x 以上。但它们的意义在于验证了分层 JIT 的核心机制:热点检测、编译触发、扁平化表示、栈上替换。
–profile 参数会打印调用计数、C1/OSR 编译、Tier2 状态与堆/GC 统计。在本机(普通 OpenJDK,无 GraalVM JVMCI)时,它如实报告 Tier2 不可用。
五、类型系统与追踪式 GC
M6 加了两个东西:
类型检查(TypeChecker.java):作用域符号表,只支持 INT 和 ARRAY 两种类型,检查未定义变量、实参数量、操作数类型。–no-check 可以关掉。
堆 + GC(Heap.java ArrayTypes.java):数组是引用类型,[n] 分配、a[i] 读写、len(a) 取长度。GC 是保守追踪式的:
根 = 解释器与编译码各帧的局部变量 + 操作数栈
标记:保守扫描,数据恰好等于句柄只会多留,不会误释放
回收:每 100 次分配触发一次
M6 验证里 arrays.uni 输出 30 499 10,502 次分配、5 次 GC、每次回收约 100 个对象。jar 和 Go 后端输出一致。
六、多后端:一套字节码,四种输出
后端 输出 数组表示 GC
C .c long long* 无(需手动)
C++ .h + .cpp vector* 无
Go .exe []int64 Go 自带
Java .jar long[] JVM 自带
每个后端都是独立的类,把 Module 翻译成对应语言的源码,再调用外部工具链(gcc / g++ / go build / javac)编译。
M3 验证里,funcs.uni 在解释器 / jar / Go 三个后端输出完全一致:
text
7 120 3628800 55
多后端一致性的意义:它证明字节码语义定义是清晰的。如果同一份字节码在解释器和编译产物上跑出不同结果,说明语义有歧义。这个约束逼着我在设计字节码时把每个指令的语义想清楚。
七、M7:GraalVM Polyglot 跨语言互操作
M7 的目标是让 Uni 编译出的函数能被 JavaScript、Python 等 guest language 直接调用。第一步已经在 polyglot/PolyglotDemo.java 里跑通:
java
try (Context context = Context.newBuilder()
.allowAllAccess(true)
.build()) {
// 1. Java 函数暴露给 JS context.getBindings("js").putMember("addOne", (java.util.function.IntUnaryOperator) x -> x + 1); int r1 = context.eval("js", "addOne(41)").asInt(); // 2. 模拟 Uni 堆对象暴露给 JS context.getBindings("js").putMember("uniHeap", new UniHeapStub()); int r2 = context.eval("js", "uniHeap.load(7) + uniHeap.load(35)").asInt(); // 3. 数组跨语言传递 context.getBindings("js").putMember("makeArray", (java.util.function.IntFunction<int[]>) n -> { int[] a = new int[n]; for (int i = 0; i < n; i++) a[i] = i * i; return a; }); Value arr = context.eval("js", "makeArray(5)");}
输出:
text
==== UniVM Polyglot Demo ====
[JS -> Java] addOne(41) = 42
[JS -> UniHeap] load(7) + load(35) = 44
[JS -> Java array] squares = 0 1 4 9 16
==== Demo 完成 ====
下一步是把真实的 Codegen 输出注册为 Value,让 JS 直接调用 .uni 文件里定义的函数。
八、踩过的坑
坑一:GraalVM JDK 不自带 Polyglot API
从 GraalVM for JDK 21 (23.1) 开始,Polyglot API 不再捆绑在 JDK 里,而是通过 Maven Central 分发。需要显式加依赖:
xml
org.graalvm.polyglot
polyglot
23.1.12
org.graalvm.polyglot
js
23.1.12
pom
坑二:Windows 路径 Bug
用 23.1.1 版本时,在 Windows 上报:
text
java.nio.file.InvalidPathException:
Illegal char <:> at index 2:
/D:/UniVM/UniVM/polyglot/target/classes/
23.1.2 版本移除了引发问题的 classpath 隔离代码,Bug 消失。
坑三:版本必须与 JDK 严格对齐
用 23.1.2 时又报:
text
Polyglot version compatibility check failed.
Your Java runtime ‘21.0.12+7-LTS-jvmci-23.1-b96’
with compiler version ‘23.1.12’
is incompatible with polyglot version ‘23.1.2’.
JDK 补丁版本和 Polyglot artifact 版本必须同步升级。 改成 23.1.12 后一切正常。
坑四:PowerShell 写文件的 BOM 问题
Set-Content -Encoding UTF8 会写入 BOM,javac 报 非法字符: ‘\ufeff’。解法:
powershell
[System.IO.File]::WriteAllText($path,content,(New−ObjectSystem.Text.UTF8Encoding(content, (New-Object System.Text.UTF8Encoding(content,(New−ObjectSystem.Text.UTF8Encoding(false)))
坑五:网络
本地 git push 到 GitHub 一直 Connection was reset。最后的方案是:先把代码推到 Gitee,再从 Gitee 导入到 GitHub。
九、现状与路线图
已完成:
✅ M1–M3:变量 / 控制流 / 函数栈帧 + 多后端
✅ M4:分层 JIT(2.47x)
✅ M4b:OSR(1.97x)+ Tier2 能力探测
✅ M5:多语言前端(Python 风格)
✅ M6:类型系统 + 追踪式 GC
✅ M7 第一步:Polyglot 互操作最小验证
待完成:
⬜ M7 完整:把 Uni 编译产物注册为 Value
⬜ 字符串统一到 TruffleString
⬜ 真实 Heap 实现 Polyglot 数组协议
⬜ Tier2:换 GraalVM 后启用 JVMCI 机器码
十、项目信息
GitHub:https://github.com/liyinqi1917/Uni_vm
Gitee 镜像:https://gitee.com/liyinqi1971/univm
许可证:MIT
环境:JDK 17+(主项目);GraalVM JDK 21 + Maven 3.9+(Polyglot 子项目)
如果你对编译器、JIT、GC、GraalVM 感兴趣,欢迎来看代码。CONTRIBUTING.md 里列了几个具体任务,从简单的到较难的都有。