Java から FFM API で Rust を呼ぶ(3/4)


はじめに

前回は jextract を導入し、Rust で作成した関数の呼び出しを Java バインディングから行えるようにしました。

今回は cbindgen により Rust ソースからヘッダファイルの生成を自動化します。


cbindgen とは

cbindgen は、RustのコードからC/C++向けのヘッダーファイルを自動生成するツールです。

リポジトリは以下です。

github.com


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側でその結果を利用する メモリ共有について見ていきます。