From 8ae6114a201b2dbd02ba725d496d34150f8c6afe Mon Sep 17 00:00:00 2001 From: Murph Murphy Date: Mon, 6 Apr 2026 12:21:31 -0600 Subject: [PATCH 1/2] Fix library override for absolutes, update changelog for this and the other unreleased change on main --- CHANGELOG.md | 6 ++ README.md | 12 +++ src/templates/NamespaceLibraryTemplate.java | 10 +- tests/tests.rs | 100 ++++++++++++++++++++ 4 files changed, 127 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ccf3cc..ab548fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## Unreleased + +- 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..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. diff --git a/README.md b/README.md index 727addc..5f53aaa 100644 --- a/README.md +++ b/README.md @@ -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..libraryOverride=/path/to/libmylib.so ... +``` + +Where `` 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. diff --git a/src/templates/NamespaceLibraryTemplate.java b/src/templates/NamespaceLibraryTemplate.java index 01ff80c..356a78a 100644 --- a/src/templates/NamespaceLibraryTemplate.java +++ b/src/templates/NamespaceLibraryTemplate.java @@ -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(); } diff --git a/tests/tests.rs b/tests/tests.rs index a8d0499..334c874 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -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> { @@ -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", + ) +} From 72f86226a6542fa1ebd6572afe05dfb6647fcb80 Mon Sep 17 00:00:00 2001 From: Murph Murphy Date: Mon, 6 Apr 2026 12:45:00 -0600 Subject: [PATCH 2/2] Update for release --- CHANGELOG.md | 8 ++++---- Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab548fb..b5afe5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ -## Unreleased +## 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..libraryOverride=/path/to/lib.so` would fail because it was always passed to `System.loadLibrary()`. Now absolute paths are correctly routed to `System.load()`. +- 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..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 diff --git a/Cargo.toml b/Cargo.toml index 78ee1fa..a7c32ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uniffi-bindgen-java" -version = "0.4.0" +version = "0.4.1" authors = ["IronCore Labs "] readme = "README.md" license = "MPL-2.0"