Skip to content
Merged

FFM #54

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/workflows/rust-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,5 @@ jobs:
with:
run_clippy: true
minimum_coverage: "0"
additional_system_deps: "libjna-java"
cargo_command_env_vars: "PATH=$JAVA_HOME_21_X64/bin:$PATH CLASSPATH=/usr/share/java/jna.jar"
cargo_command_env_vars: "PATH=$JAVA_HOME_25_X64/bin:$PATH JDK_JAVA_OPTIONS=--enable-native-access=ALL-UNNAMED"
secrets: inherit
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
## 0.4.0

- switched from JNA generated bindings to FFM ones. Benchmarked performance speedup (via upstream benchmark suite) is from 4.2x-426x.

### Breaking

- the "major" (pre-release) bump is primarily because the switch from JNA to FFM is so foundational.
- Java 22+ is required (though 21+ could work using preview features).

## 0.3.1

- remove spinlock where Java checks for Rust future completion (uses a thenCompose based CF chain to prevent unnecessary blocking)
Expand Down
24 changes: 22 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "uniffi-bindgen-java"
version = "0.3.1"
version = "0.4.0"
authors = ["IronCore Labs <info@ironcorelabs.com>"]
readme = "README.md"
license = "MPL-2.0"
Expand All @@ -14,10 +14,12 @@ edition = "2024"
[lib]
name = "uniffi_bindgen_java"
path = "src/lib.rs"
bench = false

[[bin]]
name = "uniffi-bindgen-java"
path = "src/main.rs"
bench = false

[dependencies]
anyhow = "1"
Expand Down Expand Up @@ -48,6 +50,8 @@ uniffi-example-custom-types = { git = "https://github.com/mozilla/uniffi-rs.git"
uniffi-example-futures = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" }
uniffi-example-geometry = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" }
uniffi-example-rondpoint = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" }
uniffi-example-sprites = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" }
uniffi-example-todolist = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" }
uniffi-fixture-benchmarks = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" }
uniffi-fixture-coverall = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" }
uniffi-fixture-ext-types = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" }
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ Generate [UniFFI](https://github.com/mozilla/uniffi-rs) bindings for Java.

Official Kotlin bindings already exist, which can be used by any JVM language including Java. The Java specific bindings use Java-native types where possible for a more ergonomic interface, for example the Java bindings use `CompletableFutures` instead of `kotlinx.coroutines`.

Generated bindings use Java's [Foreign Function & Memory API](https://docs.oracle.com/en/java/javase/21/core/foreign-function-and-memory-api.html) (Project Panama) instead of JNA. See [benches/](benches/) for performance comparisons with Kotlin, Python, and Swift.

We highly reccommend you use [UniFFI's proc-macro definition](https://mozilla.github.io/uniffi-rs/latest/proc_macro/index.html) instead of UDL where possible.

## Requirements

* Java 20+: `javac`, and `jar`
* The [Java Native Access](https://github.com/java-native-access/jna#download) JAR downloaded and its path added to your `$CLASSPATH` environment variable.
* Java 22+: `javac`, and `jar`
* At runtime, the JVM must be allowed to use the Foreign Function & Memory API. For classpath-based applications, pass `--enable-native-access=ALL-UNNAMED` to `java`. For JPMS modules, use `--enable-native-access=your.module.name`. See [Java's documentation](https://docs.oracle.com/en/java/javase/25/core/restricted-methods.html#GUID-080FE2FA-F96A-4987-B4E1-A9F089D11B54__GUID-70A202F4-46C0-4D4D-8CD0-9D147854F776) for more information.

## Installation

Expand Down Expand Up @@ -115,7 +117,7 @@ Arguments:

## Integrating Bindings

After generation you'll have an `--out-dir` full of Java files. Package those into a `.jar` using your build tools of choice, and the result can be imported and used as per normal in any Java project with the `JNA` dependency available.
After generation you'll have an `--out-dir` full of Java files. Package those into a `.jar` using your build tools of choice, and the result can be imported and used as per normal in any Java project. The generated code uses the Foreign Function & Memory API (no external dependencies like JNA are required).

Any top level functions in the Rust library will be static methods in a class named after the crate.

Expand All @@ -131,8 +133,7 @@ The generated Java can be configured using a `uniffi.toml` configuration file.
| `custom_types` | | A map which controls how custom types are exposed to Java. See the [custom types section of the UniFFI manual](https://mozilla.github.io/uniffi-rs/latest/udl/custom_types.html#custom-types-in-the-bindings-code) |
| `external_packages` | | A map of packages to be used for the specified external crates. The key is the Rust crate name, the value is the Java package which will be used referring to types in that crate. See the [external types section of the manual](https://mozilla.github.io/uniffi-rs/latest/udl/ext_types_external.html#kotlin) |
| `rename` | | A map to rename types, functions, methods, and their members in the generated Java bindings. See the [renaming section](https://mozilla.github.io/uniffi-rs/latest/renaming.html). |
| `android` | `false` | Used to toggle on Android specific optimizations (warning: not well tested yet) |
| `android_cleaner` | `android` | Use the `android.system.SystemCleaner` instead of `java.lang.ref.Cleaner`. Fallback in both instances is the one shipped with JNA. |
| `android` | `false` | Generate [PanamaPort](https://github.com/vova7878/PanamaPort)-compatible code for Android. Replaces `java.lang.foreign.*` with `com.v7878.foreign.*` and `java.lang.invoke.VarHandle` with `com.v7878.invoke.VarHandle`. Requires PanamaPort `io.github.vova7878.panama:Core` as a runtime dependency and Android API 26+. |
| `omit_checksums` | `false` | Whether to omit checking the library checksums as the library is initialized. Changing this will shoot yourself in the foot if you mixup your build pipeline in any way, but might speed up initialization. |

### Example
Expand Down
72 changes: 37 additions & 35 deletions benches/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ Criterion-based benchmarks measuring FFI call overhead for the generated Java bi

## Prerequisites

Rust toolchain, JDK 21+, and JNA on the `CLASSPATH` — all provided by `nix develop`.
Rust toolchain and JDK 21+ — all provided by `nix develop`.

## Running

```bash
cargo bench # full suite
cargo bench -- call-only # filter by name
cargo bench -- --save-baseline before # save a Criterion baseline
cargo bench -- --load-baseline before # compare against a saved baseline
cargo bench # full suite
cargo bench -- call-only # filter by name
cargo bench -- --save-baseline before # save a Criterion baseline
cargo bench -- --load-baseline before # compare against a saved baseline
```

## How It Works
Expand All @@ -35,40 +35,42 @@ Criterion runs inside the Rust fixture library. The Java side implements `TestCa

HTML reports are written to `target/criterion/`. Raw data persists across runs for regression detection.

### Pre-results
### Comparison: Java FFM vs JNA and upstream languages

A run of the benchmarks (and comparison to upstream) on March 26, 2026 with an M4 Max 2024 Macbook Pro, 36GB memory. There was an upstream error in Python's `nested-data` bench at this time. All calls are in microseconds unless noted.
April 1, 2026 on an M4 Max 2024 MacBook Pro, 36GB memory. Java FFM uses JDK 25 with the Foreign Function & Memory API. Java JNA uses JDK 21. Kotlin, Python, and Swift results are from upstream `uniffi-rs` main.

#### Function Calls (foreign code calling Rust)

| Test Case | Java | Kotlin | Python | Swift |
|--------------------|----------|----------|----------|----------|
| call-only | 2.1 | 2.8 | 810 ns | 172 ns |
| primitives | 1.8 | 3.0 | 1.4 | 195 ns |
| strings | 14.7 | 16.8 | 9.2 | 973 ns |
| large-strings | 15.9 | 21.5 | 12.5 | 1.3 |
| records | 14.5 | 16.6 | 18.9 | 2.6 |
| enums | 12.6 | 16.7 | 14.6 | 2.1 |
| vecs | 15.6 | 17.2 | 18.3 | 4.4 |
| hash-maps | 12.3 | 16.9 | 23.4 | 6.3 |
| interfaces | 8.6 | 13.8 | 4.3 | 396 ns |
| trait-interfaces | 9.2 | 12.8 | 4.4 | 461 ns |
| nested-data | 13.5 | 17.9 | --- | 20.6 |
| errors | 4.7 | 7.4 | 3.2 | 642 ns |
| Test Case | Java FFM | Java JNA | Kotlin | Python | Swift |
|------------------|----------|-----------|-----------|-----------|-----------|
| call-only | 4.3 ns | 1.83 us | 2.89 us | 792 ns | 165 ns |
| primitives | 4.5 ns | 1.62 us | 3.27 us | 1.39 us | 191 ns |
| strings | 324 ns | 11.46 us | 15.08 us | 9.06 us | 929 ns |
| large-strings | 3.69 us | 15.63 us | 21.69 us | 12.33 us | 1.28 us |
| records | 371 ns | 11.62 us | 15.29 us | 18.66 us | 2.54 us |
| enums | 334 ns | 14.47 us | 15.41 us | 14.22 us | 2.04 us |
| vecs | 581 ns | 13.14 us | 15.36 us | 18.02 us | 4.55 us |
| hash-maps | 638 ns | 12.91 us | 15.48 us | 23.15 us | 6.39 us |
| interfaces | 530 ns * | 6.07 us | 105.60 us | 4.21 us | 409 ns |
| trait-interfaces | 532 ns * | 11.98 us | 150.70 us | 4.30 us | 477 ns |
| nested-data | 1.90 us | 12.56 us | 15.64 us | 77.09 us | 20.86 us |
| errors | 682 ns | 4.51 us | 6.44 us | 3.20 us | 646 ns |

#### Callbacks (Rust calling foreign code)

| Test Case | Java | Kotlin | Python | Swift |
|--------------------|----------|----------|----------|----------|
| call-only | 2.6 | 3.2 | 477 ns | 121 ns |
| primitives | 4.3 | 3.9 | 793 ns | 166 ns |
| strings | 13.5 | 18.0 | 8.0 | 811 ns |
| large-strings | 18.2 | 22.6 | 10.7 | 1.6 |
| records | 16.6 | 17.5 | 14.0 | 2.8 |
| enums | 17.0 | 17.5 | 11.0 | 2.4 |
| vecs | 16.9 | 17.9 | 18.1 | 4.8 |
| hash-maps | 12.7 | 18.1 | 21.4 | 7.1 |
| interfaces | 7.8 | 35.6 | 4.1 | 429 ns |
| trait-interfaces | 11.1 | 27.2 | 4.3 | 528 ns |
| nested-data | 21.8 | 16.8 | --- | 24.1 |
| errors | 10.4 | 8.9 | 4.3 | 566 ns |
| Test Case | Java FFM | Java JNA | Kotlin | Python | Swift |
|------------------|----------|-----------|-----------|-----------|-----------|
| call-only | 55 ns | 1.98 us | 2.76 us | 492 ns | 121 ns |
| primitives | 59 ns | 2.66 us | 3.40 us | 804 ns | 166 ns |
| strings | 348 ns | 13.76 us | 15.69 us | 8.17 us | 811 ns |
| large-strings | 3.84 us | 17.47 us | 20.55 us | 10.77 us | 1.60 us |
| records | 449 ns | 11.55 us | 15.95 us | 14.26 us | 2.86 us |
| enums | 406 ns | 11.17 us | 14.93 us | 11.04 us | 2.47 us |
| vecs | 593 ns | 11.11 us | 15.02 us | 17.95 us | 5.01 us |
| hash-maps | 723 ns | 12.36 us | 15.72 us | 21.39 us | 7.16 us |
| interfaces | 964 ns * | 7.44 us | 408.65 us | 4.19 us | 448 ns |
| trait-interfaces | 790 ns * | 7.49 us | 361.69 us | 4.33 us | 552 ns |
| nested-data | 1.93 us | 19.77 us | 16.29 us | 67.20 us | 24.54 us |
| errors | 638 ns | 6.37 us | 7.98 us | 4.35 us | 574 ns |

\* Interface benchmarks have high variance due to GC pauses; min values are ~250-470ns.
Loading
Loading