はじめに
JDK 22 で正式導入された Project Panama の FFM API により、JVM 外のネイティブコードの呼び出しが容易になりました。
簡単な例で、段階的に FFM API の使い方を見ていきます。
- Java から FFM API で Rust を呼ぶ(1/4) いまココ
- Java から FFM API で Rust を呼ぶ(2/4)
- Java から FFM API で Rust を呼ぶ(3/4)
- Java から FFM API で Rust を呼ぶ(4/4)
- Java から FFM API で Rust を呼ぶ(おまけ)
プロジェクト作成
Gradle でプロジェクト作成します(テストクラスは削除しておきます)。
mkdir ffm-api cd ffm-api gradle init --type java-application --java-version 25 --no-comments --use-defaults rm app/src/test/java/org/example/AppTest.java
Rust プロジェクトも作成します。app/src/main/rust の中に作っておきます。
mkdir -p app/src/main/rust/math_lib cd app/src/main/rust/math_lib cargo init --lib
こんな感じの構成になります。

Rust側の実装
最初に Rust 側の実装として、単なる足し算を行う関数を作成します。
共有ライブラリを出力するために cdylib を指定します。
[package] name = "math_lib" version = "0.1.0" edition = "2024" [lib] crate-type = ["cdylib"]
src/lib.rc を以下のようにします。
#[unsafe(no_mangle)] pub extern "C" fn add(a: i32, b: i32) -> i32 { a + b }
マングリングを無効化するために no_mangle を指定し、extern "C" でC言語のABI(Application Binary Interface)を指定するしています。
Java側の実装
FFM API(Foreign Function & Memory API) でRustで作成した関数を呼び出します。
テンプレート作成された App.java を以下のように編集します。
package org.example; import java.lang.foreign.*; import java.lang.invoke.MethodHandle; public class App { static void main(String[] args) throws Throwable { Arena arena = Arena.global(); SymbolLookup mathLib = SymbolLookup.libraryLookup(System.mapLibraryName("math_lib"), arena); MemorySegment addNumbersSymbol = mathLib.find("add") .orElseThrow(RuntimeException::new); MethodHandle addNumbers = Linker.nativeLinker().downcallHandle( addNumbersSymbol, FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT) ); int x = 15, y = 27; int result = (int) addNumbers.invoke(x, y); System.out.println(x + " + " + y + " = " + result); } }
System.mapLibraryName("math_lib") でプラットフォームに応じたライブラリ名が得られます。macOS の場合は libmath_lib.dylib"、Linux の場合はlibmath_lib.so、Windows の場合はmath_lib.dll` のようになります。
SymbolLookup を作成し、find("add") により作成した add 関数のハンドルを取得し、これを実行する形となります。
Gradle から Rust をコンパイルする
まず、Gradle から Rust のコンパイルを行えるようにします。
以下のような cargoBuild タスクとして登録します。
val cargoExecutable = System.getProperty("user.home") + "/.cargo/bin/cargo" val cargoTargetDir = layout.buildDirectory.dir("rust/math_lib/").get().asFile tasks.register<Exec>("cargoBuild") { description = "Builds the Rust library using Cargo" group = "rust" workingDir = layout.projectDirectory.dir("src/main/rust/math_lib").asFile inputs.dir("$workingDir/src").withPropertyName("rustSourceDir") inputs.files("$workingDir/Cargo.toml", "$workingDir/Cargo.lock").withPropertyName("cargoToml") outputs.dir(cargoTargetDir).withPropertyName("cargoTargetDir") commandLine = listOf(cargoExecutable, "build", "--release", "--target-dir", cargoTargetDir.absolutePath) }
Gradle からのビルド時には、cargo のパスが通っていないので、cargoExecutable で cargo の実行ファイルへのパスを取得しています。
cargo のビルド結果は、Gradle の流儀に従い、Gradle のビルドディレクトリに生成するようにしました。cargo の引数で --target-dir として指定しています。
これで、Rust 側のビルドは以下で行えるようになります。
./gradlew cargoBuild
Gradle から 実行準備
これで準備が整いましたが、Java 側に作成したライブラリのパスを知らせる必要があります。
JNI を使う場合は、Java の起動時に "-Djava.library.path=$rustLibDir" のように指定して System.loadLibrary で読み込む形となりますが、FFM API ではプラットフォーム別の環境変数に応じて読み込みが行われます。
macOS の場合は DYLD_LIBRARY_PATH"、Linux の場合はLD_LIBRARY_PATH、Windows の場合はPATHl` などから検索されるので、実行時に環境変数を設定します。
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform tasks.named<JavaExec>("run") { dependsOn("cargoBuild") val rustLibDir = File(cargoTargetDir, "release").absolutePath val os = DefaultNativePlatform.getCurrentOperatingSystem() if (os.isMacOsX) { environment("DYLD_LIBRARY_PATH", rustLibDir) } else if (os.isWindows) { environment("PATH", rustLibDir) } else { environment("LD_LIBRARY_PATH", rustLibDir) } }
また、FFM APIによるネイティブアクセスを許可するために --enable-native-access=ALL-UNNAMED を起動引数に追加します。
application {
mainClass = "org.example.App"
applicationDefaultJvmArgs = listOf("--enable-native-access=ALL-UNNAMED")
}
実行
build.gradle.kts 全体は以下のようになりました。
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform plugins { application } repositories { mavenCentral() } java.toolchain { languageVersion = JavaLanguageVersion.of(25) } application { mainClass = "org.example.App" applicationDefaultJvmArgs = listOf("--enable-native-access=ALL-UNNAMED") } val cargoExecutable = System.getProperty("user.home") + "/.cargo/bin/cargo" val cargoTargetDir = layout.buildDirectory.dir("rust/math_lib/").get().asFile tasks.register<Exec>("cargoBuild") { description = "Builds the Rust library using Cargo" group = "rust" workingDir = layout.projectDirectory.dir("src/main/rust/math_lib").asFile inputs.dir("$workingDir/src").withPropertyName("rustSourceDir") inputs.files("$workingDir/Cargo.toml", "$workingDir/Cargo.lock").withPropertyName("cargoToml") outputs.dir(cargoTargetDir).withPropertyName("cargoTargetDir") commandLine = listOf(cargoExecutable, "build", "--release", "--target-dir", cargoTargetDir.absolutePath) } tasks.named<JavaExec>("run") { dependsOn("cargoBuild") val rustLibDir = File(cargoTargetDir, "release").absolutePath val os = DefaultNativePlatform.getCurrentOperatingSystem() if (os.isMacOsX) { environment("DYLD_LIBRARY_PATH", rustLibDir) } else if (os.isWindows) { environment("PATH", rustLibDir) } else { environment("LD_LIBRARY_PATH", rustLibDir) } }
実行してみましょう。
$ ./gradlew run Reusing configuration cache. > Task :app:run 15 + 27 = 42
Rust で書いた add() 関数を呼び出すことができました。
まとめ
Rust で書いた add() 関数を Java から呼び出すことができました。
しかし、Java 側の実装は、MethodHandle の取得がいかにも泥臭く面倒です。
これらを解消する jextract というツールが JDK チームから提供されており、これを利用することで、外部関数の呼び出しは驚くほど簡単になります。
次回は jextract の導入手順を紹介します。