はじめに
前回は jextract を導入し、Rust で作成した関数の呼び出しを Java バインディングから行えるようにしました。
- 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 を呼ぶ(おまけ)
今回は cbindgen により Rust ソースからヘッダファイルの生成を自動化します。
cbindgen とは
cbindgen は、RustのコードからC/C++向けのヘッダーファイルを自動生成するツールです。
リポジトリは以下です。
cbindgen の導入
app/src/main/rust/math_lib/Cargo.toml に cbindgen を追加します。
[package] name = "math_lib" version = "0.1.0" edition = "2024" [lib] crate-type = ["cdylib"] [build-dependencies] cbindgen = "0.29.4"
app/src/main/rust/math_lib/build.rs を以下のように作成します。
use std::env; use std::path::PathBuf; fn main() { println!("cargo:rerun-if-changed=src"); let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); let output_file = PathBuf::from(&crate_dir).join("src/math_lib.h"); cbindgen::Builder::new() .with_crate(crate_dir) .with_language(cbindgen::Language::C) .with_include_guard("MATH_LIB_H") .with_autogen_warning("/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */") .generate() .expect("Unable to generate bindings") .write_to_file(output_file); }
src/math_lib.h にヘッダファイルを生成するように設定しています。
cargo build で、前回手動で作成していたヘッダーファイルが自動生成されます。
生成されたヘッダーファイルは以下のようになります。
#ifndef MATH_LIB_H #define MATH_LIB_H /* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */ #include <stdarg.h> #include <stdbool.h> #include <stdint.h> #include <stdlib.h> int32_t add(int32_t a, int32_t b); #endif /* MATH_LIB_H */
生成されたヘッダーファイルで実行
実行してみましょう。
./gradlew run > Task :app:run 15 + 27 = 42
大丈夫そうです。
最終的には以下のコードになりました。
app/src/main/java/org/example/App.java
package org.example; import com.example.math_lib.math_lib_h; public class App { static void main(String[] args) { int x = 15; int y = 27; int result = math_lib_h.add(x, y); System.out.println(x + " + " + y + " = " + result); } }
app/src/main/rust/math_lib/Cargo.toml
[package] name = "math_lib" version = "0.1.0" edition = "2024" [lib] crate-type = ["cdylib"] [build-dependencies] cbindgen = "0.29.4"
app/src/main/rust/math_lib/build.rs
use std::env; use std::path::PathBuf; fn main() { println!("cargo:rerun-if-changed=src"); let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); let output_file = PathBuf::from(&crate_dir).join("src/math_lib.h"); cbindgen::Builder::new() .with_crate(crate_dir) .with_language(cbindgen::Language::C) .with_include_guard("MATH_LIB_H") .with_autogen_warning("/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */") .generate() .expect("Unable to generate bindings") .write_to_file(output_file); }
app/src/main/rust/math_lib/src/lib.rs
#[unsafe(no_mangle)] pub extern "C" fn add(a: i32, b: i32) -> i32 { a + b }
最後に、app/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) } } val os = DefaultNativePlatform.getCurrentOperatingSystem() val arch = DefaultNativePlatform.getCurrentArchitecture() tasks.register<Copy>("downloadJextract") { description = "download jextract" val url = when { os.isMacOsX && arch.isArm64 -> "https://download.java.net/java/early_access/jextract/25/2/openjdk-25-jextract+2-4_macos-aarch64_bin.tar.gz" os.isMacOsX && arch.isAmd64 -> "https://download.java.net/java/early_access/jextract/25/2/openjdk-25-jextract+2-4_macos-x64_bin.tar.gz" os.isLinux && arch.isArm64 -> "https://download.java.net/java/early_access/jextract/25/2/openjdk-25-jextract+2-4_linux-aarch64_bin.tar.gz" os.isLinux && arch.isAmd64 -> "https://download.java.net/java/early_access/jextract/25/2/openjdk-25-jextract+2-4_linux-x64_bin.tar.gz" os.isWindows && arch.isAmd64 -> "https://download.java.net/java/early_access/jextract/25/2/openjdk-25-jextract+2-4_windows-x64_bin.tar.gz" else -> throw Error("Unsupported OS: $os, ARCH: $arch") } val tgz = layout.buildDirectory.file("jextract.tar.gz") tgz.get().asFile.apply { if (!exists()) { parentFile.mkdirs() uri(url).toURL().openStream().use { input -> outputStream().use { out -> input.copyTo(out) } } } } from(tarTree(resources.gzip(tgz.get()))) into(layout.buildDirectory.dir("jextract")) } val jextractOutputDir = layout.buildDirectory.dir("generated/main/java") tasks.register<Exec>("jextract") { dependsOn("downloadJextract") group = "jextract" description = "Generates Java bindings from C header using jextract" doFirst { mkdir(jextractOutputDir.get()) } inputs.dir("$workingDir/src").withPropertyName("rustSourceDir") inputs.files("$workingDir/Cargo.toml", "$workingDir/Cargo.lock").withPropertyName("cargoToml") outputs.dir(jextractOutputDir).withPropertyName("jextractOutputDir") commandLine( layout.buildDirectory.dir("jextract/jextract-25/bin/jextract").get().asFile.absolutePath + if (os.isWindows) ".bat" else "", layout.projectDirectory.dir("src/main/rust/math_lib/src/math_lib.h").asFile.absolutePath, "--output", jextractOutputDir.get().asFile.absolutePath, "-t", "com.example.math_lib", "-l", "math_lib" ) } tasks.named("compileJava") { dependsOn("jextract") } sourceSets { main { java { srcDir(jextractOutputDir) } } }
まとめ
Java から FFM API で Rust で作成した関数を呼び出す例をみてきました。
ビルドスクリプトさえ作ってしまえば、面倒な操作は自動化できるので、簡単に外部関数を呼び出せますので、参考にしてください。
次回は、Java側で確保したメモリにRust側でデータを書き込み、Java側でその結果を利用する メモリ共有について見ていきます。