#java #interop #bridge #extern #java-class #file-path

bin+lib rs4j

A small, automatic, efficient, and easy-to-use Rust to Java bridge

17 releases (9 breaking)

0.10.2 Oct 1, 2024
0.9.0 Sep 26, 2024
0.4.1 Feb 9, 2024

#62 in FFI

Download history 75/week @ 2024-09-14 882/week @ 2024-09-21 605/week @ 2024-09-28 58/week @ 2024-10-05 6/week @ 2024-10-12

1,050 downloads per month

MIT license

100KB
2.5K SLoC

Rust 2K SLoC // 0.0% comments Java 274 SLoC // 0.2% comments

rs4j

Crates.io Version docs.rs Lines of code Rustdoc Coverage

A small, automatic, efficient, and easy-to-use Rust to Java bridge.

Usage

rs4j works by using a custom language syntax to translate into Rust and Java code.

A good way to use this is to make a build script and have it auto-compile your code.

You can add the package to your build script by running:

cargo add rs4j --build --features build

You will also need to make sure you have rs4j as a normal dependency:

cargo add rs4j

And set up your Cargo.toml:

[lib]
crate-type = ["cdylib"]

Set up your build.rs:

// build.rs

use rs4j::build::BindgenConfig;
use anyhow::Result;

fn main() -> Result<()> {
    // Make a new config
    BindgenConfig::new()

        // Set the package for export
        .package("your.package.here")

        // Where to save the Rust bindings
        .bindings(format!("{}/src/bindings.rs", env!("CARGO_MANIFEST_DIR")))

        // Where the input files are
        .glob(format!("{}/bindings/**/*.rs4j", env!("CARGO_MANIFEST_DIR")))?

        // Where to save java classes (is a directory)
        .output(format!("{}/java", env!("CARGO_MANIFEST_DIR")))

        // Enable JetBrains annotations
        .annotations(true)

        // Go!
        .generate()?;

    Ok(())
}

You'll also need to set up a post build script:

// post-build.rs

use anyhow::Result;
use rs4j::build::BindgenConfig;

fn main() -> Result<()> {
    let out_path = format!("{}/generated", env!("CARGO_MANIFEST_DIR"));
    let src_path = format!("{}/java/src/generated", env!("CARGO_MANIFEST_DIR"));

    BindgenConfig::new()
        .package("com.example")
        .bindings(format!("{}/src/bindings.rs", env!("CARGO_MANIFEST_DIR")))
        .glob(format!("{}/bindings/**/*.rs4j", env!("CARGO_MANIFEST_DIR")))?
        .output(&out_path)
        .annotations(false)

        // Run post-build actions
        .post_build()?

        // Copy it to your Java project
        .copy_to(src_path)?;

    Ok(())
}
# Cargo.toml

[features]
default = []
post-build = ["rs4j/build", "anyhow"]

[[bin]]
name = "post-build"
path = "post-build.rs"
required-features = ["post-build"]

[dependencies]
anyhow = { version = "[...]", optional = true }
rs4j = "[...]"

Then, once that's done, use your lib.rs (or other file) to include the generated bindings:

// Put any imports that are needed for the bindings here.
use path::to::Dependency;

// You can even define structs in the file, just before the bindings!

// Include the generated code.
include!("bindings.rs");

And use rs4j's convenient CLI to build your project!

# Install:
cargo install rs4j --features cli

# Build:
rs4j build

# Build with Zigbuild:
rs4j build -z # or --zigbuild

# Build with more options:
rs4j build -- --target aarch64-unknown-linux-gnu # Supports all options for `cargo build`!

Syntax

The syntax of the binding language is fairly simple. You start by defining a class, then filling out its methods and items.

The basic syntax run-down is:

// This class, Thing, takes in one type parameter, `A`.
// You can omit this if it doesn't take any type parameters.
class Thing<A> {
    // This makes it so that Rust knows that the type for `A`
    // will have `Clone + Copy`. This doesn't change anything
    // on the Java side, it's just so that Rust will compile.
    bound A: Clone + Copy;

    // Here, the Rust function's name is `new`, but in Java that's
    // illegal as it is a reserved keyword. To combat this, you can
    // specify a different name for the function in Java and the real
    // one in Rust.
    [new] static fn of(value: A) -> Thing;

    // This gets the value. Since this is in snake_case, rs4j will
    // automatically convert it into camelCase, renaming this to
    // `getValue` on the Java side.
    fn get_value() -> A;

    // This marks this function as mutable, meaning in Rust it will
    // mutate the struct, as if it took a `&mut self` as an argument.
    mut fn set_value(value: A);

    // You can even include trait methods, as long as Rust can find the
    // trait it belongs to!
    fn clone() -> A;
};

Support

The following primitive* types are supported:

Rust Java
String String
str String
bool Boolean
u8 Byte
u16 Short
u32 Integer
u64 Long
u128 BigInteger
i8 Byte
i16 Short
i32 Integer
i64 Long
i128 BigInteger
f32 Float
f64 Double
() Void

* Not all are primitive, but I consider them to be.

Dependencies

~1.9–10MB
~101K SLoC