Java+LLVM+FFM,无JNI、无C代码,全程Java手搓IR,JIT秒出原生指令,终端Hello World炸裂输出,编译器技能GET!全程高能无尿点,看完直接跪!
2025年最硬核的Java Advent玩法来了!往年大家都在字节码里卷来卷去,今年直接掀桌子——不用javac、不用字节码、更不用JNI去跪舔C++,我们就用纯Java,把LLVM中间码撸出来,再让LLVM自己的JIT引擎把它编译成原生指令,当场跑给你看!终端里那一声“Hello, World!”不是打印出来的,是Java亲手搓出来的机器码吼出来的!
看完这篇,你也能在简历上写“曾用Java写过一个编译器”,面试官直接给你倒茶!
先给小白五分钟补课:LLVM到底是啥?为什么Java要蹭它?
LLVM,全称“Low Level Virtual Machine”,听着像虚拟机,其实它是编译器界的乐高积木!
苹果家的clang、Rust的rustc、Swift的编译器,全是它亲儿子。
它最牛的地方在于“中间表示”——LLVM IR,一种介于高级语言和汇编之间的超级SSA(静态单赋值)语言,写起来像汇编,却能跨平台优化。
今天咱们不弄clang,就用Java的Foreign Function & Memory API(FFM,JDK22+新宠,JNI的终极替代)去调戏LLVM的C API,让Java直接生成IR,再让LLVM JIT把IR炸成x86机器码,跑在自家JVM门口,爽感堪比在星巴克里用豆汁儿做手冲!
环境一把梭:Ubuntu秒装LLVM 20,jextract一键生成Java绑定,Maven三分钟搭好舞台!
第一步,装LLVM 20,别怂,一行命令:
wget https://apt.llvm.org/llvm.sh && chmod +x llvm.sh && sudo ./llvm.sh 20 |
装完头文件+so全齐活。接着去GitHub扒官方jextract,Linux解压即用。新建Maven项目,pom里把source/target怼到25(对,就是要玩最新版),然后jextract闪亮登场:
jextract -l LLVM-20 -I /usr/include/llvm-c-20 -t com.example.llvm --output src/main/java --header-class-name LLVM /usr/include/llvm-c-20/llvm-c/Core.h …(后面一堆头文件) |
哒哒!几百个Java方法瞬间生成,C的宏、常量、函数全变Java静态方法,MemorySegment、MethodHandle安排得明明白白,JNI时代彻底拜拜!
内存段Arena:Java的“堆外内存黑卡”, native世界畅通无阻!
FFM API的灵魂是MemorySegment,而Segment必须在Arena里分配。Arena就像高级版malloc,try-with-resources自动释放,内存泄漏?不存在的!本文全程用Arena.ofConfined(),线程私有、轻量、快, arena.allocateFrom("Hello, World!") 直接把Java字符串塞进堆外内存,UTF-8零结尾,LLVM看了都说舒服。
后续全局变量、函数指针、JIT地址全在这块“黑卡”里蹦迪,Java第一次把内存玩成这么丝滑!
起手式:创建LLVM模块,打印空IR,先让lli报错给你看!
代码走起:
try (Arena arena = Arena.ofConfined()) { |
运行输出只有三行:
; ModuleID = 'hello' |
拿管道喂给lli:
java -cp … com.example.App | lli |
lli怒吼:Symbols not found: [ main ]
对,模块空得能跑火车,接下来咱就给它灌灵魂!
函数签名大保健:int main() 安排!先声明再定义,LLVM仪式感拉满!
继续加料:
var int32Type = LLVMInt32Type(); |
再打印IR,看到declare i32 @main(),lli依旧喊找不到入口,因为这只是函数“标题”,没有“正文”——basic block!
基本块BasicBlock:控制流图节点,函数体第一块砖头必须码齐!
LLVMAppendBasicBlock(mainFunc, arena.allocateFrom("entry")) |
瞬间创建entry块,此时IR里函数从declare变成define,但块里空空如也,lli报错:expected instruction opcode
好,马上塞指令!
指令Builder:LLVM里的“打字机”,指哪打哪,一行指令一杯咖啡!
LLVMCreateBuilder() 创建builder,LLVMPositionBuilderAtEnd(builder, entry) 把光标挪到entry块末尾,先来条return: |
再跑lli,安静如鸡,echo $? 返回0,世界首次清净!但咱要的是“Hello World”,不是“Hello空气”!
全局字符串常量:
@str = private constant [14 x i8] c"Hello, World!\00",Java一行代码自动搞定! |
LLVMBuildGlobalStringPtr(builder, arena.allocateFrom("Hello, World!"), arena.allocateFrom("hello_str")) |
这行神操作背后:LLVM自动在模块顶层插入@hello_str,类型[14 x i8],零结尾,private unnamed_addr,对齐1,打印IR肉眼可见!但光声明不调用,等于把奶茶封盖却不给吸管!
外部函数声明:puts老铁来自libc,先告诉LLVM“哥们长这样”!
puts签名:int puts(char*),于是: |
模块里瞬间多了一行:declare i32 @puts(ptr)
此刻,Java像极了一个认真写符号表的编译器前端,帅到没朋友!
终极CALL:LLVMBuildCall2,把hello_str塞进puts,终端炸裂输出!
创建实参数组:
var callArgs = arena.allocate(ADDRESS, 1); |
指令插入完毕,IR里entry块出现:
call i32 @puts(ptr @hello_str) |
管道喂lli,终端啪一声:Hello, World! 全网观众起立鼓掌!
JIT加速:lli只是解释器,咱要原生指令狂飙!MCJIT启动,x86机器码秒生成!
五步初始化:
LLVMLinkInMCJIT(); |
优化等级2,编译器直接拉满!随后LLVMGetPointerToGlobal(executionEngine, mainFunc) 拿到函数指针,地址塞进Java的MemorySegment,准备起飞!
MethodHandle黑科技:Java直接调用编译后的main,原生入口一毫秒都不耽误!
Linker.nativeLinker().downcallHandle(addressOfMainFunc, FunctionDescriptor.of(JAVA_INT)) |
返回一个MethodHandle,签名等于int main(),Java端直接:
int result = (int) functionHandle.invoke(); |
运行!终端先出Hello, World! 再出main() returned: 0
全程无lli、无clang、无gcc,Java一人分饰三角:前端+优化器+JIT司机,帅到模糊!
彩蛋时间:把优化等级开到3,再塞点循环向量化和内联,你能让Hello World跑出SPEC2017的既视感!
LLVM IR支持
attribute("noinline")、attribute("alwaysinline")、llvm.memset.p0i8.i64 |
FFM API还能对接SIMD、AVX512,未来Valhalla vector API一旦合体,Java将拥有官方支持的“编译器+向量化+JIT”三位一体大杀器,提前布局的你就等于提前拿到通往次世代高性能计算的船票!