Skip to content
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 0.4.1

- fix resolving callback trait implementations declared in submodules of the current crate. Previously, traits like `my_crate::metrics::MetricsRecorder` failed with "no interface with module_path" during code generation.
- fix Java keyword collisions in generated callback helper class names. Callback methods named after Java keywords (e.g., `record`) no longer produce invalid nested type declarations. Thanks @criccomini for this and the above fix!
- fix `libraryOverride` system property to work with absolute paths. Previously, passing an absolute path via `-Duniffi.component.<namespace>.libraryOverride=/path/to/lib.so` would fail because it was always passed to `System.loadLibrary()`. Now absolute paths are correctly routed to `System.load()`.

## 0.4.0

- switched from JNA generated bindings to FFM ones. Benchmarked performance speedup (via upstream benchmark suite) is from 4.2x-426x.
Expand Down
2 changes: 1 addition & 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.4.0"
version = "0.4.1"
authors = ["IronCore Labs <info@ironcorelabs.com>"]
readme = "README.md"
license = "MPL-2.0"
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,18 @@ lower = "{}.toString()"
rust-crate-name = "java.package.name"
```

## Library Loading

By default, the generated code uses `System.loadLibrary()` to find the native library via `java.library.path`. If your native library lives outside the standard search paths or automatic discovery doesn't work for your environment, you can specify an absolute path at runtime using a system property:

```
java -Duniffi.component.<namespace>.libraryOverride=/path/to/libmylib.so ...
```

Where `<namespace>` is the UniFFI namespace of your component (e.g., `arithmetic`). When the override is an absolute path, the generated code uses `System.load()` instead of `System.loadLibrary()`, bypassing `java.library.path` entirely.

You can also pass a plain library name as the override, in which case it behaves like `System.loadLibrary()` and still requires the library to be on `java.library.path`.

## Notes

- failures in CompletableFutures will cause them to `completeExceptionally`. The error that caused the failure can be checked with `e.getCause()`. When implementing an async Rust trait in Java, you'll need to `completeExceptionally` instead of throwing. See `TestFixtureFutures.java` for an example trait implementation with errors.
Expand Down
10 changes: 9 additions & 1 deletion src/templates/NamespaceLibraryTemplate.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@ static synchronized String findLibraryName(String componentName) {
}

static java.lang.foreign.SymbolLookup loadLibrary() {
System.loadLibrary(findLibraryName("{{ ci.namespace() }}"));
String name = findLibraryName("{{ ci.namespace() }}");
if (name.startsWith("/") // Unix absolute path
|| name.startsWith("\\\\") // Windows UNC path
|| (name.length() > 2 && name.charAt(1) == ':')) // Windows drive path (e.g. C:\)
{
System.load(name);
} else {
System.loadLibrary(name);
}
return java.lang.foreign.SymbolLookup.loaderLookup();
}

Expand Down
100 changes: 100 additions & 0 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,97 @@ fn run_test(fixture_name: &str, test_file: &str) -> Result<()> {
Ok(())
}

/// Run a test using an absolute path library override instead of java.library.path.
/// This validates that the generated loadLibrary() code uses System.load() for absolute paths.
fn run_test_with_library_override(
fixture_name: &str,
test_file: &str,
namespace: &str,
) -> Result<()> {
let test_path = Utf8Path::new(".").join("tests").join(test_file);
let test_helper = UniFFITestHelper::new(fixture_name)?;
// Use a synthetic path for out_dir so it doesn't collide with run_test's out_dir
// (create_out_dir is deterministic based on the path).
let out_dir_key = Utf8Path::new(".")
.join("tests")
.join("library_override")
.join(test_file);
let out_dir = test_helper.create_out_dir(env!("CARGO_TARGET_TMPDIR"), &out_dir_key)?;
let cdylib_path = test_helper.cdylib_path()?;

let mut paths = BindgenPaths::default();
paths.add_cargo_metadata_layer(false)?;
let loader = BindgenLoader::new(paths);

generate(
&loader,
&GenerateOptions {
source: cdylib_path.clone(),
out_dir: out_dir.clone(),
format: true,
crate_filter: None,
},
)?;

// Copy the cdylib to a known absolute path (no symlink needed since we pass the full path)
let native_lib_dir = out_dir.join("native");
fs::create_dir_all(&native_lib_dir)?;
let cdylib_filename = cdylib_path.file_name().unwrap();
let extension = cdylib_path.extension().unwrap();
let lib_base_name = cdylib_filename
.strip_prefix("lib")
.unwrap_or(cdylib_filename)
.split('-')
.next()
.unwrap_or(cdylib_filename);
let canonical_lib_name = format!("lib{}.{}", lib_base_name, extension);
let lib_absolute_path = native_lib_dir.join(&canonical_lib_name);
fs::copy(&cdylib_path, &lib_absolute_path)?;

let jar_file = build_jar(fixture_name, &out_dir)?;

let status = Command::new("javac")
.arg("-classpath")
.arg(calc_classpath(vec![&out_dir, &jar_file]))
.arg("-Werror")
.arg(&test_path)
.spawn()
.context("Failed to spawn `javac` to compile Java test")?
.wait()
.context("Failed to wait for `javac` when compiling Java test")?;
if !status.success() {
anyhow::bail!("running `javac` failed when compiling the Java test")
}

// Run with library override set to an absolute path and NO java.library.path,
// so this can only work if the generated code uses System.load() for absolute paths.
let compiled_path = test_path.file_stem().unwrap();
let run_status = Command::new("java")
.arg("-ea")
.arg("--enable-native-access=ALL-UNNAMED")
.arg(format!(
"-Duniffi.component.{}.libraryOverride={}",
namespace, lib_absolute_path
))
// Deliberately NOT setting -Djava.library.path
.arg("-classpath")
.arg(calc_classpath(vec![
&out_dir,
&jar_file,
&test_path.parent().unwrap().to_path_buf(),
]))
.arg(compiled_path)
.spawn()
.context("Failed to spawn `java` to run Java test")?
.wait()
.context("Failed to wait for `java` when running Java test")?;
if !run_status.success() {
anyhow::bail!("Running the `java` test with library override failed.")
}

Ok(())
}

/// Get the uniffi_toml of the fixture if it exists.
/// It looks for it in the root directory of the project `name`.
fn find_uniffi_toml(name: &str) -> Result<Option<Utf8PathBuf>> {
Expand Down Expand Up @@ -286,3 +377,12 @@ fixture_tests! {
(test_rename, "uniffi-fixture-rename", "scripts/TestRename/TestRename.java"),
(test_primitive_arrays, "uniffi-fixture-primitive-arrays", "scripts/TestPrimitiveArrays.java"),
}

#[test]
fn test_library_override_absolute_path() -> Result<()> {
run_test_with_library_override(
"uniffi-example-arithmetic",
"scripts/TestArithmetic.java",
"arithmetic",
)
}
Loading