Skip to content
Merged

FFM #54

Show file tree
Hide file tree
Changes from 14 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
2 changes: 1 addition & 1 deletion .github/workflows/rust-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ jobs:
run_clippy: true
minimum_coverage: "0"
additional_system_deps: "libjna-java"
Comment thread
skeet70 marked this conversation as resolved.
Outdated
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
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.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 21+: `javac`, and `jar`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this needs to be 22

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the current shape of the FFM API is actually available in 21+, it left preview in 22+. PanamaPort lists 22+ for the Android support part though, so might as well list that as well.

* 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.
171 changes: 171 additions & 0 deletions benches/bench_compare.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
#!/usr/bin/env python3
"""Parse Criterion benchmark output files and produce comparison tables.

Usage:
python3 bench_compare.py "Column Title" path/to/bench.txt ["Title2" path2.txt ...]
python3 bench_compare.py --speedup "Faster" file1.txt "Slower" file2.txt
nix shell nixpkgs#python3 -c python3 bench_compare.py ...

The language prefix (e.g. "java-") is extracted from bench names and appended
to the column title automatically.

With --speedup, exactly two columns are expected and the output is a speedup
table showing how much faster the first column is vs the second (ratio > 1
means first is faster, < 1 means second is faster).
"""

import re
import sys
from collections import OrderedDict

UNIT_TO_NS = {'ns': 1, 'µs': 1e3, 'us': 1e3, 'ms': 1e6, 's': 1e9}


def parse_bench_file(path):
"""Parse a Criterion benchmark output file.

Returns (language, results) where results maps
(category, bench_name) -> nanoseconds as float.
"""
results = {}
current_bench = None
language = None

name_re = re.compile(r'^(\S+?)/(\w+?)-(\S+?)(?:\s+time:|\s*$)')
time_re = re.compile(r'time:\s+\[[\d.]+ \S+ ([\d.]+) (ns|µs|us|ms|s) [\d.]+ \S+\]')

with open(path) as f:
for line in f:
stripped = line.strip()

name_m = name_re.match(stripped)
if name_m:
language = language or name_m.group(2)
current_bench = (name_m.group(1), name_m.group(3))

time_m = time_re.search(line)
if time_m and current_bench:
ns = float(time_m.group(1)) * UNIT_TO_NS[time_m.group(2)]
results[current_bench] = ns
current_bench = None

return language, results


def format_time(ns):
if ns < 1000:
return f"{ns:.2f} ns" if ns >= 10 else f"{ns:.1f} ns"
elif ns < 1e6:
return f"{ns / 1e3:.2f} us"
elif ns < 1e9:
return f"{ns / 1e6:.2f} ms"
else:
return f"{ns / 1e9:.2f} s"


def format_speedup(ratio):
if ratio >= 10:
return f"{ratio:.0f}x"
else:
return f"{ratio:.1f}x"


def collect_categories(all_results):
categories = OrderedDict()
for results in all_results:
for cat, bench in results:
if cat not in categories:
categories[cat] = OrderedDict()
if bench not in categories[cat]:
categories[cat][bench] = True
return categories


def print_comparison(columns, all_results):
categories = collect_categories(all_results)

for cat, benches in categories.items():
print(f"### {cat}\n")
header = "Test Case | " + " | ".join(columns)
sep = "-- | " + " | ".join("--" for _ in columns)
print(header)
print(sep)

for bench in benches:
values = []
for results in all_results:
ns = results.get((cat, bench))
values.append(format_time(ns) if ns is not None else "")
print(f"{bench} | " + " | ".join(values))

print()


def print_speedup(columns, all_results):
categories = collect_categories(all_results)
a_results, b_results = all_results
title = f"{columns[0]} vs {columns[1]} Speedup"

cat_names = list(categories.keys())

print(f"### {title}\n")
header = "Test Case | " + " | ".join(f"{cat} Speedup" for cat in cat_names)
sep = "-- | " + " | ".join("--" for _ in cat_names)
print(header)
print(sep)

all_benches = OrderedDict()
for benches in categories.values():
for b in benches:
all_benches[b] = True

for bench in all_benches:
values = []
for cat in cat_names:
a_ns = a_results.get((cat, bench))
b_ns = b_results.get((cat, bench))
if a_ns is not None and b_ns is not None and a_ns > 0:
values.append(format_speedup(b_ns / a_ns))
else:
values.append("")
print(f"{bench} | " + " | ".join(values))

print()


def main():
args = sys.argv[1:]

speedup = False
if "--speedup" in args:
speedup = True
args.remove("--speedup")

if len(args) < 2 or len(args) % 2 != 0:
print("Usage: bench_compare.py [--speedup] \"Title1\" file1.txt [\"Title2\" file2.txt ...]",
file=sys.stderr)
sys.exit(1)

if speedup and len(args) != 4:
print("--speedup requires exactly two title/file pairs", file=sys.stderr)
sys.exit(1)

columns = []
all_results = []
for i in range(0, len(args), 2):
title = args[i]
path = args[i + 1]
lang, results = parse_bench_file(path)
if lang:
title = f"{title} ({lang.capitalize()})"
columns.append(title)
all_results.append(results)

if speedup:
print_speedup(columns, all_results)
else:
print_comparison(columns, all_results)


if __name__ == "__main__":
main()
Loading
Loading