
2025/12/07 20:51
Java Hello World, LLVM Edition
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
記事は、Java 開発者が新しい Foreign Function & Memory API を使って、従来の JNI を回避しつつ、Java コードから直接 LLVM 中間表現(IR)をビルド・実行・JIT コンパイルする方法を説明しています。Ubuntu/Debian に
llvm.sh スクリプトで LLVM 20 をインストールし、jextract でネイティブバインディングを生成して src/main/java に com.example.llvm.LLVM クラスを作成する手順を案内します。また、Java 25 用に Maven プロジェクトを設定します。
チュートリアルでは、まず
main 関数を定義した LLVM モジュールを作り、基本ブロックを追加し、外部 C の puts 関数を呼び出す命令を構築して、グローバル文字列定数 "Hello, World!" を渡します。生成された IR は LLVMPrintModuleToString で表示され、LLVM のインタープリター(lli)で実行して「Hello, World!」と出力されることを確認します。
メモリ処理は FFM アーケーンを使い、
MemorySegments を Java の文字列に変換する例が示されています。JIT コンパイルでは、x86 Linux 用の MCJIT エンジンを初期化し、LLVMCreateJITCompilerForModule でモジュールをオンデマンドコンパイルします。グローバル関数へのポインタは LLVMGetPointerToGlobal で取得し、Linker.nativeLinker().downcallHandle を使ってメソッドハンドルを作成し、JNI を介さずに Java から JIT コンパイルされた main を呼び出します。最終的な実行例では、メソッドハンドル経由で直接「Hello, World!」が印字されます。
さらに追加の LLVM 命令(テキスト出力や算術演算など)を試すことを奨励し、GitHub 上の完全なソースコードへのリンクも示しています。この手法により、Java アプリケーションは JNI の複雑さを回避して LLVM のパフォーマンスと柔軟性を活用できるようになり、動的コード生成やクロスプラットフォームコンパイルの可能性が広がります。
本文
Java Advent – LLVM を使って FFM API 経由で “Hello, World!” を作る
概要
- 目的: Java から LLVM IR を生成し実行して「Hello, World!」を出力する。
- ツール:
- Java ≥ 22(FFM API)
- LLVM 20+(共有ライブラリとヘッダー)
– LLVM C API の Java バインディング生成jextract- Maven – プロジェクト構築
前提条件
# Ubuntu/Debian に LLVM 20 をインストール wget https://apt.llvm.org/llvm.sh chmod +x llvm.sh ./llvm.sh 20
とヘッダーがlibLLVM-20.soにあることを確認してください。/usr/include/llvm-c-20
プロジェクト設定
mvn archetype:generate \ -DgroupId=com.example \ -DartifactId=jvm-llvm-helloworld \ -DarchetypeArtifactId=maven-archetype-quickstart \ -DinteractiveMode=false
pom.xml を編集:
<properties> <maven.compiler.source>25</maven.compiler.source> <maven.compiler.target>25</maven.compiler.target> </properties>
スタートアップアプリをビルド&実行して Java が動作することを確認します。
LLVM バインディングの生成
jextract を使用(JDK の
src.zip か jextract バイナリから取得):
jextract \ -l LLVM-20 \ -I /usr/include/llvm-c-20 \ -I /usr/include/llvm-20 \ -t com.example.llvm \ --output src/main/java \ --header-class-name LLVM \ /usr/include/llvm-c-20/llvm-c/Core.h \ /usr/include/llvm-c-20/llvm-c/Support.h \ /usr/include/llvm-c-20/llvm-c/ExecutionEngine.h \ /usr/include/llvm-c-20/llvm-c/Target.h \ /usr/include/llvm-c-20/llvm-c/TargetMachine.h
バインディングが正しく生成されたかテスト:
public static void main(String[] args) { try (Arena arena = Arena.ofConfined()) { System.out.println("LLVM version: " + LLVM.LLVM_VERSION_STRING().getString(0)); } }
ネイティブアクセスフラグを付けて実行し、警告が出ないことを確認します。
モジュールの構築
try (Arena arena = Arena.ofConfined()) { // モジュール作成 var module = LLVMModuleCreateWithName(arena.allocateFrom("hello")); // ---- 関数シグネチャ: int main() ---- var int32Type = LLVMInt32Type(); var mainType = LLVMFunctionType(int32Type, NULL, 0, 0); var mainFunc = LLVMAddFunction(module, arena.allocateFrom("main"), mainType); // ---- エントリブロック ---- var entry = LLVMAppendBasicBlock(mainFunc, arena.allocateFrom("entry")); var builder = LLVMCreateBuilder(); LLVMPositionBuilderAtEnd(builder, entry); // ---- グローバル文字列 "Hello, World!" ---- var helloStr = LLVMBuildGlobalStringPtr( builder, arena.allocateFrom("Hello, World!"), arena.allocateFrom("hello_str")); // ---- puts(char*) を宣言 ---- var paramTypes = arena.allocate(ADDRESS, 1); var charPtr = LLVMPointerType(LLVMInt8Type(), 0); paramTypes.set(ADDRESS, 0, charPtr); var putsType = LLVMFunctionType(int32Type, paramTypes, 1, 0); var putsFunc = LLVMAddFunction(module, arena.allocateFrom("puts"), putsType); // ---- puts(helloStr) を呼び出し ---- var callArgs = arena.allocate(ADDRESS, 1); callArgs.set(ADDRESS, 0, helloStr); LLVMBuildCall2(builder, putsType, putsFunc, callArgs, 1, arena.allocateFrom("puts")); // ---- return 0 ---- LLVMBuildRet(builder, LLVMConstInt(int32Type, 0, 0)); // ---- 生成した IR を表示(任意) ---- var irPtr = LLVMPrintModuleToString(module); System.out.println(irPtr.getString(0)); LLVMDisposeMessage(irPtr); // ---- 後始末 ---- LLVMDisposeBuilder(builder); LLVMDisposeModule(module); }
実行すると次のように IR が出力されます。
; ModuleID = 'hello' source_filename = "hello" @hello_str = private unnamed_addr constant [14 x i8] c"Hello, World!\00", align 1 define i32 @main() { entry: %call0 = call i32 @puts(ptr @hello_str) ret i32 0 }
この IR を
lli にパイプすると「Hello, World!」が表示されます。
JIT コンパイル&Java から実行
IR の生成直後、クリーンアップ前に以下を追加します:
// ---- LLVM JIT 初期化 (x86) ---- LLVMLinkInMCJIT(); LLVMInitializeX86Target(); LLVMInitializeX86TargetInfo(); LLVMInitializeX86TargetMC(); LLVMInitializeX86AsmPrinter(); LLVMInitializeX86AsmParser(); // ---- 実行エンジン作成 ---- var jit = arena.allocate(ADDRESS); var errPtr = arena.allocate(ADDRESS); LLVMCreateJITCompilerForModule(jit, module, 2, errPtr); // エンジンと main のアドレス取得 var execEngine = jit.get(ADDRESS, 0); var mainAddr = LLVMGetPointerToGlobal(execEngine, mainFunc); // ---- コンパイル済み関数へのメソッドハンドルを作成 ---- var mainHandle = Linker.nativeLinker().downcallHandle( mainAddr, FunctionDescriptor.of(JAVA_INT)); // 引数なし、int を返す // ---- 呼び出し ---- int ret = (int) mainHandle.invoke(); System.out.println("main() returned: " + ret);
これでプログラムは次のように出力します。
Hello, World! main() returned: 0
外部インタプリタを使わず、ネイティブマシンコードとして直接実行されます。
次のステップ
- 異なる LLVM 命令(算術演算、分岐など)を試す。
- Java だけでより大きなプログラムを生成する。
- ターゲットを ARM、macOS、Windows 等に変更して実行エンジンを切り替える。
を使って追加の LLVM モジュールや自前の C ライブラリをバインドする。jextract