├── .envrc ├── .gitignore ├── .github ├── CODEOWNERS └── workflows │ ├── release.yml │ └── rust-ci.yml ├── src ├── main.rs ├── templates │ ├── ByteArrayHelper.java │ ├── Int8Helper.java │ ├── BooleanHelper.java │ ├── Int16Helper.java │ ├── Int64Helper.java │ ├── Float32Helper.java │ ├── Float64Helper.java │ ├── Int32Helper.java │ ├── CallbackInterfaceTemplate.java │ ├── ObjectCleanerHelperJvm.java │ ├── Interface.java │ ├── SequenceTemplate.java │ ├── HandleMap.java │ ├── ObjectCleanerHelperAndroid.java │ ├── CallbackInterfaceRuntime.java │ ├── OptionalTemplate.java │ ├── wrapper.java │ ├── TimestampHelper.java │ ├── DurationHelper.java │ ├── MapTemplate.java │ ├── StringHelper.java │ ├── ObjectCleanerHelper.java │ ├── FfiConverterTemplate.java │ ├── RustBufferTemplate.java │ ├── CustomTypeTemplate.java │ ├── RecordTemplate.java │ ├── EnumTemplate.java │ ├── Types.java │ ├── ErrorTemplate.java │ ├── NamespaceLibraryTemplate.java │ ├── Helpers.java │ ├── macros.java │ ├── CallbackInterfaceImpl.java │ ├── Async.java │ └── ObjectTemplate.java ├── gen_java │ ├── miscellany.rs │ ├── variant.rs │ ├── record.rs │ ├── custom.rs │ ├── callback_interface.rs │ ├── object.rs │ ├── enum_.rs │ ├── primitives.rs │ └── compounds.rs └── lib.rs ├── tests ├── scripts │ ├── TestFixtureFutures │ │ ├── uniffi-extras.toml │ │ └── TestFixtureFutures.java │ ├── TestImportedTypes │ │ ├── uniffi-extras.toml │ │ └── TestImportedTypes.java │ ├── TestCustomTypes │ │ ├── uniffi-extras.toml │ │ └── TestCustomTypes.java │ ├── TestFutures.java │ ├── TestGeometry.java │ ├── TestArithmetic.java │ ├── TestChronological.java │ └── TestRondpoint.java └── tests.rs ├── rust-toolchain.toml ├── askama.toml ├── RELEASING.md ├── flake.nix ├── CHANGELOG.md ├── Cargo.toml ├── flake.lock └── README.md /.envrc: -------------------------------------------------------------------------------- 1 | use flake; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .direnv 3 | *.class 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @IronCoreLabs/rust-dev 2 | 3 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | fn main() -> Result<()> { 4 | uniffi_bindgen_java::run_main() 5 | } 6 | -------------------------------------------------------------------------------- /tests/scripts/TestFixtureFutures/uniffi-extras.toml: -------------------------------------------------------------------------------- 1 | [bindings.java] 2 | package_name = "uniffi.fixture.futures" 3 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | profile = "default" 3 | channel = "1.87.0" 4 | components = ["rust-src", "rust-analyzer"] 5 | -------------------------------------------------------------------------------- /askama.toml: -------------------------------------------------------------------------------- 1 | [general] 2 | # Directories to search for templates, relative to the crate root. 3 | dirs = [ "src/templates"] 4 | 5 | [[syntax]] 6 | name = "java" 7 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Rust Release 2 | 3 | on: 4 | workflow_dispatch: null 5 | 6 | jobs: 7 | rust-release: 8 | uses: IronCoreLabs/workflows/.github/workflows/rust-release.yaml@rust-release-v1 9 | secrets: inherit 10 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | To release a new version create a `CHANGELOG.md` entry, bump the `Cargo.toml` version, create a matching tag, and push all of the above. 2 | Then run the [release action](https://github.com/IronCoreLabs/uniffi-bindgen-java/actions/workflows/release.yml) to release the crate to `crates.io`. 3 | -------------------------------------------------------------------------------- /tests/scripts/TestImportedTypes/uniffi-extras.toml: -------------------------------------------------------------------------------- 1 | [bindings.java.custom_types.Url] 2 | # Name of the type in the Java code 3 | type_name = "URL" 4 | # Classes that need to be imported 5 | imports = ["java.net.URI", "java.net.URL"] 6 | # Functions to convert between strings and URLs 7 | into_custom = "new URI({}).toURL()" 8 | from_custom = "{}.toString()" 9 | -------------------------------------------------------------------------------- /tests/scripts/TestCustomTypes/uniffi-extras.toml: -------------------------------------------------------------------------------- 1 | [bindings.java] 2 | package_name = "customtypes" 3 | 4 | [bindings.java.custom_types.Url] 5 | # Name of the type in the Java code 6 | type_name = "URL" 7 | # Classes that need to be imported 8 | imports = ["java.net.URI", "java.net.URL"] 9 | # Functions to convert between strings and URLs 10 | into_custom = "new URI({}).toURL()" 11 | from_custom = "{}.toString()" 12 | -------------------------------------------------------------------------------- /tests/scripts/TestFutures.java: -------------------------------------------------------------------------------- 1 | import uniffi.uniffi_example_futures.*; 2 | 3 | import java.util.concurrent.CompletableFuture; 4 | 5 | public class TestFutures { 6 | public static void main(String[] args) throws Exception { 7 | CompletableFuture future = UniffiExampleFutures.sayAfter(20L, "Alice"); 8 | 9 | future.thenAccept(result -> { 10 | assert result.equals("Hello, Alice!"); 11 | }); 12 | 13 | future.get(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/scripts/TestGeometry.java: -------------------------------------------------------------------------------- 1 | import uniffi.geometry.*; 2 | 3 | public class TestGeometry { 4 | public static void main(String[] args) throws Exception { 5 | var ln1 = new Line(new Point(0.0,0.0), new Point(1.0,2.0)); 6 | var ln2 = new Line(new Point(1.0,1.0), new Point(2.0,2.0)); 7 | 8 | assert Geometry.gradient(ln1) == 2.0; 9 | assert Geometry.gradient(ln2) == 1.0; 10 | 11 | assert Geometry.intersection(ln1, ln2).equals(new Point(0.0, 0.0)); 12 | assert Geometry.intersection(ln1, ln1) == null; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/rust-ci.yml: -------------------------------------------------------------------------------- 1 | name: Rust CI 2 | 'on': 3 | push: 4 | branches: 5 | - main 6 | pull_request: null 7 | workflow_dispatch: null 8 | schedule: 9 | - cron: 0 14 * * 1 10 | jobs: 11 | rust-ci: 12 | uses: IronCoreLabs/workflows/.github/workflows/rust-ci.yaml@rust-ci-v2 13 | with: 14 | # enable once initial implementation is done 15 | run_clippy: false 16 | minimum_coverage: "0" 17 | additional_system_deps: "libjna-java" 18 | cargo_command_env_vars: "PATH=$JAVA_HOME_21_X64/bin:$PATH CLASSPATH=/usr/share/java/jna.jar" 19 | secrets: inherit 20 | -------------------------------------------------------------------------------- /src/templates/ByteArrayHelper.java: -------------------------------------------------------------------------------- 1 | package {{ config.package_name() }}; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | public enum FfiConverterByteArray implements FfiConverterRustBuffer{ 6 | INSTANCE; 7 | 8 | @Override 9 | public byte[] read(ByteBuffer buf) { 10 | int len = buf.getInt(); 11 | byte[] byteArr = new byte[len]; 12 | buf.get(byteArr); 13 | return byteArr; 14 | } 15 | 16 | @Override 17 | public long allocationSize(byte[] value) { 18 | return 4L + (long)value.length; 19 | } 20 | 21 | @Override 22 | public void write(byte[] value, ByteBuffer buf) { 23 | buf.putInt(value.length); 24 | buf.put(value); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/templates/Int8Helper.java: -------------------------------------------------------------------------------- 1 | package {{ config.package_name() }}; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | public enum FfiConverterByte implements FfiConverter{ 6 | INSTANCE; 7 | 8 | @Override 9 | public Byte lift(Byte value) { 10 | return value; 11 | } 12 | 13 | @Override 14 | public Byte read(ByteBuffer buf) { 15 | return buf.get(); 16 | } 17 | 18 | @Override 19 | public Byte lower(Byte value) { 20 | return value; 21 | } 22 | 23 | @Override 24 | public long allocationSize(Byte value) { 25 | return 1L; 26 | } 27 | 28 | @Override 29 | public void write(Byte value, ByteBuffer buf) { 30 | buf.put(value); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/templates/BooleanHelper.java: -------------------------------------------------------------------------------- 1 | package {{ config.package_name() }}; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | public enum FfiConverterBoolean implements FfiConverter { 6 | INSTANCE; 7 | 8 | @Override 9 | public Boolean lift(Byte value) { 10 | return (int) value != 0; 11 | } 12 | 13 | @Override 14 | public Boolean read(ByteBuffer buf) { 15 | return lift(buf.get()); 16 | } 17 | 18 | @Override 19 | public Byte lower(Boolean value) { 20 | return value ? (byte) 1 : (byte) 0; 21 | } 22 | 23 | @Override 24 | public long allocationSize(Boolean value) { 25 | return 1L; 26 | } 27 | 28 | @Override 29 | public void write(Boolean value, ByteBuffer buf) { 30 | buf.put(lower(value)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/templates/Int16Helper.java: -------------------------------------------------------------------------------- 1 | package {{ config.package_name() }}; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | public enum FfiConverterShort implements FfiConverter{ 6 | INSTANCE; 7 | 8 | @Override 9 | public Short lift(Short value) { 10 | return value; 11 | } 12 | 13 | @Override 14 | public Short read(ByteBuffer buf) { 15 | return buf.getShort(); 16 | } 17 | 18 | @Override 19 | public Short lower(Short value) { 20 | return value; 21 | } 22 | 23 | @Override 24 | public long allocationSize(Short value) { 25 | return 2L; 26 | } 27 | 28 | @Override 29 | public void write(Short value, ByteBuffer buf) { 30 | buf.putShort(value); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/templates/Int64Helper.java: -------------------------------------------------------------------------------- 1 | package {{ config.package_name() }}; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | public enum FfiConverterLong implements FfiConverter { 6 | INSTANCE; 7 | 8 | @Override 9 | public Long lift(Long value) { 10 | return value; 11 | } 12 | 13 | @Override 14 | public Long read(ByteBuffer buf) { 15 | return buf.getLong(); 16 | } 17 | 18 | @Override 19 | public Long lower(Long value) { 20 | return value; 21 | } 22 | 23 | @Override 24 | public long allocationSize(Long value) { 25 | return 8L; 26 | } 27 | 28 | @Override 29 | public void write(Long value, ByteBuffer buf) { 30 | buf.putLong(value); 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /src/templates/Float32Helper.java: -------------------------------------------------------------------------------- 1 | package {{ config.package_name() }}; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | public enum FfiConverterFloat implements FfiConverter{ 6 | INSTANCE; 7 | 8 | @Override 9 | public Float lift(Float value) { 10 | return value; 11 | } 12 | 13 | @Override 14 | public Float read(ByteBuffer buf) { 15 | return buf.getFloat(); 16 | } 17 | 18 | @Override 19 | public Float lower(Float value) { 20 | return value; 21 | } 22 | 23 | @Override 24 | public long allocationSize(Float value) { 25 | return 4L; 26 | } 27 | 28 | @Override 29 | public void write(Float value, ByteBuffer buf) { 30 | buf.putFloat(value); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/templates/Float64Helper.java: -------------------------------------------------------------------------------- 1 | package {{ config.package_name() }}; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | public enum FfiConverterDouble implements FfiConverter{ 6 | INSTANCE; 7 | 8 | @Override 9 | public Double lift(Double value) { 10 | return value; 11 | } 12 | 13 | @Override 14 | public Double read(ByteBuffer buf) { 15 | return buf.getDouble(); 16 | } 17 | 18 | @Override 19 | public Double lower(Double value) { 20 | return value; 21 | } 22 | 23 | @Override 24 | public long allocationSize(Double value) { 25 | return 8L; 26 | } 27 | 28 | @Override 29 | public void write(Double value, ByteBuffer buf) { 30 | buf.putDouble(value); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/templates/Int32Helper.java: -------------------------------------------------------------------------------- 1 | package {{ config.package_name() }}; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | public enum FfiConverterInteger implements FfiConverter{ 6 | INSTANCE; 7 | 8 | @Override 9 | public Integer lift(Integer value) { 10 | return value; 11 | } 12 | 13 | @Override 14 | public Integer read(ByteBuffer buf) { 15 | return buf.getInt(); 16 | } 17 | 18 | @Override 19 | public Integer lower(Integer value) { 20 | return value; 21 | } 22 | 23 | @Override 24 | public long allocationSize(Integer value) { 25 | return 4L; 26 | } 27 | 28 | @Override 29 | public void write(Integer value, ByteBuffer buf) { 30 | buf.putInt(value); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/templates/CallbackInterfaceTemplate.java: -------------------------------------------------------------------------------- 1 | {%- let cbi = ci.get_callback_interface_definition(name).unwrap() %} 2 | {%- let ffi_init_callback = cbi.ffi_init_callback() %} 3 | {%- let interface_name = cbi|type_name(ci, config) %} 4 | {%- let interface_docstring = cbi.docstring() %} 5 | {%- let methods = cbi.methods() %} 6 | {%- let vtable = cbi.vtable() %} 7 | {%- let vtable_methods = cbi.vtable_methods() %} 8 | 9 | {% include "Interface.java" %} 10 | {% include "CallbackInterfaceImpl.java" %} 11 | 12 | package {{ config.package_name() }}; 13 | 14 | // The ffiConverter which transforms the Callbacks in to handles to pass to Rust. 15 | public final class {{ ffi_converter_name }} extends FfiConverterCallbackInterface<{{ interface_name }}> { 16 | static final {{ ffi_converter_name }} INSTANCE = new {{ ffi_converter_name }}(); 17 | 18 | private {{ ffi_converter_name }}() {} 19 | } 20 | 21 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 4 | rust-overlay.url = "github:oxalica/rust-overlay"; 5 | flake-utils.url = "github:numtide/flake-utils"; 6 | }; 7 | 8 | outputs = { nixpkgs, rust-overlay, flake-utils, ... }: 9 | flake-utils.lib.eachDefaultSystem (system: 10 | let 11 | overlays = [ (import rust-overlay) ]; 12 | pkgs = import nixpkgs { inherit system overlays; }; 13 | rusttoolchain = 14 | pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; 15 | in 16 | { 17 | # nix develop 18 | devShell = pkgs.mkShell { 19 | buildInputs = with pkgs; 20 | [ rusttoolchain pkg-config openjdk21 jna ] 21 | ++ lib.optionals stdenv.isDarwin 22 | [ darwin.apple_sdk.frameworks.SystemConfiguration ]; 23 | }; 24 | 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.2.1 2 | 3 | - Fix `OptionalTemplate` missing potential imports of `java.util.List` and `java.util.Map` 4 | 5 | ## 0.2.0 6 | 7 | - Update to uniffi 0.29.2 8 | - Consumes a uniffi fix to Python bindgen that was affecting `ironcore-alloy` (PR #2512) 9 | 10 | Consumers will also need to update to uniffi 0.29.2. 11 | 12 | ## 0.1.1 13 | 14 | - Update to uniffi 0.29.1 15 | - Fix a potential memory leak in arrays and maps reported in uniffi proper. A similar fix to the one for Kotlin applied here. 16 | 17 | Consumers will also need to update to uniffi 0.29.1, but there will be no changes required to their code. 18 | 19 | ## 0.1.0 20 | 21 | Initial pre-release. This library will be used to provide Java bindings for [IronCore Alloy](https://github.com/IronCoreLabs/ironcore-alloy/tree/main). It will recieve frequent breaking changes initially as we find improvements through that libaries' usage of it. 22 | 23 | -------------------------------------------------------------------------------- /src/templates/ObjectCleanerHelperJvm.java: -------------------------------------------------------------------------------- 1 | package {{ config.package_name() }}; 2 | 3 | import java.lang.ref.Cleaner; 4 | 5 | class JavaLangRefCleaner implements UniffiCleaner { 6 | private final Cleaner cleaner; 7 | 8 | JavaLangRefCleaner() { 9 | this.cleaner = Cleaner.create(); 10 | } 11 | 12 | @Override 13 | public UniffiCleaner.Cleanable register(Object value, Runnable cleanUpTask) { 14 | return new JavaLangRefCleanable(cleaner.register(value, cleanUpTask)); 15 | } 16 | } 17 | 18 | package {{ config.package_name() }}; 19 | 20 | import java.lang.ref.Cleaner; 21 | 22 | class JavaLangRefCleanable implements UniffiCleaner.Cleanable { 23 | private final Cleaner.Cleanable cleanable; 24 | 25 | JavaLangRefCleanable(Cleaner.Cleanable cleanable) { 26 | this.cleanable = cleanable; 27 | } 28 | 29 | @Override 30 | public void clean() { 31 | cleanable.clean(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/templates/Interface.java: -------------------------------------------------------------------------------- 1 | package {{ config.package_name() }}; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | import java.util.concurrent.CompletableFuture; 6 | import com.sun.jna.*; 7 | import com.sun.jna.ptr.*; 8 | 9 | {%- call java::docstring_value(interface_docstring, 0) %} 10 | public interface {{ interface_name }} { 11 | {% for meth in methods.iter() -%} 12 | {%- call java::docstring(meth, 4) %} 13 | public {% if meth.is_async() %}CompletableFuture<{% endif %}{% match meth.return_type() -%}{%- when Some with (return_type) %}{{ return_type|type_name(ci, config) }}{%- else -%}{% if meth.is_async() %}Void{% else %}void{% endif %}{%- endmatch %}{% if meth.is_async() %}>{% endif %} {{ meth.name()|fn_name }}({% call java::arg_list(meth, true) %}){% match meth.throws_type() %}{% when Some(throwable) %} {% if !meth.is_async() %}throws {{ throwable|type_name(ci, config) }}{% endif %}{% else %}{% endmatch %}; 14 | {% endfor %} 15 | } 16 | -------------------------------------------------------------------------------- /tests/scripts/TestCustomTypes/TestCustomTypes.java: -------------------------------------------------------------------------------- 1 | import java.net.MalformedURLException; 2 | import java.net.URL; 3 | import java.net.URI; 4 | import java.net.URISyntaxException; 5 | 6 | import customtypes.*; 7 | 8 | public class TestCustomTypes { 9 | 10 | public static void main(String[] args) throws MalformedURLException, URISyntaxException { 11 | // Get the custom types and check their data 12 | CustomTypesDemo demo = CustomTypes.getCustomTypesDemo(null); 13 | // URL is customized on the bindings side 14 | assert demo.url().equals(new Url(new URI("http://example.com/").toURL())); 15 | // Handle isn't, but because java doesn't have type aliases it's still wrapped. 16 | assert demo.handle().equals(new Handle(123L)); 17 | 18 | // // Change some data and ensure that the round-trip works 19 | demo.setUrl(new Url(new URI("http://new.example.com/").toURL())); 20 | demo.setHandle(new Handle(456L)); 21 | assert demo.equals(CustomTypes.getCustomTypesDemo(demo)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/gen_java/miscellany.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use super::{CodeType, Config}; 6 | use crate::ComponentInterface; 7 | use paste::paste; 8 | 9 | macro_rules! impl_code_type_for_miscellany { 10 | ($T:ty, $class_name:literal, $canonical_name:literal) => { 11 | paste! { 12 | #[derive(Debug)] 13 | pub struct $T; 14 | 15 | impl CodeType for $T { 16 | fn type_label(&self, _ci: &ComponentInterface, _config: &Config) -> String { 17 | $class_name.into() 18 | } 19 | 20 | fn canonical_name(&self) -> String { 21 | $canonical_name.into() 22 | } 23 | } 24 | } 25 | }; 26 | } 27 | 28 | impl_code_type_for_miscellany!(TimestampCodeType, "java.time.Instant", "Timestamp"); 29 | 30 | impl_code_type_for_miscellany!(DurationCodeType, "java.time.Duration", "Duration"); 31 | -------------------------------------------------------------------------------- /tests/scripts/TestArithmetic.java: -------------------------------------------------------------------------------- 1 | import uniffi.arithmetic.*; 2 | 3 | public class TestArithmetic { 4 | public static void main(String[] args) throws Exception { 5 | assert Arithmetic.add(2L, 4L) == 6L; 6 | assert Arithmetic.add(4L, 8L) == 12L; 7 | 8 | try { 9 | Arithmetic.sub(0L, 2L); 10 | throw new RuntimeException("Should have thrown an IntegerOverflow exception!"); 11 | } catch (uniffi.arithmetic.ArithmeticException.IntegerOverflow e) { 12 | // It's okay! 13 | } 14 | 15 | assert Arithmetic.sub(4L, 2L) == 2L; 16 | assert Arithmetic.sub(8L, 4L) == 4L; 17 | 18 | assert Arithmetic.div(8L, 4L) == 2L; 19 | 20 | try { 21 | Arithmetic.div(8L, 0L); 22 | throw new RuntimeException("Should have panicked when dividing by zero"); 23 | } catch (RuntimeException e) { 24 | if (e instanceof InternalException) { 25 | // It's okay! 26 | } else { 27 | throw e; 28 | } 29 | } 30 | 31 | assert Arithmetic.equal(2L, 2L); 32 | assert Arithmetic.equal(4L, 4L); 33 | assert !Arithmetic.equal(2L, 4L); 34 | assert !Arithmetic.equal(4L, 8L); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/gen_java/variant.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use super::{AsCodeType, CodeType, Config, JavaCodeOracle, potentially_add_external_package}; 6 | use uniffi_bindgen::interface::{ComponentInterface, Variant}; 7 | 8 | #[derive(Debug)] 9 | pub(super) struct VariantCodeType { 10 | pub v: Variant, 11 | } 12 | 13 | impl CodeType for VariantCodeType { 14 | fn type_label(&self, ci: &ComponentInterface, config: &Config) -> String { 15 | potentially_add_external_package( 16 | config, 17 | ci, 18 | self.v.name(), 19 | JavaCodeOracle.class_name(ci, self.v.name()), 20 | ) 21 | } 22 | 23 | fn canonical_name(&self) -> String { 24 | self.v.name().to_string() 25 | } 26 | } 27 | 28 | impl AsCodeType for Variant { 29 | fn as_codetype(&self) -> Box { 30 | Box::new(VariantCodeType { v: self.clone() }) 31 | } 32 | } 33 | 34 | impl AsCodeType for &Variant { 35 | fn as_codetype(&self) -> Box { 36 | Box::new(VariantCodeType { v: (*self).clone() }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/gen_java/record.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use super::{CodeType, Config, potentially_add_external_package}; 6 | use uniffi_bindgen::ComponentInterface; 7 | 8 | #[derive(Debug)] 9 | pub struct RecordCodeType { 10 | id: String, 11 | } 12 | 13 | impl RecordCodeType { 14 | pub fn new(id: String) -> Self { 15 | Self { id } 16 | } 17 | } 18 | 19 | impl CodeType for RecordCodeType { 20 | fn type_label(&self, ci: &ComponentInterface, config: &Config) -> String { 21 | potentially_add_external_package( 22 | config, 23 | ci, 24 | &self.id, 25 | super::JavaCodeOracle.class_name(ci, &self.id), 26 | ) 27 | } 28 | 29 | fn canonical_name(&self) -> String { 30 | format!("Type{}", self.id) 31 | } 32 | 33 | fn ffi_converter_instance(&self, config: &Config, ci: &ComponentInterface) -> String { 34 | potentially_add_external_package( 35 | config, 36 | ci, 37 | &self.id, 38 | format!("{}.INSTANCE", self.ffi_converter_name()), 39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/gen_java/custom.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use super::{CodeType, Config, potentially_add_external_package}; 6 | use crate::ComponentInterface; 7 | 8 | #[derive(Debug)] 9 | pub struct CustomCodeType { 10 | name: String, 11 | } 12 | 13 | impl CustomCodeType { 14 | pub fn new(name: String) -> Self { 15 | CustomCodeType { name } 16 | } 17 | } 18 | 19 | impl CodeType for CustomCodeType { 20 | fn type_label(&self, ci: &ComponentInterface, config: &Config) -> String { 21 | potentially_add_external_package( 22 | config, 23 | ci, 24 | &self.name, 25 | super::JavaCodeOracle.class_name(ci, &self.name), 26 | ) 27 | } 28 | 29 | fn canonical_name(&self) -> String { 30 | format!("Type{}", self.name) 31 | } 32 | 33 | fn ffi_converter_instance(&self, config: &Config, ci: &ComponentInterface) -> String { 34 | potentially_add_external_package( 35 | config, 36 | ci, 37 | &self.name, 38 | format!("{}.INSTANCE", self.ffi_converter_name()), 39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/templates/SequenceTemplate.java: -------------------------------------------------------------------------------- 1 | {%- let inner_type_name = inner_type|type_name(ci, config) %} 2 | /* This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | package {{ config.package_name() }}; 6 | 7 | import java.util.List; 8 | import java.nio.ByteBuffer; 9 | import java.util.stream.IntStream; 10 | import java.util.stream.Stream; 11 | 12 | public enum {{ ffi_converter_name }} implements FfiConverterRustBuffer> { 13 | INSTANCE; 14 | 15 | @Override 16 | public List<{{ inner_type_name }}> read(ByteBuffer buf) { 17 | int len = buf.getInt(); 18 | return IntStream.range(0, len).mapToObj(_i -> {{ inner_type|read_fn(config, ci) }}(buf)).toList(); 19 | } 20 | 21 | @Override 22 | public long allocationSize(List<{{ inner_type_name }}> value) { 23 | long sizeForLength = 4L; 24 | long sizeForItems = value.stream().mapToLong(inner -> {{ inner_type|allocation_size_fn(config, ci) }}(inner)).sum(); 25 | return sizeForLength + sizeForItems; 26 | } 27 | 28 | @Override 29 | public void write(List<{{ inner_type_name }}> value, ByteBuffer buf) { 30 | buf.putInt(value.size()); 31 | value.forEach(inner -> {{ inner_type|write_fn(config, ci) }}(inner, buf)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/templates/HandleMap.java: -------------------------------------------------------------------------------- 1 | package {{ config.package_name() }}; 2 | 3 | import java.util.concurrent.atomic.AtomicLong; 4 | import java.util.concurrent.ConcurrentHashMap; 5 | 6 | // This is used pass an opaque 64-bit handle representing a foreign object to the Rust code. 7 | class UniffiHandleMap { 8 | private final ConcurrentHashMap map = new ConcurrentHashMap<>(); 9 | private final AtomicLong counter = new AtomicLong(0); 10 | 11 | public int size() { 12 | return map.size(); 13 | } 14 | 15 | // Insert a new object into the handle map and get a handle for it 16 | public long insert(T obj) { 17 | long handle = counter.getAndAdd(1); 18 | map.put(handle, obj); 19 | return handle; 20 | } 21 | 22 | // Get an object from the handle map 23 | public T get(long handle) { 24 | T obj = map.get(handle); 25 | if (obj == null) { 26 | throw new InternalException("UniffiHandleMap.get: Invalid handle"); 27 | } 28 | return obj; 29 | } 30 | 31 | // Remove an entry from the handlemap and get the Java object back 32 | public T remove(long handle) { 33 | T obj = map.remove(handle); 34 | if (obj == null) { 35 | throw new InternalException("UniffiHandleMap: Invalid handle"); 36 | } 37 | return obj; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/templates/ObjectCleanerHelperAndroid.java: -------------------------------------------------------------------------------- 1 | package {{ config.package_name() }}; 2 | 3 | import android.os.Build; 4 | import androidx.annotation.RequiresApi; 5 | import java.lang.ref.Cleaner; 6 | 7 | // The SystemCleaner, available from API Level 33. 8 | // Some API Level 33 OSes do not support using it, so we require API Level 34. 9 | class AndroidSystemCleaner implements UniffiCleaner { 10 | private final Cleaner cleaner; 11 | 12 | @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) 13 | AndroidSystemCleaner() { 14 | this.cleaner = android.system.SystemCleaner.cleaner(); 15 | } 16 | 17 | @Override 18 | public UniffiCleaner.Cleanable register(Object value, Runnable cleanUpTask) { 19 | return new AndroidSystemCleanable(cleaner.register(value, cleanUpTask)); 20 | } 21 | } 22 | 23 | package {{ config.package_name() }}; 24 | 25 | import android.os.Build; 26 | import androidx.annotation.RequiresApi; 27 | import java.lang.ref.Cleaner; 28 | 29 | class AndroidSystemCleanable implements UniffiCleaner.Cleanable { 30 | private final Cleaner.Cleanable cleanable; 31 | 32 | @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) 33 | AndroidSystemCleanable(Cleaner.Cleanable cleanable) { 34 | this.cleanable = cleanable; 35 | } 36 | 37 | @Override 38 | public void clean() { 39 | cleanable.clean(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/templates/CallbackInterfaceRuntime.java: -------------------------------------------------------------------------------- 1 | package {{ config.package_name() }}; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | public abstract class FfiConverterCallbackInterface implements FfiConverter { 6 | // Magic number for the Rust proxy to call using the same mechanism as every other method, 7 | // to free the callback once it's dropped by Rust. 8 | static final int IDX_CALLBACK_FREE = 0; 9 | // Callback return codes 10 | static final int UNIFFI_CALLBACK_SUCCESS = 0; 11 | static final int UNIFFI_CALLBACK_ERROR = 1; 12 | static final int UNIFFI_CALLBACK_UNEXPECTED_ERROR = 2; 13 | 14 | public final UniffiHandleMap handleMap = new UniffiHandleMap<>(); 15 | 16 | void drop(long handle) { 17 | handleMap.remove(handle); 18 | } 19 | 20 | @Override 21 | public CallbackInterface lift(Long value) { 22 | return handleMap.get(value); 23 | } 24 | 25 | @Override 26 | public CallbackInterface read(ByteBuffer buf) { 27 | return lift(buf.getLong()); 28 | } 29 | 30 | @Override 31 | public Long lower(CallbackInterface value) { 32 | return handleMap.insert(value); 33 | } 34 | 35 | @Override 36 | public long allocationSize(CallbackInterface value) { 37 | return 8L; 38 | } 39 | 40 | @Override 41 | public void write(CallbackInterface value, ByteBuffer buf) { 42 | buf.putLong(lower(value)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/gen_java/callback_interface.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use super::{CodeType, Config, potentially_add_external_package}; 6 | use crate::ComponentInterface; 7 | 8 | #[derive(Debug)] 9 | pub struct CallbackInterfaceCodeType { 10 | id: String, 11 | } 12 | 13 | impl CallbackInterfaceCodeType { 14 | pub fn new(id: String) -> Self { 15 | Self { id } 16 | } 17 | } 18 | 19 | impl CodeType for CallbackInterfaceCodeType { 20 | fn type_label(&self, ci: &ComponentInterface, config: &Config) -> String { 21 | potentially_add_external_package( 22 | config, 23 | ci, 24 | &self.id, 25 | super::JavaCodeOracle.class_name(ci, &self.id), 26 | ) 27 | } 28 | 29 | fn canonical_name(&self) -> String { 30 | format!("Type{}", self.id) 31 | } 32 | 33 | fn initialization_fn(&self) -> Option { 34 | Some(format!( 35 | "UniffiCallbackInterface{}.INSTANCE.register", 36 | self.id 37 | )) 38 | } 39 | 40 | fn ffi_converter_instance(&self, config: &Config, ci: &ComponentInterface) -> String { 41 | potentially_add_external_package( 42 | config, 43 | ci, 44 | &self.id, 45 | format!("{}.INSTANCE", self.ffi_converter_name()), 46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/templates/OptionalTemplate.java: -------------------------------------------------------------------------------- 1 | {%- let inner_type_name = inner_type|type_name(ci, config) %} 2 | package {{ config.package_name() }}; 3 | 4 | import java.nio.ByteBuffer; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | // public class TestForOptionals {} 9 | 10 | {#- 11 | Swift, Python, and Go bindings all use nullable/nilable optionals instead of `Optional` types where they exist. 12 | Kotlin and C# bindings use their type system to directly express nullability with `?`. 13 | Java has an `Optional` type that some FP Java folks use, but we'll lean on the more straightforward Java way here 14 | and have it be invisibly nullable, because that's the normal Java way. 15 | #} 16 | public enum {{ ffi_converter_name }} implements FfiConverterRustBuffer<{{ inner_type_name }}> { 17 | INSTANCE; 18 | 19 | @Override 20 | public {{ inner_type_name }} read(ByteBuffer buf) { 21 | if (buf.get() == (byte)0) { 22 | return null; 23 | } 24 | return {{ inner_type|read_fn(config, ci) }}(buf); 25 | } 26 | 27 | @Override 28 | public long allocationSize({{ inner_type_name }} value) { 29 | if (value == null) { 30 | return 1L; 31 | } else { 32 | return 1L + {{ inner_type|allocation_size_fn(config, ci) }}(value); 33 | } 34 | } 35 | 36 | @Override 37 | public void write({{ inner_type_name }} value, ByteBuffer buf) { 38 | if (value == null) { 39 | buf.put((byte)0); 40 | } else { 41 | buf.put((byte)1); 42 | {{ inner_type|write_fn(config, ci) }}(value, buf); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/gen_java/object.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use super::{CodeType, Config, potentially_add_external_package}; 6 | use uniffi_bindgen::{ComponentInterface, interface::ObjectImpl}; 7 | 8 | #[derive(Debug)] 9 | pub struct ObjectCodeType { 10 | name: String, 11 | imp: ObjectImpl, 12 | } 13 | 14 | impl ObjectCodeType { 15 | pub fn new(name: String, imp: ObjectImpl) -> Self { 16 | Self { name, imp } 17 | } 18 | } 19 | 20 | impl CodeType for ObjectCodeType { 21 | fn type_label(&self, ci: &ComponentInterface, config: &Config) -> String { 22 | potentially_add_external_package( 23 | config, 24 | ci, 25 | &self.name, 26 | super::JavaCodeOracle.class_name(ci, &self.name), 27 | ) 28 | } 29 | 30 | fn canonical_name(&self) -> String { 31 | format!("Type{}", self.name) 32 | } 33 | 34 | fn initialization_fn(&self) -> Option { 35 | self.imp 36 | .has_callback_interface() 37 | .then(|| format!("UniffiCallbackInterface{}.INSTANCE.register", self.name)) 38 | } 39 | 40 | fn ffi_converter_instance(&self, config: &Config, ci: &ComponentInterface) -> String { 41 | potentially_add_external_package( 42 | config, 43 | ci, 44 | &self.name, 45 | format!("{}.INSTANCE", self.ffi_converter_name()), 46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/gen_java/enum_.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use super::{CodeType, Config, potentially_add_external_package}; 6 | use crate::ComponentInterface; 7 | use uniffi_bindgen::backend::Literal; 8 | 9 | #[derive(Debug)] 10 | pub struct EnumCodeType { 11 | id: String, 12 | } 13 | 14 | impl EnumCodeType { 15 | pub fn new(id: String) -> Self { 16 | Self { id } 17 | } 18 | } 19 | 20 | impl CodeType for EnumCodeType { 21 | fn type_label(&self, ci: &ComponentInterface, config: &Config) -> String { 22 | potentially_add_external_package( 23 | config, 24 | ci, 25 | &self.id, 26 | super::JavaCodeOracle.class_name(ci, &self.id), 27 | ) 28 | } 29 | 30 | fn canonical_name(&self) -> String { 31 | format!("Type{}", self.id) 32 | } 33 | 34 | fn literal(&self, literal: &Literal, ci: &ComponentInterface, config: &Config) -> String { 35 | if let Literal::Enum(v, _) = literal { 36 | format!( 37 | "{}.{}", 38 | self.type_label(ci, config), 39 | super::JavaCodeOracle.enum_variant_name(v) 40 | ) 41 | } else { 42 | unreachable!(); 43 | } 44 | } 45 | 46 | fn ffi_converter_instance(&self, config: &Config, ci: &ComponentInterface) -> String { 47 | potentially_add_external_package( 48 | config, 49 | ci, 50 | &self.id, 51 | format!("{}.INSTANCE", self.ffi_converter_name()), 52 | ) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/templates/wrapper.java: -------------------------------------------------------------------------------- 1 | // This file was autogenerated by some hot garbage in the `uniffi` crate. 2 | // Trust me, you don't want to mess with it! 3 | 4 | // Common helper code. 5 | // 6 | // Ideally this would live in a separate .java file where it can be unittested etc 7 | // in isolation, and perhaps even published as a re-useable package. 8 | // 9 | // However, it's important that the details of how this helper code works (e.g. the 10 | // way that different builtin types are passed across the FFI) exactly match what's 11 | // expected by the Rust code on the other side of the interface. In practice right 12 | // now that means coming from the exact some version of `uniffi` that was used to 13 | // compile the Rust component. The easiest way to ensure this is to bundle the Java 14 | // helpers directly inline like we're doing here. 15 | 16 | {% include "RustBufferTemplate.java" %} 17 | {% include "FfiConverterTemplate.java" %} 18 | {% include "Helpers.java" %} 19 | {% include "HandleMap.java" %} 20 | 21 | // Contains loading, initialization code, 22 | // and the FFI Function declarations in a com.sun.jna.Library. 23 | {% include "NamespaceLibraryTemplate.java" %} 24 | 25 | // Async support 26 | {%- if ci.has_async_fns() %} 27 | {% include "Async.java" %} 28 | {%- endif %} 29 | 30 | // Public interface members begin here. 31 | {{ type_helper_code }} 32 | 33 | package {{ config.package_name() }}; 34 | 35 | import java.util.List; 36 | import java.util.Map; 37 | import java.util.concurrent.CompletableFuture; 38 | 39 | {%- call java::docstring_value(ci.namespace_docstring(), 0) %} 40 | public class {{ ci.namespace()|class_name(ci) }} { 41 | {%- for func in ci.function_definitions() %} 42 | {% call java::func_decl("public static", "", func, 4) %} 43 | {%- endfor %} 44 | } 45 | 46 | {% import "macros.java" as java %} 47 | -------------------------------------------------------------------------------- /src/templates/TimestampHelper.java: -------------------------------------------------------------------------------- 1 | package {{ config.package_name() }}; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.time.Instant; 5 | import java.time.Duration; 6 | import java.time.DateTimeException; 7 | 8 | public enum FfiConverterTimestamp implements FfiConverterRustBuffer { 9 | INSTANCE; 10 | 11 | @Override 12 | public Instant read(ByteBuffer buf) { 13 | long seconds = buf.getLong(); 14 | // Type mismatch (should be u32) but we check for overflow/underflow below 15 | long nanoseconds = (long) buf.getInt(); 16 | if (nanoseconds < 0) { 17 | throw new DateTimeException("Instant nanoseconds exceed minimum or maximum supported by uniffi"); 18 | } 19 | if (seconds >= 0) { 20 | return Instant.EPOCH.plus(Duration.ofSeconds(seconds, nanoseconds)); 21 | } else { 22 | return Instant.EPOCH.minus(Duration.ofSeconds(-seconds, nanoseconds)); 23 | } 24 | } 25 | 26 | // 8 bytes for seconds, 4 bytes for nanoseconds 27 | @Override 28 | public long allocationSize(Instant value) { 29 | return 12L; 30 | } 31 | 32 | @Override 33 | public void write(Instant value, ByteBuffer buf) { 34 | Duration epochOffset = Duration.between(Instant.EPOCH, value); 35 | 36 | var sign = 1; 37 | if (epochOffset.isNegative()) { 38 | sign = -1; 39 | epochOffset = epochOffset.negated(); 40 | } 41 | 42 | if (epochOffset.getNano() < 0) { 43 | // Java docs provide guarantee that nano will always be positive, so this should be impossible 44 | // See: https://docs.oracle.com/javase/8/docs/api/java/time/Instant.html 45 | throw new IllegalArgumentException("Invalid timestamp, nano value must be non-negative"); 46 | } 47 | 48 | buf.putLong(sign * epochOffset.getSeconds()); 49 | // Type mismatch (should be u32) but since values will always be between 0 and 999,999,999 it should be OK 50 | buf.putInt(epochOffset.getNano()); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/templates/DurationHelper.java: -------------------------------------------------------------------------------- 1 | package {{ config.package_name() }}; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.time.Instant; 5 | import java.time.Duration; 6 | import java.time.DateTimeException; 7 | 8 | public enum FfiConverterDuration implements FfiConverterRustBuffer { 9 | INSTANCE; 10 | 11 | @Override 12 | public Duration read(ByteBuffer buf) { 13 | // Type mismatch (should be u64) but we check for overflow/underflow below 14 | long seconds = buf.getLong(); 15 | // Type mismatch (should be u32) but we check for overflow/underflow below 16 | long nanoseconds = (long) buf.getInt(); 17 | if (seconds < 0) { 18 | throw new DateTimeException("Duration exceeds minimum or maximum value supported by uniffi"); 19 | } 20 | if (nanoseconds < 0) { 21 | throw new DateTimeException("Duration nanoseconds exceed minimum or maximum supported by uniffi"); 22 | } 23 | return Duration.ofSeconds(seconds, nanoseconds); 24 | } 25 | 26 | // 8 bytes for seconds, 4 bytes for nanoseconds 27 | @Override 28 | public long allocationSize(Duration value) { 29 | return 12L; 30 | } 31 | 32 | @Override 33 | public void write(Duration value, ByteBuffer buf) { 34 | if (value.getSeconds() < 0) { 35 | // Rust does not support negative Durations 36 | throw new IllegalArgumentException("Invalid duration, must be non-negative"); 37 | } 38 | 39 | if (value.getNano() < 0) { 40 | // Java docs provide guarantee that nano will always be positive, so this should be impossible 41 | // See: https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html 42 | throw new IllegalArgumentException("Invalid duration, nano value must be non-negative"); 43 | } 44 | 45 | // Type mismatch (should be u64) but since Rust doesn't support negative durations we should be OK 46 | buf.putLong(value.getSeconds()); 47 | // Type mismatch (should be u32) but since values will always be between 0 and 999,999,999 it should be OK 48 | buf.putInt(value.getNano()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "uniffi-bindgen-java" 3 | version = "0.2.1" 4 | authors = ["IronCore Labs "] 5 | readme = "README.md" 6 | license = "MPL-2.0" 7 | repository = "https://github.com/IronCoreLabs/uniffi-bindgen-java" 8 | documentation = "https://docs.rs/uniffi-bindgen-java" 9 | keywords = ["bindgen", "ffi", "java"] 10 | description = "a java bindings generator for uniffi rust" 11 | exclude = [".github/*", ".envrc", "flake.nix", "flake.lock", "RELEASING.md"] 12 | edition = "2024" 13 | 14 | [lib] 15 | name = "uniffi_bindgen_java" 16 | path = "src/lib.rs" 17 | 18 | [[bin]] 19 | name = "uniffi-bindgen-java" 20 | path = "src/main.rs" 21 | 22 | [dependencies] 23 | anyhow = "1" 24 | askama = { version = "0.13", default-features = false, features = ["config"] } 25 | camino = "1.1.6" 26 | cargo_metadata = "0.19" 27 | clap = { version = "4", default-features = false, features = [ 28 | "derive", 29 | "help", 30 | "std", 31 | "cargo", 32 | ] } 33 | heck = "0.5" 34 | once_cell = "1.19.0" 35 | paste = "1" 36 | regex = "1.10.4" 37 | serde = "1" 38 | textwrap = "0.16.1" 39 | toml = "0.5" # can't be on 8, `Value` is part of public interface 40 | uniffi_bindgen = "0.29.2" 41 | uniffi_meta = "0.29.2" 42 | 43 | [dev-dependencies] 44 | glob = "0.3" 45 | itertools = "0.14.0" 46 | uniffi-example-arithmetic = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.29.2" } 47 | uniffi-example-custom-types = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.29.2" } 48 | uniffi-example-futures = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.29.2" } 49 | uniffi-example-geometry = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.29.2" } 50 | uniffi-example-rondpoint = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.29.2" } 51 | uniffi-fixture-coverall = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.29.2" } 52 | uniffi-fixture-ext-types = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.29.2" } 53 | uniffi-fixture-futures = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.29.2" } 54 | uniffi-fixture-time = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.29.2" } 55 | uniffi_testing = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.29.2" } 56 | -------------------------------------------------------------------------------- /src/templates/MapTemplate.java: -------------------------------------------------------------------------------- 1 | {%- let key_type_name = key_type|type_name(ci, config) %} 2 | {%- let value_type_name = value_type|type_name(ci, config) %} 3 | package {{ config.package_name() }}; 4 | 5 | import java.nio.ByteBuffer; 6 | import java.util.Map; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.stream.IntStream; 10 | import java.util.stream.Stream; 11 | import java.util.stream.Collectors; 12 | 13 | public enum {{ ffi_converter_name }} implements FfiConverterRustBuffer> { 14 | INSTANCE; 15 | 16 | @Override 17 | public Map<{{ key_type_name }}, {{ value_type_name }}> read(ByteBuffer buf) { 18 | int len = buf.getInt(); 19 | // Collectors.toMap would be preferred here, but theres a bug that doesn't allow 20 | // null values in the map, even though that is valid Java 21 | return IntStream.range(0, len).boxed().collect( 22 | HashMap::new, 23 | (m, v) -> m.put( 24 | {{ key_type|read_fn(config, ci) }}(buf), 25 | {{ value_type|read_fn(config, ci) }}(buf) 26 | ), 27 | HashMap::putAll 28 | ); 29 | } 30 | 31 | @Override 32 | public long allocationSize(Map<{{ key_type_name }}, {{ value_type_name }}> value) { 33 | long spaceForMapSize = 4; 34 | long spaceForChildren = value.entrySet().stream().mapToLong(entry -> 35 | {{ key_type|allocation_size_fn(config, ci) }}(entry.getKey()) + 36 | {{ value_type|allocation_size_fn(config, ci) }}(entry.getValue()) 37 | ).sum(); 38 | return spaceForMapSize + spaceForChildren; 39 | } 40 | 41 | @Override 42 | public void write(Map<{{ key_type_name }}, {{ value_type_name }}> value, ByteBuffer buf) { 43 | buf.putInt(value.size()); 44 | // The parens on `(k, v)` here ensure we're calling the right method, 45 | // which is important for compatibility with older android devices. 46 | // Ref https://blog.danlew.net/2017/03/16/kotlin-puzzler-whose-line-is-it-anyways/ 47 | for (var entry : value.entrySet()) { 48 | {{ key_type|write_fn(config, ci) }}(entry.getKey(), buf); 49 | {{ value_type|write_fn(config, ci) }}(entry.getValue(), buf); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/scripts/TestChronological.java: -------------------------------------------------------------------------------- 1 | import uniffi.chronological.*; 2 | 3 | import java.time.Duration; 4 | import java.time.Instant; 5 | import java.time.DateTimeException; 6 | 7 | public class TestChronological { 8 | public static void main(String[] args) throws Exception { 9 | // Test passing timestamp and duration while returning timestamp 10 | assert Chronological.add(Instant.ofEpochSecond(100, 100), Duration.ofSeconds(1, 1)) 11 | .equals(Instant.ofEpochSecond(101, 101)); 12 | 13 | // Test passing timestamp while returning duration 14 | assert Chronological.diff(Instant.ofEpochSecond(101, 101), Instant.ofEpochSecond(100, 100)) 15 | .equals(Duration.ofSeconds(1, 1)); 16 | 17 | // Test pre-epoch timestamps 18 | assert Chronological.add(Instant.parse("1955-11-05T00:06:00.283000001Z"), Duration.ofSeconds(1, 1)) 19 | .equals(Instant.parse("1955-11-05T00:06:01.283000002Z")); 20 | 21 | // Test exceptions are propagated 22 | try { 23 | Chronological.diff(Instant.ofEpochSecond(100), Instant.ofEpochSecond(101)); 24 | throw new RuntimeException("Should have thrown a TimeDiffError exception!"); 25 | } catch (ChronologicalException e) { 26 | // It's okay! 27 | } 28 | 29 | // Test max Instant upper bound 30 | assert Chronological.add(Instant.MAX, Duration.ofSeconds(0)).equals(Instant.MAX); 31 | 32 | // Test max Instant upper bound overflow 33 | try { 34 | Chronological.add(Instant.MAX, Duration.ofSeconds(1)); 35 | throw new RuntimeException("Should have thrown a DateTimeException exception!"); 36 | } catch (DateTimeException e) { 37 | // It's okay! 38 | } 39 | 40 | // Test that rust timestamps behave like Java timestamps 41 | // Unfortunately the JVM clock may be lower resolution than the Rust clock. 42 | // Sleep for 1ms between each call, which should ensure the JVM clock ticks 43 | // forward. 44 | var javaBefore = Instant.now(); 45 | Thread.sleep(10); 46 | var rustNow = Chronological.now(); 47 | Thread.sleep(10); 48 | var javaAfter = Instant.now(); 49 | assert javaBefore.isBefore(rustNow); 50 | assert javaAfter.isAfter(rustNow); 51 | 52 | // Test optional values work 53 | assert Chronological.optional(Instant.MAX, Duration.ofSeconds(0)); 54 | assert Chronological.optional(null, Duration.ofSeconds(0)) == false; 55 | assert Chronological.optional(Instant.MAX, null) == false; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1748460289, 24 | "narHash": "sha256-7doLyJBzCllvqX4gszYtmZUToxKvMUrg45EUWaUYmBg=", 25 | "owner": "nixos", 26 | "repo": "nixpkgs", 27 | "rev": "96ec055edbe5ee227f28cdbc3f1ddf1df5965102", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "nixos", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "nixpkgs_2": { 38 | "locked": { 39 | "lastModified": 1744536153, 40 | "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", 41 | "owner": "NixOS", 42 | "repo": "nixpkgs", 43 | "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", 44 | "type": "github" 45 | }, 46 | "original": { 47 | "owner": "NixOS", 48 | "ref": "nixpkgs-unstable", 49 | "repo": "nixpkgs", 50 | "type": "github" 51 | } 52 | }, 53 | "root": { 54 | "inputs": { 55 | "flake-utils": "flake-utils", 56 | "nixpkgs": "nixpkgs", 57 | "rust-overlay": "rust-overlay" 58 | } 59 | }, 60 | "rust-overlay": { 61 | "inputs": { 62 | "nixpkgs": "nixpkgs_2" 63 | }, 64 | "locked": { 65 | "lastModified": 1748572605, 66 | "narHash": "sha256-k0nhPtkVDQkVJckRw6fGIeeDBktJf1BH0i8T48o7zkk=", 67 | "owner": "oxalica", 68 | "repo": "rust-overlay", 69 | "rev": "405ef13a5b80a0a4d4fc87c83554423d80e5f929", 70 | "type": "github" 71 | }, 72 | "original": { 73 | "owner": "oxalica", 74 | "repo": "rust-overlay", 75 | "type": "github" 76 | } 77 | }, 78 | "systems": { 79 | "locked": { 80 | "lastModified": 1681028828, 81 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 82 | "owner": "nix-systems", 83 | "repo": "default", 84 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 85 | "type": "github" 86 | }, 87 | "original": { 88 | "owner": "nix-systems", 89 | "repo": "default", 90 | "type": "github" 91 | } 92 | } 93 | }, 94 | "root": "root", 95 | "version": 7 96 | } 97 | -------------------------------------------------------------------------------- /src/templates/StringHelper.java: -------------------------------------------------------------------------------- 1 | package {{ config.package_name() }}; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.nio.CharBuffer; 5 | import java.nio.charset.CharacterCodingException; 6 | import java.nio.charset.CharsetEncoder; 7 | import java.nio.charset.CodingErrorAction; 8 | import java.nio.charset.StandardCharsets; 9 | 10 | public enum FfiConverterString implements FfiConverter { 11 | INSTANCE; 12 | 13 | // Note: we don't inherit from FfiConverterRustBuffer, because we use a 14 | // special encoding when lowering/lifting. We can use `RustBuffer.len` to 15 | // store our length and avoid writing it out to the buffer. 16 | @Override 17 | public String lift(RustBuffer.ByValue value) { 18 | try { 19 | byte[] byteArr = new byte[(int) value.len]; 20 | value.asByteBuffer().get(byteArr); 21 | return new String(byteArr, StandardCharsets.UTF_8); 22 | } finally { 23 | RustBuffer.free(value); 24 | } 25 | } 26 | 27 | @Override 28 | public String read(ByteBuffer buf) { 29 | int len = buf.getInt(); 30 | byte[] byteArr = new byte[len]; 31 | buf.get(byteArr); 32 | return new String(byteArr, StandardCharsets.UTF_8); 33 | } 34 | 35 | private ByteBuffer toUtf8(String value) { 36 | // Make sure we don't have invalid UTF-16, check for lone surrogates. 37 | CharsetEncoder encoder = StandardCharsets.UTF_8.newEncoder(); 38 | encoder.onMalformedInput(CodingErrorAction.REPORT); 39 | try { 40 | return encoder.encode(CharBuffer.wrap(value)); 41 | } catch (CharacterCodingException e) { 42 | throw new RuntimeException(e); 43 | } 44 | } 45 | 46 | @Override 47 | public RustBuffer.ByValue lower(String value) { 48 | ByteBuffer byteBuf = toUtf8(value); 49 | // Ideally we'd pass these bytes to `ffi_bytebuffer_from_bytes`, but doing so would require us 50 | // to copy them into a JNA `Memory`. So we might as well directly copy them into a `RustBuffer`. 51 | RustBuffer.ByValue rbuf = RustBuffer.alloc((long) byteBuf.limit()); 52 | rbuf.asByteBuffer().put(byteBuf); 53 | return rbuf; 54 | } 55 | 56 | // We aren't sure exactly how many bytes our string will be once it's UTF-8 57 | // encoded. Allocate 3 bytes per UTF-16 code unit which will always be 58 | // enough. 59 | @Override 60 | public long allocationSize(String value) { 61 | long sizeForLength = 4L; 62 | long sizeForString = (long) value.length() * 3L; 63 | return sizeForLength + sizeForString; 64 | } 65 | 66 | @Override 67 | public void write(String value, ByteBuffer buf) { 68 | ByteBuffer byteBuf = toUtf8(value); 69 | buf.putInt(byteBuf.limit()); 70 | buf.put(byteBuf); 71 | } 72 | } 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/templates/ObjectCleanerHelper.java: -------------------------------------------------------------------------------- 1 | package {{ config.package_name() }}; 2 | 3 | // The cleaner interface for Object finalization code to run. 4 | // This is the entry point to any implementation that we're using. 5 | // 6 | // The cleaner registers objects and returns cleanables, so now we are 7 | // defining a `UniffiCleaner` with a `UniffiClenaer.Cleanable` to abstract the 8 | // different implmentations available at compile time. 9 | interface UniffiCleaner { 10 | interface Cleanable { 11 | void clean(); 12 | } 13 | 14 | UniffiCleaner.Cleanable register(Object value, Runnable cleanUpTask); 15 | 16 | public static UniffiCleaner create() { 17 | {% if config.android_cleaner() %} 18 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { 19 | return new AndroidSystemCleaner(); 20 | } else { 21 | return new UniffiJnaCleaner(); 22 | } 23 | {%- else %} 24 | try { 25 | // For safety's sake: if the library hasn't been run in android_cleaner = true 26 | // mode, but is being run on Android, then we still need to think about 27 | // Android API versions. 28 | // So we check if java.lang.ref.Cleaner is there, and use that… 29 | Class.forName("java.lang.ref.Cleaner"); 30 | return new JavaLangRefCleaner(); 31 | } catch (ClassNotFoundException e) { 32 | // … otherwise, fallback to the JNA cleaner. 33 | return new UniffiJnaCleaner(); 34 | } 35 | {%- endif %} 36 | } 37 | } 38 | 39 | package {{ config.package_name() }}; 40 | 41 | import com.sun.jna.internal.Cleaner; 42 | 43 | // The fallback Jna cleaner, which is available for both Android, and the JVM. 44 | class UniffiJnaCleaner implements UniffiCleaner { 45 | private final Cleaner cleaner = Cleaner.getCleaner(); 46 | 47 | @Override 48 | public UniffiCleaner.Cleanable register(Object value, Runnable cleanUpTask) { 49 | return new UniffiJnaCleanable(cleaner.register(value, cleanUpTask)); 50 | } 51 | } 52 | 53 | package {{ config.package_name() }}; 54 | 55 | import com.sun.jna.internal.Cleaner; 56 | 57 | class UniffiJnaCleanable implements UniffiCleaner.Cleanable { 58 | private final Cleaner.Cleanable cleanable; 59 | 60 | public UniffiJnaCleanable(Cleaner.Cleanable cleanable) { 61 | this.cleanable = cleanable; 62 | } 63 | 64 | @Override 65 | public void clean() { 66 | cleanable.clean(); 67 | } 68 | } 69 | 70 | // We decide at uniffi binding generation time whether we were 71 | // using Android or not. 72 | // There are further runtime checks to chose the correct implementation 73 | // of the cleaner. 74 | {% if config.android_cleaner() %} 75 | {%- include "ObjectCleanerHelperAndroid.java" %} 76 | {%- else %} 77 | {%- include "ObjectCleanerHelperJvm.java" %} 78 | {%- endif %} 79 | 80 | -------------------------------------------------------------------------------- /src/templates/FfiConverterTemplate.java: -------------------------------------------------------------------------------- 1 | package {{ config.package_name() }}; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.nio.ByteOrder; 5 | 6 | // The FfiConverter interface handles converter types to and from the FFI 7 | // 8 | // All implementing objects should be public to support external types. When a 9 | // type is external we need to import it's FfiConverter. 10 | public interface FfiConverter { 11 | // Convert an FFI type to a Java type 12 | JavaType lift(FfiType value); 13 | 14 | // Convert an Java type to an FFI type 15 | FfiType lower(JavaType value); 16 | 17 | // Read a Java type from a `ByteBuffer` 18 | JavaType read(ByteBuffer buf); 19 | 20 | // Calculate bytes to allocate when creating a `RustBuffer` 21 | // 22 | // This must return at least as many bytes as the write() function will 23 | // write. It can return more bytes than needed, for example when writing 24 | // Strings we can't know the exact bytes needed until we the UTF-8 25 | // encoding, so we pessimistically allocate the largest size possible (3 26 | // bytes per codepoint). Allocating extra bytes is not really a big deal 27 | // because the `RustBuffer` is short-lived. 28 | long allocationSize(JavaType value); 29 | 30 | // Write a Java type to a `ByteBuffer` 31 | void write(JavaType value, ByteBuffer buf); 32 | 33 | // Lower a value into a `RustBuffer` 34 | // 35 | // This method lowers a value into a `RustBuffer` rather than the normal 36 | // FfiType. It's used by the callback interface code. Callback interface 37 | // returns are always serialized into a `RustBuffer` regardless of their 38 | // normal FFI type. 39 | default RustBuffer.ByValue lowerIntoRustBuffer(JavaType value) { 40 | RustBuffer.ByValue rbuf = RustBuffer.alloc(allocationSize(value)); 41 | try { 42 | ByteBuffer bbuf = rbuf.data.getByteBuffer(0, rbuf.capacity); 43 | bbuf.order(ByteOrder.BIG_ENDIAN); 44 | write(value, bbuf); 45 | rbuf.writeField("len", (long)bbuf.position()); 46 | return rbuf; 47 | } catch (Throwable e) { 48 | RustBuffer.free(rbuf); 49 | throw e; 50 | } 51 | } 52 | 53 | // Lift a value from a `RustBuffer`. 54 | // 55 | // This here mostly because of the symmetry with `lowerIntoRustBuffer()`. 56 | // It's currently only used by the `FfiConverterRustBuffer` class below. 57 | default JavaType liftFromRustBuffer(RustBuffer.ByValue rbuf) { 58 | ByteBuffer byteBuf = rbuf.asByteBuffer(); 59 | try { 60 | JavaType item = read(byteBuf); 61 | if (byteBuf.hasRemaining()) { 62 | throw new RuntimeException("junk remaining in buffer after lifting, something is very wrong!!"); 63 | } 64 | return item; 65 | } finally { 66 | RustBuffer.free(rbuf); 67 | } 68 | } 69 | } 70 | 71 | package {{ config.package_name() }}; 72 | 73 | // FfiConverter that uses `RustBuffer` as the FfiType 74 | public interface FfiConverterRustBuffer extends FfiConverter { 75 | @Override 76 | default JavaType lift(RustBuffer.ByValue value) { 77 | return liftFromRustBuffer(value); 78 | } 79 | @Override 80 | default RustBuffer.ByValue lower(JavaType value) { 81 | return lowerIntoRustBuffer(value); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/scripts/TestImportedTypes/TestImportedTypes.java: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import uniffi.imported_types_lib.*; 6 | import uniffi.imported_types_sublib.*; 7 | import uniffi.uniffi_one_ns.*; 8 | import uniffi.ext_types_custom.*; 9 | import uniffi.custom_types.*; 10 | import java.util.List; 11 | import java.util.stream.Stream; 12 | 13 | public class TestImportedTypes { 14 | public static void main(String[] args) throws Exception { 15 | var ct = ImportedTypesLib.getCombinedType(null); 16 | assert ct.uot().sval().equals("hello"); 17 | assert ct.guid().equals(new Guid("a-guid")); 18 | assert ct.url().equals(new Url(new java.net.URL("http://example.com/"))); 19 | 20 | var ct2 = ImportedTypesLib.getCombinedType(null); 21 | assert ct.equals(ct2); 22 | 23 | assert ImportedTypesLib.getObjectsType(null).maybeInterface() == null; 24 | assert ImportedTypesLib.getObjectsType(null).maybeTrait() == null; 25 | assert ImportedTypesLib.getUniffiOneTrait(null) == null; 26 | 27 | assert ImportedTypesSublib.getSubType(null).maybeInterface() == null; 28 | assert ImportedTypesSublib.getTraitImpl().hello().equals("sub-lib trait impl says hello"); 29 | 30 | var url = new Url(new java.net.URL("http://example.com/")); 31 | assert ImportedTypesLib.getUrl(url).equals(url); 32 | assert ImportedTypesLib.getMaybeUrl(url).equals(url); 33 | assert ImportedTypesLib.getMaybeUrl(null) == null; 34 | assert ImportedTypesLib.getUrls(List.of(url)).equals(List.of(url)); 35 | // List.of doesn't allow `null`, though `null` is allowed in `List` /shrug 36 | assert ImportedTypesLib.getMaybeUrls(Stream.of(url, null).toList()).equals(Stream.of(url, null).toList()); 37 | 38 | assert ExtTypesCustom.getGuid(new Guid("guid")).equals(new Guid("guid")); 39 | assert ExtTypesCustom.getOuid(new Ouid("ouid")).equals(new Ouid("ouid")); 40 | // assert ImportedTypesLib.getImportedGuid(new Guid("guid")).equals(new Guid("guid")); 41 | assert ImportedTypesLib.getImportedOuid(new Ouid("ouid")).equals(new Ouid("ouid")); 42 | 43 | var uot = new UniffiOneType("hello"); 44 | assert ImportedTypesLib.getUniffiOneType(uot).equals(uot); 45 | assert ImportedTypesLib.getMaybeUniffiOneType(uot).equals(uot); 46 | assert ImportedTypesLib.getMaybeUniffiOneType(null) == null; 47 | assert ImportedTypesLib.getUniffiOneTypes(List.of(uot)).equals(List.of(uot)); 48 | assert ImportedTypesLib.getMaybeUniffiOneTypes(Stream.of(uot, null).toList()).equals(Stream.of(uot, null).toList()); 49 | 50 | var uopmt = new UniffiOneProcMacroType("hello from proc-macro world"); 51 | assert ImportedTypesLib.getUniffiOneProcMacroType(uopmt).equals(uopmt); 52 | assert UniffiOneNs.getMyProcMacroType(uopmt).equals(uopmt); 53 | 54 | var uoe = UniffiOneEnum.ONE; 55 | assert ImportedTypesLib.getUniffiOneEnum(uoe).equals(uoe); 56 | assert ImportedTypesLib.getMaybeUniffiOneEnum(uoe).equals(uoe); 57 | assert ImportedTypesLib.getMaybeUniffiOneEnum(null) == null; 58 | assert ImportedTypesLib.getUniffiOneEnums(List.of(uoe)).equals(List.of(uoe)); 59 | assert ImportedTypesLib.getMaybeUniffiOneEnums(Stream.of(uoe, null).toList()).equals(Stream.of(uoe, null).toList()); 60 | 61 | assert ct.ecd().sval().equals("ecd"); 62 | assert ImportedTypesLib.getExternalCrateInterface("foo").value().equals("foo"); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/gen_java/primitives.rs: -------------------------------------------------------------------------------- 1 | use super::{CodeType, Config}; 2 | use paste::paste; 3 | use uniffi_bindgen::backend::Literal; 4 | use uniffi_bindgen::interface::{ComponentInterface, Radix, Type}; 5 | 6 | fn render_literal(literal: &Literal, _ci: &ComponentInterface, _config: &Config) -> String { 7 | fn typed_number(type_: &Type, num_str: String) -> String { 8 | let unwrapped_type = match type_ { 9 | Type::Optional { inner_type } => inner_type, 10 | t => t, 11 | }; 12 | match unwrapped_type { 13 | // Bytes, Shorts and Ints can all be inferred from the type. 14 | Type::Int8 | Type::Int16 | Type::Int32 => num_str, 15 | Type::Int64 => format!("{num_str}L"), 16 | 17 | // Java has pretty hacky unsigned support, see https://docs.oracle.com/javase/8/docs/api/java/lang/Long.html (search for unsigned) 18 | // and https://docs.oracle.com/javase/8/docs/api/java/lang/Integer.html search for unsigned. 19 | Type::UInt8 | Type::UInt16 | Type::UInt32 => { 20 | format!("Integer.parseUnsignedInt({num_str})") 21 | } 22 | Type::UInt64 => format!("Long.parseUnsignedLong({num_str})"), 23 | 24 | Type::Float32 => format!("{num_str}f"), 25 | Type::Float64 => num_str, 26 | _ => panic!("Unexpected literal: {num_str} for type: {type_:?}"), 27 | } 28 | } 29 | 30 | match literal { 31 | Literal::Boolean(v) => format!("{v}"), 32 | Literal::String(s) => format!("\"{s}\""), 33 | Literal::Int(i, radix, type_) => typed_number( 34 | type_, 35 | match radix { 36 | Radix::Octal => format!("{i:#x}"), 37 | Radix::Decimal => format!("{i}"), 38 | Radix::Hexadecimal => format!("{i:#x}"), 39 | }, 40 | ), 41 | Literal::UInt(i, radix, type_) => typed_number( 42 | type_, 43 | match radix { 44 | Radix::Octal => format!("{i:#x}"), 45 | Radix::Decimal => format!("{i}"), 46 | Radix::Hexadecimal => format!("{i:#x}"), 47 | }, 48 | ), 49 | Literal::Float(string, type_) => typed_number(type_, string.clone()), 50 | 51 | _ => unreachable!("Literal"), 52 | } 53 | } 54 | 55 | macro_rules! impl_code_type_for_primitive { 56 | ($T:ty, $class_name:literal) => { 57 | paste! { 58 | #[derive(Debug)] 59 | pub struct $T; 60 | 61 | impl CodeType for $T { 62 | fn type_label(&self, _ci: &ComponentInterface, _config: &Config) -> String { 63 | format!("{}", $class_name) 64 | } 65 | 66 | fn canonical_name(&self) -> String { 67 | $class_name.into() 68 | } 69 | 70 | fn literal(&self, literal: &Literal, ci: &ComponentInterface, config: &Config) -> String { 71 | render_literal(&literal, ci, config) 72 | } 73 | } 74 | } 75 | }; 76 | } 77 | 78 | #[derive(Debug)] 79 | pub struct BytesCodeType; 80 | impl CodeType for BytesCodeType { 81 | fn type_label(&self, _ci: &ComponentInterface, _config: &Config) -> String { 82 | "byte[]".to_string() 83 | } 84 | 85 | fn canonical_name(&self) -> String { 86 | "ByteArray".to_string() 87 | } 88 | 89 | fn literal(&self, literal: &Literal, ci: &ComponentInterface, config: &Config) -> String { 90 | render_literal(&literal, ci, config) 91 | } 92 | } 93 | 94 | impl_code_type_for_primitive!(BooleanCodeType, "Boolean"); 95 | impl_code_type_for_primitive!(StringCodeType, "String"); 96 | impl_code_type_for_primitive!(Int8CodeType, "Byte"); 97 | impl_code_type_for_primitive!(Int16CodeType, "Short"); 98 | impl_code_type_for_primitive!(Int32CodeType, "Integer"); 99 | impl_code_type_for_primitive!(Int64CodeType, "Long"); 100 | impl_code_type_for_primitive!(Float32CodeType, "Float"); 101 | impl_code_type_for_primitive!(Float64CodeType, "Double"); 102 | -------------------------------------------------------------------------------- /src/gen_java/compounds.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use super::{AsCodeType, CodeType, Config}; 6 | use uniffi_bindgen::{ 7 | ComponentInterface, 8 | backend::{Literal, Type}, 9 | }; 10 | 11 | #[derive(Debug)] 12 | pub struct OptionalCodeType { 13 | inner: Type, 14 | } 15 | 16 | impl OptionalCodeType { 17 | pub fn new(inner: Type) -> Self { 18 | Self { inner } 19 | } 20 | fn inner(&self) -> &Type { 21 | &self.inner 22 | } 23 | } 24 | 25 | impl CodeType for OptionalCodeType { 26 | fn type_label(&self, ci: &ComponentInterface, config: &Config) -> String { 27 | super::JavaCodeOracle 28 | .find(self.inner()) 29 | .type_label(ci, config) 30 | .to_string() 31 | } 32 | 33 | fn canonical_name(&self) -> String { 34 | format!( 35 | "Optional{}", 36 | super::JavaCodeOracle.find(self.inner()).canonical_name() 37 | ) 38 | } 39 | 40 | fn literal(&self, literal: &Literal, ci: &ComponentInterface, config: &Config) -> String { 41 | match literal { 42 | Literal::None => "null".into(), 43 | Literal::Some { inner } => super::JavaCodeOracle 44 | .find(&self.inner) 45 | .literal(inner, ci, config), 46 | _ => panic!("Invalid literal for Optional type: {literal:?}"), 47 | } 48 | } 49 | } 50 | 51 | #[derive(Debug)] 52 | pub struct SequenceCodeType { 53 | inner: Type, 54 | } 55 | 56 | impl SequenceCodeType { 57 | pub fn new(inner: Type) -> Self { 58 | Self { inner } 59 | } 60 | fn inner(&self) -> &Type { 61 | &self.inner 62 | } 63 | } 64 | 65 | impl CodeType for SequenceCodeType { 66 | fn type_label(&self, ci: &ComponentInterface, config: &Config) -> String { 67 | format!( 68 | "List<{}>", 69 | super::JavaCodeOracle 70 | .find(self.inner()) 71 | .type_label(ci, config) 72 | ) 73 | } 74 | 75 | fn canonical_name(&self) -> String { 76 | format!( 77 | "Sequence{}", 78 | super::JavaCodeOracle.find(self.inner()).canonical_name() 79 | ) 80 | } 81 | 82 | fn literal(&self, literal: &Literal, _ci: &ComponentInterface, _config: &Config) -> String { 83 | match literal { 84 | Literal::EmptySequence => "List.of()".into(), 85 | _ => panic!("Invalid literal for List type: {literal:?}"), 86 | } 87 | } 88 | } 89 | 90 | #[derive(Debug)] 91 | pub struct MapCodeType { 92 | key: Type, 93 | value: Type, 94 | } 95 | 96 | impl MapCodeType { 97 | pub fn new(key: Type, value: Type) -> Self { 98 | Self { key, value } 99 | } 100 | 101 | fn key(&self) -> &Type { 102 | &self.key 103 | } 104 | 105 | fn value(&self) -> &Type { 106 | &self.value 107 | } 108 | } 109 | 110 | impl CodeType for MapCodeType { 111 | fn type_label(&self, ci: &ComponentInterface, config: &Config) -> String { 112 | format!( 113 | "Map<{}, {}>", 114 | super::JavaCodeOracle 115 | .find(self.key()) 116 | .type_label(ci, config), 117 | super::JavaCodeOracle 118 | .find(self.value()) 119 | .type_label(ci, config), 120 | ) 121 | } 122 | 123 | fn canonical_name(&self) -> String { 124 | format!( 125 | "Map{}{}", 126 | self.key().as_codetype().canonical_name(), 127 | self.value().as_codetype().canonical_name(), 128 | ) 129 | } 130 | 131 | fn literal(&self, literal: &Literal, _ci: &ComponentInterface, _config: &Config) -> String { 132 | match literal { 133 | Literal::EmptyMap => "Map.of()".into(), 134 | _ => panic!("Invalid literal for Map type: {literal:?}"), 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/templates/RustBufferTemplate.java: -------------------------------------------------------------------------------- 1 | package {{ config.package_name() }}; 2 | 3 | import com.sun.jna.Structure; 4 | import com.sun.jna.Pointer; 5 | 6 | /** 7 | * This is a helper for safely working with byte buffers returned from the Rust code. 8 | * A rust-owned buffer is represented by its capacity, its current length, and a 9 | * pointer to the underlying data. 10 | */ 11 | @Structure.FieldOrder({ "capacity", "len", "data" }) 12 | public class RustBuffer extends Structure { 13 | public long capacity; 14 | public long len; 15 | public Pointer data; 16 | 17 | public static class ByValue extends RustBuffer implements Structure.ByValue {} 18 | public static class ByReference extends RustBuffer implements Structure.ByReference {} 19 | 20 | void setValue(RustBuffer other) { 21 | this.capacity = other.capacity; 22 | this.len = other.len; 23 | this.data = other.data; 24 | } 25 | 26 | public static RustBuffer.ByValue alloc(long size) { 27 | RustBuffer.ByValue buffer = UniffiHelpers.uniffiRustCall((UniffiRustCallStatus status) -> { 28 | return (RustBuffer.ByValue) UniffiLib.INSTANCE.{{ ci.ffi_rustbuffer_alloc().name() }}(size, status); 29 | }); 30 | if (buffer.data == null) { 31 | throw new RuntimeException("RustBuffer.alloc() returned null data pointer (size=" + size + ")"); 32 | } 33 | return buffer; 34 | } 35 | 36 | public static void free(RustBuffer.ByValue buffer) { 37 | UniffiHelpers.uniffiRustCall((status) -> { 38 | UniffiLib.INSTANCE.{{ ci.ffi_rustbuffer_free().name() }}(buffer, status); 39 | return null; 40 | }); 41 | } 42 | 43 | public java.nio.ByteBuffer asByteBuffer() { 44 | if (this.data != null) { 45 | java.nio.ByteBuffer byteBuffer = this.data.getByteBuffer(0, this.len); 46 | byteBuffer.order(java.nio.ByteOrder.BIG_ENDIAN); 47 | return byteBuffer; 48 | } 49 | return null; 50 | } 51 | } 52 | 53 | package {{ config.package_name() }}; 54 | 55 | import com.sun.jna.Structure; 56 | import com.sun.jna.Pointer; 57 | /** 58 | * The equivalent of the `*mut RustBuffer` type. 59 | * Required for callbacks taking in an out pointer. 60 | * 61 | * Size is the sum of all values in the struct. 62 | */ 63 | public class RustBufferByReference extends Structure implements Structure.ByReference { 64 | public RustBufferByReference() { 65 | super(16); 66 | } 67 | 68 | /** 69 | * Set the pointed-to `RustBuffer` to the given value. 70 | */ 71 | public void setValue(RustBuffer.ByValue value) { 72 | // NOTE: The offsets are as they are in the C-like struct. 73 | Pointer pointer = this.getPointer(); 74 | pointer.setInt(0, (int) value.capacity); 75 | pointer.setInt(4, (int) value.len); 76 | pointer.setPointer(8, value.data); 77 | } 78 | 79 | /** 80 | * Get a `RustBuffer.ByValue` from this reference. 81 | */ 82 | public RustBuffer.ByValue getValue() { 83 | Pointer pointer = this.getPointer(); 84 | RustBuffer.ByValue value = new RustBuffer.ByValue(); 85 | value.writeField("capacity", pointer.getLong(0)); 86 | value.writeField("len", pointer.getLong(8)); 87 | value.writeField("data", pointer.getLong(16)); 88 | 89 | return value; 90 | } 91 | } 92 | 93 | package {{ config.package_name() }}; 94 | 95 | import com.sun.jna.Structure; 96 | import com.sun.jna.Pointer; 97 | 98 | // This is a helper for safely passing byte references into the rust code. 99 | // It's not actually used at the moment, because there aren't many things that you 100 | // can take a direct pointer to in the JVM, and if we're going to copy something 101 | // then we might as well copy it into a `RustBuffer`. But it's here for API 102 | // completeness. 103 | @Structure.FieldOrder({ "len", "data" }) 104 | public class ForeignBytes extends Structure { 105 | public int len; 106 | public Pointer data; 107 | 108 | public static class ByValue extends ForeignBytes implements Structure.ByValue {} 109 | } 110 | -------------------------------------------------------------------------------- /src/templates/CustomTypeTemplate.java: -------------------------------------------------------------------------------- 1 | {%- let package_name = config.package_name() %} 2 | {%- let ffi_type_name=builtin|ffi_type|ref|ffi_type_name_by_value(config, ci) %} 3 | {%- match config.custom_types.get(name.as_str()) %} 4 | {%- when None %} 5 | {#- Define a newtype record that delegates to the builtin #} 6 | 7 | package {{ package_name }}; 8 | 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | public record {{ type_name }}( 13 | {{ builtin|type_name(ci, config) }} value 14 | ) { 15 | } 16 | 17 | package {{ package_name }}; 18 | 19 | import java.nio.ByteBuffer; 20 | import com.sun.jna.Pointer; 21 | 22 | public enum {{ ffi_converter_name }} implements FfiConverter<{{ type_name }}, {{ ffi_type_name}}> { 23 | INSTANCE; 24 | @Override 25 | public {{ type_name }} lift({{ ffi_type_name }} value) { 26 | var builtinValue = {{ builtin|lift_fn(config, ci) }}(value); 27 | return new {{ type_name }}(builtinValue); 28 | } 29 | @Override 30 | public {{ ffi_type_name }} lower({{ type_name }} value) { 31 | var builtinValue = value.value(); 32 | return {{ builtin|lower_fn(config, ci) }}(builtinValue); 33 | } 34 | @Override 35 | public {{ type_name }} read(ByteBuffer buf) { 36 | var builtinValue = {{ builtin|read_fn(config, ci) }}(buf); 37 | return new {{ type_name }}(builtinValue); 38 | } 39 | @Override 40 | public long allocationSize({{ type_name }} value) { 41 | var builtinValue = value.value(); 42 | return {{ builtin|allocation_size_fn(config, ci) }}(builtinValue); 43 | } 44 | @Override 45 | public void write({{ type_name }} value, ByteBuffer buf) { 46 | var builtinValue = value.value(); 47 | {{ builtin|write_fn(config, ci) }}(builtinValue, buf); 48 | } 49 | } 50 | 51 | {%- when Some with (custom_type_config) %} 52 | 53 | {# 54 | When the config specifies a different type name, use that other type inside our newtype. 55 | Lift/lower using their configured code. 56 | #} 57 | {%- match custom_type_config.type_name %} 58 | {%- when Some(concrete_type_name) %} 59 | 60 | package {{ package_name }}; 61 | 62 | {%- match custom_type_config.imports %} 63 | {%- when Some(imports) %} 64 | {%- for import_name in imports %} 65 | import {{ import_name }}; 66 | {%- endfor %} 67 | {%- else %} 68 | {%- endmatch %} 69 | import java.util.List; 70 | import java.util.Map; 71 | 72 | public record {{ type_name }}( 73 | {{ concrete_type_name }} value 74 | ) {} 75 | 76 | {%- else %} 77 | {%- endmatch %} 78 | 79 | package {{ package_name }}; 80 | 81 | import java.nio.ByteBuffer; 82 | import com.sun.jna.Pointer; 83 | 84 | {%- match custom_type_config.imports %} 85 | {%- when Some(imports) %} 86 | {%- for import_name in imports %} 87 | import {{ import_name }}; 88 | {%- endfor %} 89 | {%- else %} 90 | {%- endmatch %} 91 | // FFI converter with custom code. 92 | public enum {{ ffi_converter_name }} implements FfiConverter<{{ type_name }}, {{ ffi_type_name }}> { 93 | INSTANCE; 94 | @Override 95 | public {{ type_name }} lift({{ ffi_type_name }} value) { 96 | var builtinValue = {{ builtin|lift_fn(config, ci) }}(value); 97 | try{ 98 | return new {{ type_name}}({{ custom_type_config.lift("builtinValue") }}); 99 | } catch(Exception e){ 100 | throw new RuntimeException(e); 101 | } 102 | } 103 | @Override 104 | public {{ ffi_type_name }} lower({{ type_name }} value) { 105 | try{ 106 | var builtinValue = {{ custom_type_config.lower("value.value()") }}; 107 | return {{ builtin|lower_fn(config, ci) }}(builtinValue); 108 | } catch(Exception e){ 109 | throw new RuntimeException(e); 110 | } 111 | } 112 | @Override 113 | public {{ type_name }} read(ByteBuffer buf) { 114 | try{ 115 | var builtinValue = {{ builtin|read_fn(config, ci) }}(buf); 116 | return new {{ type_name }}({{ custom_type_config.lift("builtinValue") }}); 117 | } catch(Exception e){ 118 | throw new RuntimeException(e); 119 | } 120 | } 121 | @Override 122 | public long allocationSize({{ type_name }} value) { 123 | try { 124 | var builtinValue = {{ custom_type_config.lower("value.value()") }}; 125 | return {{ builtin|allocation_size_fn(config, ci) }}(builtinValue); 126 | } catch(Exception e){ 127 | throw new RuntimeException(e); 128 | } 129 | } 130 | @Override 131 | public void write({{ type_name }} value, ByteBuffer buf) { 132 | try { 133 | var builtinValue = {{ custom_type_config.lower("value.value()") }}; 134 | {{ builtin|write_fn(config, ci) }}(builtinValue, buf); 135 | } catch(Exception e){ 136 | throw new RuntimeException(e); 137 | } 138 | } 139 | } 140 | {%- endmatch %} 141 | -------------------------------------------------------------------------------- /src/templates/RecordTemplate.java: -------------------------------------------------------------------------------- 1 | {%- let rec = ci.get_record_definition(name).unwrap() %} 2 | package {{ config.package_name() }}; 3 | 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.nio.ByteBuffer; 7 | import java.util.Objects; 8 | 9 | {%- call java::docstring(rec, 0) %} 10 | {%- if rec.has_fields() %} 11 | {%- if config.generate_immutable_records() %} 12 | public record {{ type_name }}( 13 | {%- for field in rec.fields() %} 14 | {%- call java::docstring(field, 4) %} 15 | {{ field|type_name(ci, config) }} {{ field.name()|var_name -}} 16 | {% if !loop.last %}, {% endif %} 17 | {%- endfor %} 18 | ) {% if contains_object_references %}implements AutoCloseable {% endif %}{ 19 | {% if contains_object_references %} 20 | @Override 21 | public void close() { 22 | {% call java::destroy_fields(rec) %} 23 | } 24 | {% endif %} 25 | } 26 | {% else %} 27 | public class {{ type_name }} {% if contains_object_references %}implements AutoCloseable {% endif %}{ 28 | {%- for field in rec.fields() %} 29 | {%- call java::docstring(field, 4) %} 30 | private {{ field|type_name(ci, config) }} {{ field.name()|var_name -}}; 31 | {%- endfor %} 32 | 33 | public {{ type_name }}( 34 | {%- for field in rec.fields() %} 35 | {{ field|type_name(ci, config) }} {{ field.name()|var_name -}} 36 | {% if !loop.last %}, {% endif %} 37 | {%- endfor %} 38 | ) { 39 | {%- for field in rec.fields() %} 40 | {% let field_var_name = field.name()|var_name %} 41 | this.{{ field_var_name }} = {{ field_var_name -}}; 42 | {%- endfor %} 43 | } 44 | 45 | {%- for field in rec.fields() %} 46 | {% let field_var_name = field.name()|var_name %} 47 | public {{ field|type_name(ci, config) }} {{ field_var_name }}() { 48 | return this.{{ field_var_name }}; 49 | } 50 | {%- endfor %} 51 | 52 | {%- for field in rec.fields() %} 53 | {%- let field_var_name = field.name()|var_name %} 54 | public void {{ field.name()|setter}}({{ field|type_name(ci, config) }} {{ field_var_name }}) { 55 | this.{{ field_var_name }} = {{ field_var_name }}; 56 | } 57 | {%- endfor %} 58 | 59 | {% if contains_object_references %} 60 | @Override 61 | public void close() { 62 | {% call java::destroy_fields(rec) %} 63 | } 64 | {% endif %} 65 | 66 | @Override 67 | public boolean equals(Object other) { 68 | if (other instanceof {{ type_name }}) { 69 | {{ type_name }} t = ({{ type_name }}) other; 70 | return ({% for field in rec.fields() %}{% let field_var_name = field.name()|var_name %} 71 | {#- currently all primitives are already referenced by their boxed values in generated code, so `.equals` works for everything #} 72 | Objects.equals({{ field_var_name }}, t.{{ field_var_name }}){% if !loop.last%} && {% endif %} 73 | {% endfor %} 74 | ); 75 | }; 76 | return false; 77 | } 78 | 79 | @Override 80 | public int hashCode() { 81 | return Objects.hash({% for field in rec.fields() %}{{ field.name()|var_name }}{% if !loop.last%}, {% endif %}{% endfor %}); 82 | } 83 | } 84 | {% endif %} 85 | {%- else %} 86 | public class {{ type_name }} { 87 | @Override 88 | public boolean equals(Object other) { 89 | return other instanceof {{ type_name }}; 90 | } 91 | 92 | @Override 93 | public int hashCode() { 94 | return getClass().hashCode(); 95 | } 96 | } 97 | {%- endif %} 98 | 99 | package {{ config.package_name() }}; 100 | 101 | import java.nio.ByteBuffer; 102 | 103 | public enum {{ rec|ffi_converter_name }} implements FfiConverterRustBuffer<{{ type_name }}> { 104 | INSTANCE; 105 | 106 | @Override 107 | public {{ type_name }} read(ByteBuffer buf) { 108 | {%- if rec.has_fields() %} 109 | return new {{ type_name }}( 110 | {%- for field in rec.fields() %} 111 | {{ field|read_fn(config, ci) }}(buf){% if !loop.last %},{% else %}{% endif %} 112 | {%- endfor %} 113 | ); 114 | {%- else %} 115 | return new {{ type_name }}(); 116 | {%- endif %} 117 | } 118 | 119 | @Override 120 | public long allocationSize({{ type_name }} value) { 121 | {%- if rec.has_fields() %} 122 | return ( 123 | {%- for field in rec.fields() %} 124 | {{ field|allocation_size_fn(config, ci) }}(value.{{ field.name()|var_name }}()){% if !loop.last %} +{% endif %} 125 | {%- endfor %} 126 | ); 127 | {%- else %} 128 | return 0L; 129 | {%- endif %} 130 | } 131 | 132 | @Override 133 | public void write({{ type_name }} value, ByteBuffer buf) { 134 | {%- for field in rec.fields() %} 135 | {{ field|write_fn(config, ci) }}(value.{{ field.name()|var_name }}(), buf); 136 | {%- endfor %} 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/templates/EnumTemplate.java: -------------------------------------------------------------------------------- 1 | package {{ config.package_name() }}; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | 6 | {%- if e.is_flat() %} 7 | {% call java::docstring(e, 0) %} 8 | {% match e.variant_discr_type() %} 9 | {% when None %} 10 | public enum {{ type_name }} { 11 | {%- for variant in e.variants() -%} 12 | {%- call java::docstring(variant, 4) %} 13 | {{ variant|variant_name}}{% if loop.last %};{% else %},{% endif %} 14 | {%- endfor %} 15 | } 16 | {% when Some with (variant_discr_type) %} 17 | public enum {{ type_name }} { 18 | {% for variant in e.variants() -%} 19 | {%- call java::docstring(variant, 4) %} 20 | {{ variant|variant_name}}({{ e|variant_discr_literal(loop.index0)}}){% if loop.last %};{% else %},{% endif %}} 21 | {%- endfor %} 22 | 23 | private final {{ variant_discr_type|type_name(ci, config) }} value; 24 | {{type_name}}({{ variant_discr_type|type_name(ci, config) }} value) { 25 | this.value = value; 26 | } 27 | } 28 | {% endmatch %} 29 | 30 | package {{ config.package_name() }}; 31 | 32 | import java.nio.ByteBuffer; 33 | 34 | public enum {{ e|ffi_converter_name}} implements FfiConverterRustBuffer<{{ type_name }}> { 35 | INSTANCE; 36 | 37 | @Override 38 | public {{ type_name }} read(ByteBuffer buf) { 39 | try { 40 | return {{ type_name }}.values()[buf.getInt() - 1]; 41 | } catch (IndexOutOfBoundsException e) { 42 | throw new RuntimeException("invalid enum value, something is very wrong!!", e); 43 | } 44 | } 45 | 46 | @Override 47 | public long allocationSize({{ type_name }} value) { 48 | return 4L; 49 | } 50 | 51 | @Override 52 | public void write({{ type_name }} value, ByteBuffer buf) { 53 | buf.putInt(value.ordinal() + 1); 54 | } 55 | } 56 | 57 | {% else %} 58 | 59 | {%- call java::docstring(e, 0) %} 60 | public sealed interface {{ type_name }}{% if contains_object_references %} extends AutoCloseable {% endif %} { 61 | {% for variant in e.variants() -%} 62 | {%- call java::docstring(variant, 4) %} 63 | {% if !variant.has_fields() -%} 64 | record {{ variant|type_name(ci, config)}}() implements {{ type_name }} { 65 | {% if contains_object_references %} 66 | @Override 67 | public void close() { 68 | // Nothing to destroy 69 | } 70 | {% endif %} 71 | } 72 | {% else -%} 73 | record {{ variant|type_name(ci, config)}}( 74 | {%- for field in variant.fields() -%} 75 | {%- call java::docstring(field, 8) %} 76 | {{ field|type_name(ci, config)}} {% call java::field_name(field, loop.index) %}{% if loop.last %}{% else %}, {% endif %} 77 | {%- endfor -%} 78 | ) implements {{ type_name }} { 79 | {% if contains_object_references %} 80 | @Override 81 | public void close() { 82 | {% call java::destroy_fields(variant) %} 83 | } 84 | {% endif %} 85 | } 86 | {%- endif %} 87 | {% endfor %} 88 | } 89 | 90 | package {{ config.package_name() }}; 91 | 92 | import java.nio.ByteBuffer; 93 | 94 | public enum {{ e|ffi_converter_name}} implements FfiConverterRustBuffer<{{ type_name }}> { 95 | INSTANCE; 96 | 97 | @Override 98 | public {{ type_name }} read(ByteBuffer buf) { 99 | return switch (buf.getInt()) { 100 | {%- for variant in e.variants() %} 101 | case {{ loop.index }} -> new {{ type_name }}.{{variant|type_name(ci, config)}}( 102 | {%- if variant.has_fields() -%} 103 | {% for field in variant.fields() -%} 104 | {{ field|read_fn(config, ci) }}(buf){% if loop.last %}{% else %},{% endif %} 105 | {% endfor -%} 106 | {%- endif %}); 107 | {%- endfor %} 108 | default -> 109 | throw new RuntimeException("invalid enum value, something is very wrong!"); 110 | }; 111 | } 112 | 113 | @Override 114 | public long allocationSize({{ type_name }} value) { 115 | return switch (value) { 116 | {%- for variant in e.variants() %} 117 | case {{ type_name }}.{{ variant|type_name(ci, config) }}({%- for field in variant.fields() %}var {% call java::field_name(field, loop.index) -%}{% if !loop.last%}, {% endif %}{% endfor %}) -> 118 | (4L 119 | {%- for field in variant.fields() %} 120 | + {{ field|allocation_size_fn(config, ci) }}({%- call java::field_name(field, loop.index) -%}) 121 | {%- endfor %}); 122 | {%- endfor %} 123 | }; 124 | } 125 | 126 | @Override 127 | public void write({{ type_name }} value, ByteBuffer buf) { 128 | switch (value) { 129 | {%- for variant in e.variants() %} 130 | case {{ type_name }}.{{ variant|type_name(ci, config) }}({%- for field in variant.fields() %}var {% call java::field_name(field, loop.index) -%}{% if !loop.last%}, {% endif %}{% endfor %}) -> { 131 | buf.putInt({{ loop.index }}); 132 | {%- for field in variant.fields() %} 133 | {{ field|write_fn(config, ci) }}({%- call java::field_name(field, loop.index) -%}, buf); 134 | {%- endfor %} 135 | } 136 | {%- endfor %} 137 | }; 138 | } 139 | } 140 | 141 | {% endif %} 142 | -------------------------------------------------------------------------------- /src/templates/Types.java: -------------------------------------------------------------------------------- 1 | {%- import "macros.java" as java %} 2 | 3 | package {{ config.package_name() }}; 4 | 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.stream.Stream; 8 | 9 | public interface AutoCloseableHelper { 10 | static void close(Object... args) { 11 | Stream 12 | .of(args) 13 | .forEach(obj -> { 14 | // this is all to avoid the problem reported in uniffi-rs#2467 15 | if (obj instanceof AutoCloseable) { 16 | try { 17 | ((AutoCloseable) obj).close(); 18 | } catch (Exception e) { 19 | throw new RuntimeException(e); 20 | } 21 | } 22 | if (obj instanceof List) { 23 | for (int i = 0; i < ((List) obj).size(); i++) { 24 | Object element = ((List) obj).get(i); 25 | if (element instanceof AutoCloseable) { 26 | try { 27 | ((AutoCloseable) element).close(); 28 | } catch (Exception e) { 29 | throw new RuntimeException(e); 30 | } 31 | } 32 | } 33 | } 34 | if (obj instanceof Map) { 35 | for (var value : ((Map) obj).values()) { 36 | if (value instanceof AutoCloseable) { 37 | try { 38 | ((AutoCloseable) value).close(); 39 | } catch (Exception e) { 40 | throw new RuntimeException(e); 41 | } 42 | } 43 | } 44 | } 45 | if (obj instanceof Iterable) { 46 | for (var value : ((Iterable) obj)) { 47 | if (value instanceof AutoCloseable) { 48 | try { 49 | ((AutoCloseable) value).close(); 50 | } catch (Exception e) { 51 | throw new RuntimeException(e); 52 | } 53 | } 54 | } 55 | } 56 | }); 57 | } 58 | } 59 | package {{ config.package_name() }}; 60 | 61 | public class NoPointer { 62 | // Private constructor to prevent instantiation 63 | private NoPointer() {} 64 | 65 | // Static final instance of the class so it can be used in tests 66 | public static final NoPointer INSTANCE = new NoPointer(); 67 | } 68 | 69 | {%- for type_ in ci.iter_local_types() %} 70 | {%- let type_name = type_|type_name(ci, config) %} 71 | {%- let ffi_converter_name = type_|ffi_converter_name %} 72 | {%- let ffi_converter_instance = type_|ffi_converter_instance(config, ci) %} 73 | {%- let canonical_type_name = type_|canonical_name %} 74 | {%- let contains_object_references = ci.item_contains_object_references(type_) %} 75 | 76 | {# 77 | # Map `Type` instances to an include statement for that type. 78 | # 79 | # There is a companion match in `JavaCodeOracle::create_code_type()` which performs a similar function for the 80 | # Rust code. 81 | # 82 | # - When adding additional types here, make sure to also add a match arm to that function. 83 | # - To keep things manageable, let's try to limit ourselves to these 2 mega-matches 84 | #} 85 | {%- match type_ %} 86 | 87 | {%- when Type::Boolean %} 88 | {%- include "BooleanHelper.java" %} 89 | 90 | {%- when Type::Bytes %} 91 | {%- include "ByteArrayHelper.java" %} 92 | 93 | {%- when Type::CallbackInterface { module_path, name } %} 94 | {% include "CallbackInterfaceTemplate.java" %} 95 | 96 | {%- when Type::Custom { module_path, name, builtin } %} 97 | {%- if !ci.is_external(type_) %} 98 | {% include "CustomTypeTemplate.java" %} 99 | {%- endif %} 100 | 101 | {%- when Type::Duration %} 102 | {% include "DurationHelper.java" %} 103 | 104 | {%- when Type::Enum { name, module_path } %} 105 | {%- let e = ci.get_enum_definition(name).unwrap() %} 106 | {%- if !ci.is_name_used_as_error(name) %} 107 | {% include "EnumTemplate.java" %} 108 | {%- else %} 109 | {% include "ErrorTemplate.java" %} 110 | {%- endif -%} 111 | 112 | {%- when Type::Int64 or Type::UInt64 %} 113 | {%- include "Int64Helper.java" %} 114 | 115 | {%- when Type::Int8 or Type::UInt8 %} 116 | {%- include "Int8Helper.java" %} 117 | 118 | {%- when Type::Int16 or Type::UInt16 %} 119 | {%- include "Int16Helper.java" %} 120 | 121 | {%- when Type::Int32 or Type::UInt32 %} 122 | {%- include "Int32Helper.java" %} 123 | 124 | {%- when Type::Float32 %} 125 | {%- include "Float32Helper.java" %} 126 | 127 | {%- when Type::Float64 %} 128 | {%- include "Float64Helper.java" %} 129 | 130 | {%- when Type::Map { key_type, value_type } %} 131 | {% include "MapTemplate.java" %} 132 | 133 | {%- when Type::Optional { inner_type } %} 134 | {% include "OptionalTemplate.java" %} 135 | 136 | {%- when Type::Object { module_path, name, imp } %} 137 | {% include "ObjectTemplate.java" %} 138 | 139 | {%- when Type::Record { name, module_path } %} 140 | {% include "RecordTemplate.java" %} 141 | 142 | {%- when Type::Sequence { inner_type } %} 143 | {% include "SequenceTemplate.java" %} 144 | 145 | {%- when Type::String %} 146 | {%- include "StringHelper.java" %} 147 | 148 | {%- when Type::Timestamp %} 149 | {% include "TimestampHelper.java" %} 150 | 151 | {%- else %} 152 | {%- endmatch %} 153 | {%- endfor %} 154 | -------------------------------------------------------------------------------- /src/templates/ErrorTemplate.java: -------------------------------------------------------------------------------- 1 | package {{ config.package_name() }}; 2 | 3 | {%- let type_name = type_|type_name(ci, config) %} 4 | {%- let ffi_converter_instance = type_|ffi_converter_instance(config, ci) %} 5 | {%- let canonical_type_name = type_|canonical_name %} 6 | 7 | {% if e.is_flat() %} 8 | {%- call java::docstring(e, 0) %} 9 | public class {{ type_name }} extends Exception { 10 | private {{ type_name }}(String message) { 11 | super(message); 12 | } 13 | 14 | {% for variant in e.variants() -%} 15 | {%- call java::docstring(variant, 4) %} 16 | public static class {{ variant|error_variant_name }} extends {{ type_name }}{% if contains_object_references %}, AutoCloseable{% endif %} { 17 | public {{ variant|error_variant_name }}(String message) { 18 | super(message); 19 | } 20 | } 21 | {% endfor %} 22 | } 23 | 24 | 25 | {%- else %} 26 | {%- call java::docstring(e, 0) %} 27 | public class {{ type_name }} extends Exception { 28 | private {{ type_name }}(String message) { 29 | super(message); 30 | } 31 | 32 | {% for variant in e.variants() -%} 33 | {%- call java::docstring(variant, 4) %} 34 | {%- let variant_name = variant|error_variant_name %} 35 | public static class {{ variant_name }} extends {{ type_name }}{% if contains_object_references %}, AutoCloseable{% endif %} { 36 | {% for field in variant.fields() -%} 37 | {%- call java::docstring(field, 8) %} 38 | {{ field|type_name(ci, config) }} {% call java::field_name(field, loop.index) %}; 39 | {% endfor -%} 40 | 41 | public {{ variant_name }}( 42 | {%- for field in variant.fields() -%} 43 | {{ field|type_name(ci, config)}} {% call java::field_name(field, loop.index) %}{% if loop.last %}{% else %}, {% endif %} 44 | {%- endfor -%} 45 | ) { 46 | super(new StringBuilder() 47 | {%- for field in variant.fields() %} 48 | .append("{% call java::field_name_unquoted(field, loop.index) %}=") 49 | .append({% call java::field_name(field, loop.index) %}) 50 | {% if !loop.last %} 51 | .append(", ") 52 | {% endif %} 53 | {% endfor %} 54 | .toString()); 55 | {% for field in variant.fields() -%} 56 | this.{% call java::field_name(field, loop.index) %} = {% call java::field_name(field, loop.index) %}; 57 | {% endfor -%} 58 | } 59 | 60 | {% for field in variant.fields() -%} 61 | public {{ field|type_name(ci, config) }} {% call java::field_name(field, loop.index) %}() { 62 | return this.{% call java::field_name(field, loop.index) %}; 63 | } 64 | {% endfor %} 65 | 66 | {% if contains_object_references %} 67 | @Override 68 | void close() { 69 | {%- if variant.has_fields() %} 70 | {% call java::destroy_fields(variant) %} 71 | {% else -%} 72 | // Nothing to destroy 73 | {%- endif %} 74 | } 75 | {% endif %} 76 | } 77 | {% endfor %} 78 | } 79 | {%- endif %} 80 | 81 | package {{ config.package_name() }}; 82 | 83 | public class {{ type_name }}ErrorHandler implements UniffiRustCallStatusErrorHandler<{{ type_name }}> { 84 | @Override 85 | public {{ type_name }} lift(RustBuffer.ByValue errorBuf){ 86 | return {{ ffi_converter_instance }}.lift(errorBuf); 87 | } 88 | } 89 | 90 | package {{ config.package_name() }}; 91 | 92 | import java.nio.ByteBuffer; 93 | 94 | public enum {{ e|ffi_converter_name }} implements FfiConverterRustBuffer<{{ type_name }}> { 95 | INSTANCE; 96 | 97 | @Override 98 | public {{ type_name }} read(ByteBuffer buf) { 99 | {%- if e.is_flat() %} 100 | return switch(buf.getInt()) { 101 | {%- for variant in e.variants() %} 102 | case {{ loop.index }} -> new {{ type_name }}.{{ variant|error_variant_name }}({{ Type::String.borrow()|read_fn(config, ci) }}(buf)); 103 | {%- endfor %} 104 | default -> throw new RuntimeException("invalid error enum value, something is very wrong!!"); 105 | }; 106 | {%- else %} 107 | 108 | return switch(buf.getInt()) { 109 | {%- for variant in e.variants() %} 110 | case {{ loop.index }} -> new {{ type_name }}.{{ variant|error_variant_name }}({% if variant.has_fields() %} 111 | {% for field in variant.fields() -%} 112 | {{ field|read_fn(config, ci) }}(buf){% if loop.last %}{% else %},{% endif %} 113 | {% endfor -%} 114 | {%- endif -%}); 115 | {%- endfor %} 116 | default -> throw new RuntimeException("invalid error enum value, something is very wrong!!"); 117 | }; 118 | {%- endif %} 119 | } 120 | 121 | @Override 122 | public long allocationSize({{ type_name }} value) { 123 | {%- if e.is_flat() %} 124 | return 4L; 125 | {%- else %} 126 | return switch(value) { 127 | {%- for variant in e.variants() %} 128 | case {{ type_name }}.{{ variant|error_variant_name }} x -> ( 129 | // Add the size for the Int that specifies the variant plus the size needed for all fields 130 | 4L 131 | {%- for field in variant.fields() %} 132 | + {{ field|allocation_size_fn(config, ci) }}(x.{% call java::field_name(field, loop.index) %}) 133 | {%- endfor %} 134 | ); 135 | {%- endfor %} 136 | default -> throw new RuntimeException("invalid error enum value, something is very wrong!!"); 137 | }; 138 | {%- endif %} 139 | } 140 | 141 | @Override 142 | public void write({{ type_name }} value, ByteBuffer buf) { 143 | switch(value) { 144 | {%- for variant in e.variants() %} 145 | case {{ type_name }}.{{ variant|error_variant_name }} x -> { 146 | buf.putInt({{ loop.index }}); 147 | {%- for field in variant.fields() %} 148 | {{ field|write_fn(config, ci) }}(x.{% call java::field_name(field, loop.index) %}, buf); 149 | {%- endfor %} 150 | } 151 | {%- endfor %} 152 | default -> throw new RuntimeException("invalid error enum value, something is very wrong!!"); 153 | }; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/templates/NamespaceLibraryTemplate.java: -------------------------------------------------------------------------------- 1 | package {{ config.package_name() }}; 2 | 3 | import com.sun.jna.Library; 4 | import com.sun.jna.Native; 5 | 6 | final class NamespaceLibrary { 7 | static synchronized String findLibraryName(String componentName) { 8 | String libOverride = System.getProperty("uniffi.component." + componentName + ".libraryOverride"); 9 | if (libOverride != null) { 10 | return libOverride; 11 | } 12 | return "{{ config.cdylib_name() }}"; 13 | } 14 | 15 | static Lib loadIndirect(String componentName, Class clazz) { 16 | return Native.load(findLibraryName(componentName), clazz); 17 | } 18 | 19 | static void uniffiCheckContractApiVersion(UniffiLib lib) { 20 | // Get the bindings contract version from our ComponentInterface 21 | int bindingsContractVersion = {{ ci.uniffi_contract_version() }}; 22 | // Get the scaffolding contract version by calling the into the dylib 23 | int scaffoldingContractVersion = lib.{{ ci.ffi_uniffi_contract_version().name() }}(); 24 | if (bindingsContractVersion != scaffoldingContractVersion) { 25 | throw new RuntimeException("UniFFI contract version mismatch: try cleaning and rebuilding your project"); 26 | } 27 | } 28 | 29 | static void uniffiCheckApiChecksums(UniffiLib lib) { 30 | {%- for (name, expected_checksum) in ci.iter_checksums() %} 31 | if (lib.{{ name }}() != ((short) {{ expected_checksum }})) { 32 | throw new RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project"); 33 | } 34 | {%- endfor %} 35 | } 36 | } 37 | 38 | // Define FFI callback types 39 | {%- for def in ci.ffi_definitions() %} 40 | {%- match def %} 41 | {%- when FfiDefinition::CallbackFunction(callback) %} 42 | package {{ config.package_name() }}; 43 | 44 | import com.sun.jna.*; 45 | import com.sun.jna.ptr.*; 46 | 47 | interface {{ callback.name()|ffi_callback_name }} extends Callback { 48 | public {% match callback.return_type() %}{%- when Some(return_type) %}{{ return_type|ffi_type_name_for_ffi_struct(config, ci) }}{%- when None %}void{%- endmatch %} callback( 49 | {%- for arg in callback.arguments() -%} 50 | {{ arg.type_().borrow()|ffi_type_name_for_ffi_struct(config, ci) }} {{ arg.name().borrow()|var_name }}{% if !loop.last %},{% endif %} 51 | {%- endfor -%} 52 | {%- if callback.has_rust_call_status_arg() -%}{% if callback.arguments().len() != 0 %},{% endif %} 53 | UniffiRustCallStatus uniffiCallStatus 54 | {%- endif -%} 55 | ); 56 | } 57 | {%- when FfiDefinition::Struct(ffi_struct) %} 58 | package {{ config.package_name() }}; 59 | 60 | import com.sun.jna.Structure; 61 | import com.sun.jna.Pointer; 62 | 63 | @Structure.FieldOrder({ {% for field in ffi_struct.fields() %}"{{ field.name()|var_name_raw }}"{% if !loop.last %}, {% endif %}{% endfor %} }) 64 | public class {{ ffi_struct.name()|ffi_struct_name }} extends Structure { 65 | {%- for field in ffi_struct.fields() %} 66 | public {{ field.type_().borrow()|ffi_type_name_for_ffi_struct(config, ci) }} {{ field.name()|var_name }} = {{ field.type_()|ffi_default_value }}; 67 | {%- endfor %} 68 | 69 | // no-arg constructor required so JNA can instantiate and reflect 70 | public {{ ffi_struct.name()|ffi_struct_name}}() { 71 | super(); 72 | } 73 | 74 | public {{ ffi_struct.name()|ffi_struct_name }}( 75 | {%- for field in ffi_struct.fields() %} 76 | {{ field.type_().borrow()|ffi_type_name_for_ffi_struct(config, ci) }} {{ field.name()|var_name }}{% if !loop.last %},{% endif %} 77 | {%- endfor %} 78 | ) { 79 | {%- for field in ffi_struct.fields() %} 80 | this.{{ field.name()|var_name }} = {{ field.name()|var_name }}; 81 | {%- endfor %} 82 | } 83 | 84 | public static class UniffiByValue extends {{ ffi_struct.name()|ffi_struct_name }} implements Structure.ByValue { 85 | public UniffiByValue( 86 | {%- for field in ffi_struct.fields() %} 87 | {{ field.type_().borrow()|ffi_type_name_for_ffi_struct(config, ci) }} {{ field.name()|var_name }}{% if !loop.last %},{% endif %} 88 | {%- endfor %} 89 | ) { 90 | super({%- for field in ffi_struct.fields() -%} 91 | {{ field.name()|var_name }}{% if !loop.last %},{% endif %} 92 | {% endfor %}); 93 | } 94 | } 95 | 96 | void uniffiSetValue({{ ffi_struct.name()|ffi_struct_name }} other) { 97 | {%- for field in ffi_struct.fields() %} 98 | {{ field.name()|var_name }} = other.{{ field.name()|var_name }}; 99 | {%- endfor %} 100 | } 101 | 102 | } 103 | {%- when FfiDefinition::Function(_) %} 104 | {# functions are handled below #} 105 | {%- endmatch %} 106 | {%- endfor %} 107 | 108 | package {{ config.package_name() }}; 109 | 110 | import com.sun.jna.Library; 111 | import com.sun.jna.Pointer; 112 | 113 | // A JNA Library to expose the extern-C FFI definitions. 114 | // This is an implementation detail which will be called internally by the public API. 115 | interface UniffiLib extends Library { 116 | UniffiLib INSTANCE = UniffiLibInitializer.load(); 117 | 118 | {% if ci.contains_object_types() %} 119 | // The Cleaner for the whole library 120 | static UniffiCleaner CLEANER = UniffiCleaner.create(); 121 | {%- endif %} 122 | 123 | {% for func in ci.iter_ffi_function_definitions() -%} 124 | {% match func.return_type() %}{% when Some with (return_type) %}{{ return_type.borrow()|ffi_type_name_by_value(config, ci) }}{% when None %}void{% endmatch %} {{ func.name() }}({%- call java::arg_list_ffi_decl(func) %}); 125 | {% endfor %} 126 | } 127 | 128 | package {{ config.package_name() }}; 129 | 130 | // Java doesn't allow for static init blocks in an interface outside of a static property with a default. 131 | // To get around that and make sure that when the UniffiLib interface loads it has an initialized library 132 | // we call this class. The init code won't be called until a function on this interface is called unfortunately. 133 | final class UniffiLibInitializer { 134 | static UniffiLib load() { 135 | UniffiLib instance = NamespaceLibrary.loadIndirect("{{ ci.namespace() }}", UniffiLib.class); 136 | NamespaceLibrary.uniffiCheckContractApiVersion(instance); 137 | NamespaceLibrary.uniffiCheckApiChecksums(instance); 138 | {% for init_fn in self.initialization_fns() -%} 139 | {{ init_fn }}(instance); 140 | {% endfor -%} 141 | return instance; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/templates/Helpers.java: -------------------------------------------------------------------------------- 1 | package {{ config.package_name() }}; 2 | 3 | import com.sun.jna.Structure; 4 | import com.sun.jna.Pointer; 5 | 6 | @Structure.FieldOrder({ "code", "error_buf" }) 7 | public class UniffiRustCallStatus extends Structure { 8 | public byte code; 9 | public RustBuffer.ByValue error_buf; 10 | 11 | public static class ByValue extends UniffiRustCallStatus implements Structure.ByValue {} 12 | 13 | public boolean isSuccess() { 14 | return code == UNIFFI_CALL_SUCCESS; 15 | } 16 | 17 | public boolean isError() { 18 | return code == UNIFFI_CALL_ERROR; 19 | } 20 | 21 | public boolean isPanic() { 22 | return code == UNIFFI_CALL_UNEXPECTED_ERROR; 23 | } 24 | 25 | public void setCode(byte code) { 26 | this.code = code; 27 | } 28 | 29 | public void setErrorBuf(RustBuffer.ByValue errorBuf) { 30 | this.error_buf = errorBuf; 31 | } 32 | 33 | public static UniffiRustCallStatus.ByValue create(byte code, RustBuffer.ByValue errorBuf) { 34 | UniffiRustCallStatus.ByValue callStatus = new UniffiRustCallStatus.ByValue(); 35 | callStatus.code = code; 36 | callStatus.error_buf = errorBuf; 37 | return callStatus; 38 | } 39 | 40 | public static final byte UNIFFI_CALL_SUCCESS = 0; 41 | public static final byte UNIFFI_CALL_ERROR = 1; 42 | public static final byte UNIFFI_CALL_UNEXPECTED_ERROR = 2; 43 | } 44 | 45 | package {{ config.package_name() }}; 46 | 47 | public class InternalException extends RuntimeException { 48 | public InternalException(String message) { 49 | super(message); 50 | } 51 | } 52 | 53 | package {{ config.package_name() }}; 54 | 55 | public interface UniffiRustCallStatusErrorHandler { 56 | E lift(RustBuffer.ByValue errorBuf); 57 | } 58 | 59 | package {{ config.package_name() }}; 60 | 61 | // UniffiRustCallStatusErrorHandler implementation for times when we don't expect a CALL_ERROR 62 | class UniffiNullRustCallStatusErrorHandler implements UniffiRustCallStatusErrorHandler { 63 | @Override 64 | public InternalException lift(RustBuffer.ByValue errorBuf) { 65 | RustBuffer.free(errorBuf); 66 | return new InternalException("Unexpected CALL_ERROR"); 67 | } 68 | } 69 | 70 | package {{ config.package_name() }}; 71 | 72 | import java.util.function.Function; 73 | import java.util.function.Consumer; 74 | import java.util.function.Supplier; 75 | import java.util.concurrent.Callable; 76 | 77 | // Helpers for calling Rust 78 | // In practice we usually need to be synchronized to call this safely, so it doesn't 79 | // synchronize itself 80 | public final class UniffiHelpers { 81 | // Call a rust function that returns a Result<>. Pass in the Error class companion that corresponds to the Err 82 | static U uniffiRustCallWithError(UniffiRustCallStatusErrorHandler errorHandler, Function callback) throws E { 83 | UniffiRustCallStatus status = new UniffiRustCallStatus(); 84 | U returnValue = callback.apply(status); 85 | uniffiCheckCallStatus(errorHandler, status); 86 | return returnValue; 87 | } 88 | 89 | // Overload to call a rust function that returns a Result<()>, because void is outside Java's type system. Pass in the Error class companion that corresponds to the Err 90 | static void uniffiRustCallWithError(UniffiRustCallStatusErrorHandler errorHandler, Consumer callback) throws E { 91 | UniffiRustCallStatus status = new UniffiRustCallStatus(); 92 | callback.accept(status); 93 | uniffiCheckCallStatus(errorHandler, status); 94 | } 95 | 96 | // Check UniffiRustCallStatus and throw an error if the call wasn't successful 97 | static void uniffiCheckCallStatus(UniffiRustCallStatusErrorHandler errorHandler, UniffiRustCallStatus status) throws E { 98 | if (status.isSuccess()) { 99 | return; 100 | } else if (status.isError()) { 101 | throw errorHandler.lift(status.error_buf); 102 | } else if (status.isPanic()) { 103 | // when the rust code sees a panic, it tries to construct a rustbuffer 104 | // with the message. but if that code panics, then it just sends back 105 | // an empty buffer. 106 | if (status.error_buf.len > 0) { 107 | throw new InternalException({{ Type::String.borrow()|lift_fn(config, ci) }}(status.error_buf)); 108 | } else { 109 | throw new InternalException("Rust panic"); 110 | } 111 | } else { 112 | throw new InternalException("Unknown rust call status: " + status.code); 113 | } 114 | } 115 | 116 | // Call a rust function that returns a plain value 117 | static U uniffiRustCall(Function callback) { 118 | return uniffiRustCallWithError(new UniffiNullRustCallStatusErrorHandler(), callback); 119 | } 120 | 121 | // Call a rust function that returns nothing 122 | static void uniffiRustCall(Consumer callback) { 123 | uniffiRustCallWithError(new UniffiNullRustCallStatusErrorHandler(), callback); 124 | } 125 | 126 | static void uniffiTraitInterfaceCall( 127 | UniffiRustCallStatus callStatus, 128 | Supplier makeCall, 129 | Consumer writeReturn 130 | ) { 131 | try { 132 | writeReturn.accept(makeCall.get()); 133 | } catch (Exception e) { 134 | callStatus.setCode(UniffiRustCallStatus.UNIFFI_CALL_UNEXPECTED_ERROR); 135 | callStatus.setErrorBuf({{ Type::String.borrow()|lower_fn(config, ci) }}(e.toString())); 136 | } 137 | } 138 | 139 | static void uniffiTraitInterfaceCallWithError( 140 | UniffiRustCallStatus callStatus, 141 | Callable makeCall, 142 | Consumer writeReturn, 143 | Function lowerError, 144 | Class errorClazz 145 | ) { 146 | try { 147 | writeReturn.accept(makeCall.call()); 148 | } catch (Exception e) { 149 | if (errorClazz.isAssignableFrom(e.getClass())) { 150 | @SuppressWarnings("unchecked") 151 | E castedE = (E) e; 152 | callStatus.setCode(UniffiRustCallStatus.UNIFFI_CALL_ERROR); 153 | callStatus.setErrorBuf(lowerError.apply(castedE)); 154 | } else { 155 | callStatus.setCode(UniffiRustCallStatus.UNIFFI_CALL_UNEXPECTED_ERROR); 156 | callStatus.setErrorBuf({{ Type::String.borrow()|lower_fn(config, ci) }}(e.toString())); 157 | } 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/templates/macros.java: -------------------------------------------------------------------------------- 1 | {# 2 | // Template to call into rust. Used in several places. 3 | // Variable names in `arg_list` should match up with arg lists 4 | // passed to rust via `arg_list_lowered` 5 | #} 6 | 7 | {%- macro to_ffi_call(func) -%} 8 | {%- if func.takes_self() %} 9 | callWithPointer(it -> { 10 | try { 11 | {% if func.return_type().is_some() %} 12 | return {%- call to_raw_ffi_call(func) %}; 13 | {% else %} 14 | {%- call to_raw_ffi_call(func) %}; 15 | {% endif %} 16 | } catch (Exception e) { 17 | throw new RuntimeException(e); 18 | } 19 | }) 20 | {% else %} 21 | {%- call to_raw_ffi_call(func) %} 22 | {% endif %} 23 | {%- endmacro %} 24 | 25 | {%- macro to_raw_ffi_call(func) -%} 26 | {%- match func.throws_type() %} 27 | {%- when Some(e) %} 28 | UniffiHelpers.uniffiRustCallWithError(new {{ e|type_name(ci, config) }}ErrorHandler(), 29 | {%- else %} 30 | UniffiHelpers.uniffiRustCall( 31 | {%- endmatch %} _status -> { 32 | {% if func.return_type().is_some() %}return {% endif %}UniffiLib.INSTANCE.{{ func.ffi_func().name() }}( 33 | {% if func.takes_self() %}it, {% endif -%} 34 | {% if func.arguments().len() != 0 %}{% call arg_list_lowered(func) -%}, {% endif -%} 35 | _status); 36 | }) 37 | {%- endmacro -%} 38 | 39 | {%- macro func_decl(func_decl, annotation, callable, indent) %} 40 | {%- if self::can_render_callable(callable, ci) %} 41 | {%- call docstring(callable, indent) %} 42 | {%- if annotation != "" %} 43 | @{{ annotation }} 44 | {% endif %} 45 | {%- if callable.is_async() %} 46 | {{ func_decl }} CompletableFuture<{% match callable.return_type() -%}{%- when Some with (return_type) -%}{{ return_type|type_name(ci, config) }}{%- when None %}Void{%- endmatch %}> {{ callable.name()|fn_name }}( 47 | {%- call arg_list(callable, !callable.takes_self()) -%} 48 | ){ 49 | return {% call call_async(callable) %}; 50 | } 51 | {%- else -%} 52 | {{ func_decl }} {% match callable.return_type() -%}{%- when Some with (return_type) -%}{{ return_type|type_name(ci, config) }}{%- when None %}void{%- endmatch %} {{ callable.name()|fn_name }}( 53 | {%- call arg_list(callable, !callable.takes_self()) -%} 54 | ) {% match callable.throws_type() -%} 55 | {%- when Some(throwable) -%} 56 | throws {{ throwable|type_name(ci, config) }} 57 | {%- else -%} 58 | {%- endmatch %} { 59 | try { 60 | {% match callable.return_type() -%}{%- when Some with (return_type) -%}return {{ return_type|lift_fn(config, ci) }}({% call to_ffi_call(callable) %}){%- when None %}{% call to_ffi_call(callable) %}{%- endmatch %}; 61 | } catch (RuntimeException _e) { 62 | {% match callable.throws_type() %} 63 | {% when Some(throwable) %} 64 | if ({{ throwable|type_name(ci, config) }}.class.isInstance(_e.getCause())) { 65 | throw ({{ throwable|type_name(ci, config) }})_e.getCause(); 66 | } 67 | {% else %} 68 | {% endmatch %} 69 | if (InternalException.class.isInstance(_e.getCause())) { 70 | throw (InternalException)_e.getCause(); 71 | } 72 | throw _e; 73 | } 74 | } 75 | {% endif %} 76 | {%- else %} 77 | // Sorry, the callable "{{ callable.name() }}" isn't supported. 78 | {%- endif %} 79 | {% endmacro %} 80 | 81 | {%- macro call_async(callable) -%} 82 | UniffiAsyncHelpers.uniffiRustCallAsync( 83 | {%- if callable.takes_self() %} 84 | callWithPointer(thisPtr -> { 85 | return UniffiLib.INSTANCE.{{ callable.ffi_func().name() }}( 86 | thisPtr{% if callable.arguments().len() != 0 %},{% endif %} 87 | {% call arg_list_lowered(callable) %} 88 | ); 89 | }), 90 | {%- else %} 91 | UniffiLib.INSTANCE.{{ callable.ffi_func().name() }}({% call arg_list_lowered(callable) %}), 92 | {%- endif %} 93 | {{ callable|async_poll(ci) }}, 94 | {{ callable|async_complete(ci, config) }}, 95 | {{ callable|async_free(ci) }}, 96 | // lift function 97 | {%- match callable.return_type() %} 98 | {%- when Some(return_type) %} 99 | (it) -> {{ return_type|lift_fn(config, ci) }}(it), 100 | {%- when None %} 101 | () -> {}, 102 | {%- endmatch %} 103 | // Error FFI converter 104 | {%- match callable.throws_type() %} 105 | {%- when Some(e) %} 106 | new {{ e|type_name(ci, config) }}ErrorHandler() 107 | {%- when None %} 108 | new UniffiNullRustCallStatusErrorHandler() 109 | {%- endmatch %} 110 | ) 111 | {%- endmacro %} 112 | 113 | {%- macro arg_list_lowered(func) %} 114 | {%- for arg in func.arguments() %} 115 | {{- arg|lower_fn(config, ci) }}({{ arg.name()|var_name }}) 116 | {%- if !loop.last %}, {% endif -%} 117 | {%- endfor %} 118 | {%- endmacro -%} 119 | 120 | {#- 121 | // Arglist as used in Java declarations of methods, functions and constructors. 122 | // even if `is_decl` there won't be default values, Java doesn't support them in any reasonable way. 123 | // Note the var_name and type_name filters. 124 | -#} 125 | 126 | {% macro arg_list(func, is_decl) %} 127 | {%- for arg in func.arguments() -%} 128 | {{ arg|type_name(ci, config) }} {{ arg.name()|var_name }} 129 | {%- if !loop.last %}, {% endif -%} 130 | {%- endfor %} 131 | {%- endmacro %} 132 | 133 | {#- 134 | // Arglist as used in the UniffiLib function declarations. 135 | // Note unfiltered name but ffi_type_name filters. 136 | -#} 137 | {%- macro arg_list_ffi_decl(func) %} 138 | {%- for arg in func.arguments() %} 139 | {{- arg.type_().borrow()|ffi_type_name_by_value(config, ci) }} {{arg.name()|var_name -}}{%- if !loop.last %}, {% endif -%} 140 | {%- endfor %} 141 | {%- if func.has_rust_call_status_arg() %}{% if func.arguments().len() != 0 %}, {% endif %}UniffiRustCallStatus uniffi_out_errmk{% endif %} 142 | {%- endmacro -%} 143 | 144 | {% macro field_name(field, field_num) %} 145 | {%- if field.name().is_empty() -%} 146 | v{{- field_num -}} 147 | {%- else -%} 148 | {{ field.name()|var_name }} 149 | {%- endif -%} 150 | {%- endmacro %} 151 | 152 | {% macro field_name_unquoted(field, field_num) %} 153 | {%- if field.name().is_empty() -%} 154 | v{{- field_num -}} 155 | {%- else -%} 156 | {{ field.name()|var_name|unquote }} 157 | {%- endif -%} 158 | {%- endmacro %} 159 | 160 | // Macro for destroying fields 161 | {%- macro destroy_fields(member) %} 162 | AutoCloseableHelper.close( 163 | {%- for field in member.fields() %} 164 | this.{{ field.name()|var_name }}{%- if !loop.last %}, {% endif -%} 165 | {% endfor -%}); 166 | {%- endmacro -%} 167 | 168 | {%- macro docstring_value(maybe_docstring, indent_spaces) %} 169 | {%- match maybe_docstring %} 170 | {%- when Some(docstring) %} 171 | {{ docstring|docstring(indent_spaces) }} 172 | {%- else %} 173 | {%- endmatch %} 174 | {%- endmacro %} 175 | 176 | {%- macro docstring(defn, indent_spaces) %} 177 | {%- call docstring_value(defn.docstring(), indent_spaces) %} 178 | {%- endmacro %} 179 | -------------------------------------------------------------------------------- /src/templates/CallbackInterfaceImpl.java: -------------------------------------------------------------------------------- 1 | {% if self.include_once_check("CallbackInterfaceRuntime.java") %}{% include "CallbackInterfaceRuntime.java" %}{% endif %} 2 | 3 | package {{ config.package_name() }}; 4 | 5 | import java.util.concurrent.CompletableFuture; 6 | import java.util.concurrent.Callable; 7 | import java.util.function.Function; 8 | import java.util.function.Consumer; 9 | import java.util.function.Supplier; 10 | import java.util.List; 11 | import com.sun.jna.*; 12 | import com.sun.jna.ptr.*; 13 | 14 | {%- let trait_impl=format!("UniffiCallbackInterface{}", name) %} 15 | 16 | // Put the implementation in an object so we don't pollute the top-level namespace 17 | public class {{ trait_impl }} { 18 | public static final {{ trait_impl }} INSTANCE = new {{ trait_impl }}(); 19 | {{ vtable|ffi_type_name_by_value(config, ci) }} vtable; 20 | 21 | {{ trait_impl }}() { 22 | vtable = new {{ vtable|ffi_type_name_by_value(config, ci) }}( 23 | {%- for (ffi_callback, meth) in vtable_methods.iter() %} 24 | {{ meth.name()|var_name }}.INSTANCE, 25 | {%- endfor %} 26 | UniffiFree.INSTANCE 27 | ); 28 | } 29 | 30 | // Registers the foreign callback with the Rust side. 31 | // This method is generated for each callback interface. 32 | void register(UniffiLib lib) { 33 | lib.{{ ffi_init_callback.name() }}(vtable); 34 | } 35 | 36 | {%- for (ffi_callback, meth) in vtable_methods.iter() %} 37 | {% let inner_method_class = meth.name()|var_name %} 38 | public static class {{ inner_method_class }} implements {{ ffi_callback.name()|ffi_callback_name }} { 39 | public static final {{ inner_method_class }} INSTANCE = new {{ inner_method_class }}(); 40 | private {{ inner_method_class }}() {} 41 | 42 | @Override 43 | public {% match ffi_callback.return_type() %}{% when Some(return_type) %}{{ return_type|ffi_type_name_for_ffi_struct(config, ci) }}{% when None %}void{% endmatch %} callback( 44 | {%- for arg in ffi_callback.arguments() -%} 45 | {{ arg.type_().borrow()|ffi_type_name_for_ffi_struct(config, ci) }} {{ arg.name().borrow()|var_name }}{% if !loop.last || (loop.last && ffi_callback.has_rust_call_status_arg()) %},{% endif %} 46 | {%- endfor -%} 47 | {%- if ffi_callback.has_rust_call_status_arg() -%} 48 | UniffiRustCallStatus uniffiCallStatus 49 | {%- endif -%} 50 | ) { 51 | var uniffiObj = {{ ffi_converter_name }}.INSTANCE.handleMap.get(uniffiHandle); 52 | {% if !meth.is_async() && meth.throws_type().is_some() %}Callable{% else %}Supplier{%endif%}<{% if meth.is_async() %}{{ meth|async_return_type(ci, config) }}{% else %}{% match meth.return_type() %}{% when Some(return_type)%}{{ return_type|type_name(ci, config)}}{% when None %}Void{% endmatch %}{% endif %}> makeCall = () -> { 53 | {% if meth.return_type().is_some() || meth.is_async() %}return {% endif %}uniffiObj.{{ meth.name()|fn_name() }}( 54 | {%- for arg in meth.arguments() %} 55 | {{ arg|lift_fn(config, ci) }}({{ arg.name()|var_name }}){% if !loop.last %},{% endif %} 56 | {%- endfor %} 57 | ); 58 | {% if meth.return_type().is_none() && !meth.is_async() %}return null;{% endif %} 59 | }; 60 | {%- if !meth.is_async() %} 61 | {%- match meth.return_type() %} 62 | {%- when Some(return_type) %} 63 | Consumer<{{ return_type|type_name(ci, config)}}> writeReturn = ({{ return_type|type_name(ci, config) }} value) -> { uniffiOutReturn.setValue({{ return_type|lower_fn(config, ci) }}(value)); }; 64 | {%- when None %} 65 | Consumer writeReturn = (nothing) -> {}; 66 | {%- endmatch %} 67 | 68 | {%- match meth.throws_type() %} 69 | {%- when None %} 70 | UniffiHelpers.uniffiTraitInterfaceCall(uniffiCallStatus, makeCall, writeReturn); 71 | {%- when Some(error_type) %} 72 | UniffiHelpers.uniffiTraitInterfaceCallWithError( 73 | uniffiCallStatus, 74 | makeCall, 75 | writeReturn, 76 | ({{error_type|type_name(ci, config) }} e) -> { return {{ error_type|lower_fn(config, ci) }}(e); }, 77 | {{error_type|type_name(ci, config)}}.class 78 | ); 79 | {%- endmatch %} 80 | 81 | {%- else %} 82 | Consumer<{{ meth|async_inner_return_type(ci, config) }}> uniffiHandleSuccess = ({% match meth.return_type() %}{%- when Some(return_type) %}returnValue{%- when None %}nothing{% endmatch %}) -> { 83 | var uniffiResult = new {{ meth.foreign_future_ffi_result_struct().name()|ffi_struct_name }}.UniffiByValue( 84 | {%- match meth.return_type() %} 85 | {%- when Some(return_type) %} 86 | {{ return_type|lower_fn(config, ci) }}(returnValue), 87 | {%- when None %} 88 | {%- endmatch %} 89 | new UniffiRustCallStatus.ByValue() 90 | ); 91 | uniffiResult.write(); 92 | uniffiFutureCallback.callback(uniffiCallbackData, uniffiResult); 93 | }; 94 | Consumer uniffiHandleError = (callStatus) -> { 95 | uniffiFutureCallback.callback( 96 | uniffiCallbackData, 97 | new {{ meth.foreign_future_ffi_result_struct().name()|ffi_struct_name }}.UniffiByValue( 98 | {%- match meth.return_type() %} 99 | {%- when Some(return_type) %} 100 | {{ return_type.into()|ffi_default_value }}, 101 | {%- when None %} 102 | {%- endmatch %} 103 | callStatus 104 | ) 105 | ); 106 | }; 107 | 108 | uniffiOutReturn.uniffiSetValue( 109 | {%- match meth.throws_type() %} 110 | {%- when None %} 111 | UniffiAsyncHelpers.uniffiTraitInterfaceCallAsync( 112 | makeCall, 113 | uniffiHandleSuccess, 114 | uniffiHandleError 115 | ) 116 | {%- when Some(error_type) %} 117 | UniffiAsyncHelpers.uniffiTraitInterfaceCallAsyncWithError( 118 | makeCall, 119 | uniffiHandleSuccess, 120 | uniffiHandleError, 121 | ({{error_type|type_name(ci, config) }} e) -> {{ error_type|lower_fn(config, ci) }}(e), 122 | {{ error_type|type_name(ci, config)}}.class 123 | ) 124 | {%- endmatch %} 125 | ); 126 | {%- endif %} 127 | } 128 | } 129 | {%- endfor %} 130 | 131 | public static class UniffiFree implements {{ "CallbackInterfaceFree"|ffi_callback_name }} { 132 | public static final UniffiFree INSTANCE = new UniffiFree(); 133 | 134 | private UniffiFree() {} 135 | 136 | @Override 137 | public void callback(long handle) { 138 | {{ ffi_converter_name }}.INSTANCE.handleMap.remove(handle); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # uniffi-bindgen-java 2 | 3 | Generate [UniFFI](https://github.com/mozilla/uniffi-rs) bindings for Java. 4 | 5 | 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`. 6 | 7 | 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. 8 | 9 | ## Requirements 10 | 11 | * Java 20+: `javac`, and `jar` 12 | * The [Java Native Access](https://github.com/java-native-access/jna#download) JAR downloaded and its path added to your `$CLASSPATH` environment variable. 13 | 14 | ## Installation 15 | 16 | MSRV is `1.77.0`. 17 | 18 | `cargo install uniffi-bindgen-java --git https://github.com/IronCoreLabs/uniffi-bindgen-java` 19 | 20 | ## Usage 21 | 22 | ``` 23 | uniffi-bindgen-java --help 24 | Java scaffolding and bindings generator for Rust 25 | 26 | Usage: 27 | 28 | Commands: 29 | generate Generate Java bindings 30 | scaffolding Generate Rust scaffolding code 31 | print-repr Print a debug representation of the interface from a dynamic library 32 | 33 | Options: 34 | -h, --help Print help 35 | -V, --version Print version 36 | ``` 37 | 38 | ### Generate Bindings 39 | 40 | ``` 41 | uniffi-bindgen-java generate --help 42 | Generate Java bindings 43 | 44 | Usage: 45 | 46 | Arguments: 47 | Path to the UDL file, or cdylib if `library-mode` is specified 48 | 49 | Options: 50 | -o, --out-dir Directory in which to write generated files. Default is same folder as .udl file 51 | -n, --no-format Do not try to format the generated bindings 52 | -c, --config Path to optional uniffi config file. This config is merged with the `uniffi.toml` config present in each crate, with its values taking precedence 53 | --lib-file Extract proc-macro metadata from a native lib (cdylib or staticlib) for this crate 54 | --library Pass in a cdylib path rather than a UDL file 55 | --crate When `--library` is passed, only generate bindings for one crate. When `--library` is not passed, use this as the crate name instead of attempting to locate and parse Cargo.toml 56 | ``` 57 | 58 | As an example: 59 | 60 | ``` 61 | > git clone https://github.com/mozilla/uniffi-rs.git 62 | > cd uniffi-rs/examples/arithmetic-proc-macro 63 | > cargo b --release 64 | > uniffi-bindgen-java generate --out-dir ./generated-java --library ../../target/release/libarithmeticpm.so 65 | > ll generated-java/uniffi/arithmeticpm/ 66 | total 216 67 | -rw-r--r-- 1 user users 295 Jul 24 13:02 ArithmeticExceptionErrorHandler.java 68 | -rw-r--r-- 1 user users 731 Jul 24 13:02 ArithmeticException.java 69 | -rw-r--r-- 1 user users 3126 Jul 24 13:02 Arithmeticpm.java 70 | -rw-r--r-- 1 user users 512 Jul 24 13:02 AutoCloseableHelper.java 71 | -rw-r--r-- 1 user users 584 Jul 24 13:02 FfiConverterBoolean.java 72 | ... 73 | > cat generated-java/uniffi/arithmeticpm/Arithmeticpm.java 74 | package uniffi.arithmeticpm; 75 | 76 | 77 | import java.util.List; 78 | import java.util.Map; 79 | import java.util.concurrent.CompletableFuture; 80 | public class Arithmeticpm { 81 | public static Long add(Long a, Long b) throws ArithmeticException { 82 | try { 83 | ... 84 | 85 | ``` 86 | 87 | ### Generate Scaffolding 88 | 89 | ``` 90 | uniffi-bindgen-java scaffolding --help 91 | Generate Rust scaffolding code 92 | 93 | Usage: 94 | 95 | Arguments: 96 | Path to the UDL file 97 | 98 | Options: 99 | -o, --out-dir Directory in which to write generated files. Default is same folder as .udl file 100 | -n, --no-format Do not try to format the generated bindings 101 | ``` 102 | 103 | ### Print Debug Representation 104 | 105 | ``` 106 | uniffi-bindgen-java print-repr --help 107 | Print a debug representation of the interface from a dynamic library 108 | 109 | Usage: 110 | 111 | Arguments: 112 | Path to the library file (.so, .dll, .dylib, or .a) 113 | ``` 114 | 115 | ## Integrating Bindings 116 | 117 | 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. 118 | 119 | Any top level functions in the Rust library will be static methods in a class named after the crate. 120 | 121 | ## Configuration 122 | 123 | The generated Java can be configured using a `uniffi.toml` configuration file. 124 | 125 | | Configuration name | Default | Description | 126 | | --- | --- | --- | 127 | | `package_name` | `uniffi` | The Java package name - ie, the value use in the `package` statement at the top of generated files. | 128 | | `cdylib_name` | `uniffi_{namespace}` | The name of the compiled Rust library containing the FFI implementation (not needed when using `generate --library`) | 129 | | `generate_immutable_records` | `false` | Whether to generate records with immutable fields (`record` instead of `class`). | 130 | | `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) | 131 | | `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) | 132 | | `android` | `false` | Used to toggle on Android specific optimizations (warning: not well tested yet) | 133 | | `android_cleaner` | `android` | Use the `android.system.SystemCleaner` instead of `java.lang.ref.Cleaner`. Fallback in both instances is the one shipped with JNA. | 134 | 135 | ### Example 136 | 137 | #### Custom types 138 | 139 | ``` 140 | [bindings.java] 141 | package_name = "customtypes" 142 | 143 | [bindings.java.custom_types.Url] 144 | # Name of the type in the Java code 145 | type_name = "URL" 146 | # Classes that need to be imported 147 | imports = ["java.net.URI", "java.net.URL"] 148 | # Functions to convert between strings and URLs 149 | lift = "new URI({}).toURL()" 150 | lower = "{}.toString()" 151 | ``` 152 | 153 | #### External Types 154 | 155 | ``` 156 | [bindings.java.external_packages] 157 | # This specifies that external types from the crate `rust-crate-name` will be referred by by the package `"java.package.name`. 158 | rust-crate-name = "java.package.name" 159 | ``` 160 | 161 | ## Notes 162 | 163 | - 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. 164 | - all primitives are signed in Java by default. Rust correctly interprets the a signed primitive value from Java as unsigned when told to. Callers of Uniffi functions need to be aware when making comparisons (`compareUnsigned`) or printing when a value is actually unsigned to code around footguns on this side. 165 | 166 | ## Unsupported features 167 | 168 | * Defaults aren't supported in Java so [uniffi struct, method, and function defaults](https://mozilla.github.io/uniffi-rs/proc_macro/index.html#default-values) don't exist in the Java code. *Note*: a reasonable case could be made for supporting defaults on structs by way of generated builder patterns. PRs welcome. 169 | * Output formatting isn't currently supported because a standalone command line Java formatter wasn't found. PRs welcome enabling that feature, the infrastructure is in place. 170 | 171 | ## Testing 172 | 173 | We pull down the pinned examples directly from Uniffi and run Java tests using the generated bindings. Run `cargo t` to run all of them. 174 | 175 | Note that if you need additional toml entries for your test, you can put a `uniffi-extras.toml` as a sibling of the test and it will be read in addition to the base `uniffi.toml` for the example. See [CustomTypes](./tests/scripts/TestCustomTypes/) for an example. Settings in `uniffi-extras.toml` apply across all namespaces. 176 | 177 | ## Versioning 178 | 179 | `uniffi-bindgen-java` is versioned separately from `uniffi-rs`. We follow the [Cargo SemVer rules](https://doc.rust-lang.org/cargo/reference/resolver.html#semver-compatibility), so versions are compatible if their left-most non-zero major/minor/patch component is the same. Any modification to the generator that causes a consumer of the generated code to need to make changes is considered breaking. 180 | 181 | `uniffi-bindgen-java` is currently unstable and being developed by IronCore Labs to target features required by [`ironcore-alloy`](https://github.com/IronCoreLabs/ironcore-alloy/). The major version is currently 0, and most changes are likely to bump the minor version. 182 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use camino::Utf8PathBuf; 3 | use clap::{Parser, Subcommand}; 4 | use std::{ 5 | collections::HashMap, 6 | fs::{self}, 7 | }; 8 | use uniffi_bindgen::{BindingGenerator, Component, ComponentInterface, GenerationSettings}; 9 | 10 | mod gen_java; 11 | 12 | pub struct JavaBindingGenerator; 13 | impl BindingGenerator for JavaBindingGenerator { 14 | type Config = gen_java::Config; 15 | 16 | fn new_config(&self, root_toml: &toml::Value) -> Result { 17 | Ok( 18 | match root_toml.get("bindings").and_then(|b| b.get("java")) { 19 | Some(v) => v.clone().try_into()?, 20 | None => Default::default(), 21 | }, 22 | ) 23 | } 24 | 25 | fn update_component_configs( 26 | &self, 27 | settings: &GenerationSettings, 28 | components: &mut Vec>, 29 | ) -> Result<()> { 30 | for c in &mut *components { 31 | c.config 32 | .package_name 33 | .get_or_insert_with(|| format!("uniffi.{}", c.ci.namespace())); 34 | c.config.cdylib_name.get_or_insert_with(|| { 35 | settings 36 | .cdylib 37 | .clone() 38 | .unwrap_or_else(|| format!("uniffi_{}", c.ci.namespace())) 39 | }); 40 | } 41 | // We need to update package names 42 | let packages = HashMap::::from_iter( 43 | components 44 | .iter() 45 | .map(|c| (c.ci.crate_name().to_string(), c.config.package_name())), 46 | ); 47 | for c in components { 48 | for (ext_crate, ext_package) in &packages { 49 | if ext_crate != c.ci.crate_name() 50 | && !c.config.external_packages.contains_key(ext_crate) 51 | { 52 | c.config 53 | .external_packages 54 | .insert(ext_crate.to_string(), ext_package.clone()); 55 | } 56 | } 57 | } 58 | Ok(()) 59 | } 60 | 61 | fn write_bindings( 62 | &self, 63 | settings: &GenerationSettings, 64 | components: &[Component], 65 | ) -> anyhow::Result<()> { 66 | let filename_capture = regex::Regex::new( 67 | r"(?m)^(?:public\s)?(?:final\s)?(?:sealed\s)?(?:abstract\s)?(?:static\s)?(?:class|interface|enum|record)\s(\w+)", 68 | ) 69 | .unwrap(); 70 | for Component { ci, config, .. } in components { 71 | let bindings_str = gen_java::generate_bindings(config, ci)?; 72 | let java_package_out_dir = &settings.out_dir.join( 73 | config 74 | .package_name() 75 | .split('.') 76 | .collect::>() 77 | .join("/"), 78 | ); 79 | fs::create_dir_all(java_package_out_dir)?; 80 | let package_line = format!("package {};", config.package_name()); 81 | let split_classes = bindings_str.split(&package_line); 82 | let writable = split_classes 83 | .map(|file| (filename_capture.captures(file), file)) 84 | .filter(|(x, _)| x.is_some()) 85 | .map(|(captures, file)| (captures.unwrap().get(1).unwrap().as_str(), file)) 86 | .collect::>(); 87 | for (filename, file) in writable { 88 | let java_file_location = java_package_out_dir.join(format!("{}.java", filename)); 89 | fs::write(&java_file_location, format!("{}\n{}", package_line, file))?; 90 | } 91 | if settings.try_format_code { 92 | // TODO: if there's a CLI formatter that makes sense to use here, use it, PRs welcome 93 | // seems like palantir-java-format is popular, but it's only exposed through plugins 94 | // google-java-format is legacy popular and does have an executable all-deps JAR, but 95 | // must be called with the full jar path including version numbers 96 | // prettier sorta works but requires npm and packages be around for a java generator 97 | } 98 | } 99 | Ok(()) 100 | } 101 | } 102 | 103 | #[derive(Parser)] 104 | #[clap(name = "uniffi-bindgen-java")] 105 | #[clap(version = clap::crate_version!())] 106 | #[clap(propagate_version = true, disable_help_subcommand = true)] 107 | /// Java scaffolding and bindings generator for Rust 108 | struct Cli { 109 | #[clap(subcommand)] 110 | command: Commands, 111 | } 112 | 113 | #[derive(Subcommand)] 114 | enum Commands { 115 | /// Generate Java bindings 116 | Generate { 117 | /// Directory in which to write generated files. Default is same folder as .udl file. 118 | #[clap(long, short)] 119 | out_dir: Option, 120 | 121 | /// Do not try to format the generated bindings. 122 | #[clap(long, short)] 123 | no_format: bool, 124 | 125 | /// Path to optional uniffi config file. This config is merged with the `uniffi.toml` config present in each crate, with its values taking precedence. 126 | #[clap(long, short)] 127 | config: Option, 128 | 129 | /// Extract proc-macro metadata from a native lib (cdylib or staticlib) for this crate. 130 | #[clap(long)] 131 | lib_file: Option, 132 | 133 | /// Pass in a cdylib path rather than a UDL file 134 | #[clap(long = "library")] 135 | library_mode: bool, 136 | 137 | /// When `--library` is passed, only generate bindings for one crate. 138 | /// When `--library` is not passed, use this as the crate name instead of attempting to 139 | /// locate and parse Cargo.toml. 140 | #[clap(long = "crate")] 141 | crate_name: Option, 142 | 143 | /// Path to the UDL file, or cdylib if `library-mode` is specified 144 | source: Utf8PathBuf, 145 | 146 | /// Whether we should exclude dependencies when running "cargo metadata". 147 | /// This will mean external types may not be resolved if they are implemented in crates 148 | /// outside of this workspace. 149 | /// This can be used in environments when all types are in the namespace and fetching 150 | /// all sub-dependencies causes obscure platform specific problems. 151 | #[clap(long)] 152 | metadata_no_deps: bool, 153 | }, 154 | /// Generate Rust scaffolding code 155 | Scaffolding { 156 | /// Directory in which to write generated files. Default is same folder as .udl file. 157 | #[clap(long, short)] 158 | out_dir: Option, 159 | 160 | /// Do not try to format the generated bindings. 161 | #[clap(long, short)] 162 | no_format: bool, 163 | 164 | /// Path to the UDL file. 165 | udl_file: Utf8PathBuf, 166 | }, 167 | /// Print a debug representation of the interface from a dynamic library 168 | PrintRepr { 169 | /// Path to the library file (.so, .dll, .dylib, or .a) 170 | path: Utf8PathBuf, 171 | }, 172 | } 173 | 174 | pub fn run_main() -> Result<()> { 175 | let cli = Cli::parse(); 176 | match cli.command { 177 | Commands::Generate { 178 | out_dir, 179 | no_format, 180 | config, 181 | lib_file, 182 | library_mode, 183 | crate_name, 184 | source, 185 | metadata_no_deps, 186 | } => { 187 | if library_mode { 188 | use uniffi_bindgen::library_mode::generate_bindings; 189 | if lib_file.is_some() { 190 | panic!("--lib-file is not compatible with --library.") 191 | } 192 | let out_dir = out_dir.expect("--out-dir is required when using --library"); 193 | 194 | let config_supplier = { 195 | use uniffi_bindgen::cargo_metadata::CrateConfigSupplier; 196 | let mut cmd = cargo_metadata::MetadataCommand::new(); 197 | if metadata_no_deps { 198 | cmd.no_deps(); 199 | } 200 | let metadata = cmd.exec()?; 201 | CrateConfigSupplier::from(metadata) 202 | }; 203 | 204 | generate_bindings( 205 | &source, 206 | crate_name, 207 | &JavaBindingGenerator, 208 | &config_supplier, 209 | config.as_deref(), 210 | &out_dir, 211 | !no_format, 212 | )?; 213 | } else { 214 | use uniffi_bindgen::generate_bindings; 215 | generate_bindings( 216 | &source, 217 | config.as_deref(), 218 | JavaBindingGenerator, 219 | out_dir.as_deref(), 220 | lib_file.as_deref(), 221 | crate_name.as_deref(), 222 | !no_format, 223 | )?; 224 | } 225 | } 226 | Commands::Scaffolding { 227 | out_dir, 228 | no_format, 229 | udl_file, 230 | } => { 231 | uniffi_bindgen::generate_component_scaffolding( 232 | &udl_file, 233 | out_dir.as_deref(), 234 | !no_format, 235 | )?; 236 | } 237 | Commands::PrintRepr { path } => { 238 | uniffi_bindgen::print_repr(&path)?; 239 | } 240 | }; 241 | Ok(()) 242 | } 243 | -------------------------------------------------------------------------------- /tests/scripts/TestRondpoint.java: -------------------------------------------------------------------------------- 1 | import java.text.MessageFormat; 2 | import java.util.List; 3 | import java.util.Map; 4 | import java.util.function.BiFunction; 5 | import java.util.function.Function; 6 | import java.util.stream.Collectors; 7 | 8 | import uniffi.rondpoint.*; 9 | 10 | public class TestRondpoint { 11 | public static void main(String[] args) throws Exception { 12 | Dictionnaire dico = new Dictionnaire(Enumeration.DEUX, true, (byte)0, 123456789L); 13 | Dictionnaire copyDico = Rondpoint.copieDictionnaire(dico); 14 | assert dico.equals(copyDico); 15 | 16 | assert Rondpoint.copieEnumeration(Enumeration.DEUX).equals(Enumeration.DEUX); 17 | assert Rondpoint.copieEnumerations(List.of(Enumeration.UN, Enumeration.DEUX)).equals(List.of(Enumeration.UN, Enumeration.DEUX)); 18 | assert Rondpoint.copieCarte(Map.ofEntries( 19 | Map.entry("0", new EnumerationAvecDonnees.Zero()), 20 | Map.entry("1", new EnumerationAvecDonnees.Un(1)), 21 | Map.entry("2", new EnumerationAvecDonnees.Deux(2, "deux")) 22 | )).equals(Map.ofEntries( 23 | Map.entry("0", new EnumerationAvecDonnees.Zero()), 24 | Map.entry("1", new EnumerationAvecDonnees.Un(1)), 25 | Map.entry("2", new EnumerationAvecDonnees.Deux(2, "deux"))) 26 | ); 27 | 28 | var var1 = new EnumerationAvecDonnees.Zero(); 29 | var var2 = new EnumerationAvecDonnees.Un(1); 30 | var var3 = new EnumerationAvecDonnees.Un(2); 31 | assert !var1.equals(var2); 32 | assert !var2.equals(var3); 33 | assert var1.equals(new EnumerationAvecDonnees.Zero()); 34 | assert !var1.equals(new EnumerationAvecDonnees.Un(1)); 35 | assert var2.equals(new EnumerationAvecDonnees.Un(1)); 36 | 37 | assert Rondpoint.switcheroo(false); 38 | 39 | // Test the roundtrip across the FFI. 40 | // This shows that the values we send come back in exactly the same state as we sent them. 41 | // i.e. it shows that lowering from Java and lifting into Rust is symmetrical with 42 | // lowering from Rust and lifting into Java. 43 | var rt = new Retourneur(); 44 | 45 | // Booleans 46 | affirmAllerRetour(List.of(true, false), rt::identiqueBoolean); 47 | 48 | // Bytes 49 | affirmAllerRetour(List.of(Byte.MIN_VALUE, Byte.MAX_VALUE), rt::identiqueI8); 50 | affirmAllerRetour(List.of((byte)0x00, (byte)0xFF), rt::identiqueU8); 51 | 52 | // Shorts 53 | affirmAllerRetour(List.of(Short.MIN_VALUE, Short.MAX_VALUE), rt::identiqueI16); 54 | affirmAllerRetour(List.of((short)0x0000, (short)0xFFFF), rt::identiqueU16); 55 | 56 | // Ints 57 | affirmAllerRetour(List.of(0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE), rt::identiqueI32); 58 | affirmAllerRetour(List.of(0x00000000, 0xFFFFFFFF), rt::identiqueU32); 59 | 60 | // Longs 61 | affirmAllerRetour(List.of(0L, 1L, -1L, Long.MIN_VALUE, Long.MAX_VALUE), rt::identiqueI64); 62 | affirmAllerRetour(List.of(0L, 1L, 0L, Long.MAX_VALUE), rt::identiqueU64); 63 | 64 | // Floats 65 | affirmAllerRetour(List.of(0.0F, 0.5F, 0.25F, Float.MIN_VALUE, Float.MAX_VALUE), rt::identiqueFloat); 66 | 67 | // Doubles 68 | affirmAllerRetour(List.of(0.0, 1.0, Double.MIN_VALUE, Double.MAX_VALUE), rt::identiqueDouble); 69 | 70 | // Strings 71 | affirmAllerRetour(List.of("", "abc", "null\u0000byte", "été", "ښي لاس ته لوستلو لوستل", "😻emoji 👨‍👧‍👦multi-emoji, 🇨🇭a flag, a canal, panama"), rt::identiqueString); 72 | 73 | // Records of primitives 74 | affirmAllerRetour(List.of(-1, 0, 1).stream().map(i -> new DictionnaireNombresSignes(i.byteValue(), i.shortValue(), i, i.longValue())).collect(Collectors.toList()), rt::identiqueNombresSignes); 75 | affirmAllerRetour(List.of(0, 1).stream().map(i -> new DictionnaireNombres(i.byteValue(), i.shortValue(), i, i.longValue())).collect(Collectors.toList()), rt::identiqueNombres); 76 | 77 | rt.close(); 78 | 79 | // Test one way across the FFI. 80 | // 81 | // We send one representation of a value to lib.rs, and it transforms it into another, a string. 82 | // lib.rs sends the string back, and then we compare here in Java. 83 | // 84 | // This shows that the values are transformed into strings the same way in both Java and Rust. 85 | // i.e. if we assume that the string return works (we test this assumption elsewhere) 86 | // we show that lowering from kotlin and lifting into rust has values that both Java and Rust 87 | // both stringify in the same way. i.e. the same values. 88 | // 89 | // If we roundtripping proves the symmetry of our lowering/lifting from here to Rust, and lowering/lifting from Rust to here, 90 | // and this convinces us that lowering/lifting from here to Rust is correct, then 91 | // together, we've shown the correctness of the return leg. 92 | var st = new Stringifier(); 93 | 94 | // Test the efficacy of the string transport from Rust. If this fails, but everything else 95 | // works, then things are very weird. 96 | var wellKnown = st.wellKnownString("java"); 97 | assert "uniffi 💚 java!".equals(wellKnown) : MessageFormat.format("wellKnownString 'uniffi 💚 java!' == '{0}'", wellKnown); 98 | 99 | // Booleans 100 | affirmEnchaine(List.of(true, false), st::toStringBoolean, TestRondpoint::defaultStringyEquals); 101 | 102 | // All primitives are signed in Java by default. Rust correctly interprets the same signed max as an unsigned max 103 | // when told to. We have to mask the value we expect on the comparison side for Java, or else it will toString them 104 | // as signed values. Callers of Uniffi functions need to be aware when making comparisons (`compareUnsigned`) or 105 | // printing when a value is actually unsigned to code around footguns on this side. 106 | // Bytes 107 | affirmEnchaine(List.of(Byte.MIN_VALUE, Byte.MAX_VALUE), st::toStringI8, TestRondpoint::defaultStringyEquals); 108 | affirmEnchaine(List.of(Byte.MIN_VALUE, Byte.MAX_VALUE), st::toStringU8, (obs, exp) -> obs.equals(String.valueOf(exp & 0xff))); 109 | 110 | // Shorts 111 | affirmEnchaine(List.of(Short.MIN_VALUE, Short.MAX_VALUE), st::toStringI16, TestRondpoint::defaultStringyEquals); 112 | affirmEnchaine(List.of(Short.MIN_VALUE, Short.MAX_VALUE), st::toStringU16, (obs, exp) -> obs.equals(String.valueOf(exp & 0xffff))); 113 | 114 | // Ints 115 | affirmEnchaine(List.of(0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE), st::toStringI32, TestRondpoint::defaultStringyEquals); 116 | affirmEnchaine(List.of(0, 1, Integer.MIN_VALUE, Integer.MAX_VALUE), st::toStringU32, (obs, exp) -> obs.equals(Integer.toUnsignedString(exp))); 117 | 118 | // Longs 119 | affirmEnchaine(List.of(0L, 1L, -1L, Long.MIN_VALUE, Long.MAX_VALUE), st::toStringI64, TestRondpoint::defaultStringyEquals); 120 | affirmEnchaine(List.of(0L, 1L, 0L, Long.MAX_VALUE), st::toStringU64, TestRondpoint::defaultStringyEquals); 121 | 122 | // Floats 123 | // MIN_VALUE is 1.4E-45. Accuracy and formatting get weird at small sizes. 124 | affirmEnchaine(List.of(0.0F, 1.0F, -1.0F, Float.MIN_VALUE, Float.MAX_VALUE), st::toStringFloat, (s, n) -> Float.parseFloat(s) == n); 125 | 126 | // Doubles 127 | // MIN_VALUE is 4.9E-324. Accuracy and formatting get weird at small sizes. 128 | affirmEnchaine(List.of(0.0, 1.0, -1.0, Double.MIN_VALUE, Double.MAX_VALUE), st::toStringDouble, (s, n) -> Double.parseDouble(s) == n); 129 | 130 | st.close(); 131 | 132 | // Defaults aren't supported in Java, so we check that our Java `None` equivalent goes across the barrier correctly 133 | // as an option and comes back instead. See Kotlin's rondpoint tests for reference default behavior if you want to 134 | // PR defaults as a feature. 135 | // Step 1: call the methods without arguments, check that Option works. 136 | var op = new Optionneur(); 137 | 138 | // Optionals 139 | assert op.sinonNull(null) == null; 140 | assert op.sinonZero(null) == null; 141 | 142 | // Step 2. Convince ourselves that if we pass something else, then that changes the output. 143 | // We have shown something coming out of the sinon methods, but without eyeballing the Rust 144 | // we can't be sure that the arguments will change the return value. 145 | affirmAllerRetour(List.of("foo", "bar"), op::sinonString); 146 | affirmAllerRetour(List.of(true, false), op::sinonBoolean); 147 | affirmAllerRetour(List.of(List.of("a", "b"), List.of()), op::sinonSequence); 148 | 149 | // Optionals 150 | affirmAllerRetour(List.of("0", "1"), op::sinonNull); 151 | affirmAllerRetour(List.of(0, 1), op::sinonZero); 152 | 153 | // Integers 154 | affirmAllerRetour(List.of((byte)0, (byte)1), op::sinonI8Dec); 155 | affirmAllerRetour(List.of((byte)0, (byte)1), op::sinonU8Dec); 156 | affirmAllerRetour(List.of((short)0, (short)1), op::sinonI16Dec); 157 | affirmAllerRetour(List.of((short)0, (short)1), op::sinonU16Dec); 158 | affirmAllerRetour(List.of(0, 1), op::sinonI32Dec); 159 | affirmAllerRetour(List.of(0, 1), op::sinonU32Dec); 160 | affirmAllerRetour(List.of(0L, 1L), op::sinonI64Dec); 161 | affirmAllerRetour(List.of(0L, 1L), op::sinonU64Dec); 162 | 163 | // Hexadecimal integers 164 | affirmAllerRetour(List.of((byte)0, (byte)1), op::sinonI8Hex); 165 | affirmAllerRetour(List.of((byte)0, (byte)1), op::sinonU8Hex); 166 | affirmAllerRetour(List.of((short)0, (short)1), op::sinonI16Hex); 167 | affirmAllerRetour(List.of((short)0, (short)1), op::sinonU16Hex); 168 | affirmAllerRetour(List.of(0, 1), op::sinonI32Hex); 169 | affirmAllerRetour(List.of(0, 1), op::sinonU32Hex); 170 | affirmAllerRetour(List.of(0L, 1L), op::sinonI64Hex); 171 | affirmAllerRetour(List.of(0L, 1L), op::sinonU64Hex); 172 | 173 | // Octal integers 174 | affirmAllerRetour(List.of(0, 1), op::sinonU32Oct); 175 | 176 | // Floats 177 | affirmAllerRetour(List.of(0.0f, 1.0f), op::sinonF32); 178 | affirmAllerRetour(List.of(0.0, 1.0), op::sinonF64); 179 | 180 | // Enums 181 | affirmAllerRetour(List.of(Enumeration.values()), op::sinonEnum); 182 | 183 | op.close(); 184 | } 185 | 186 | private static void affirmAllerRetour(List vs, Function f) { 187 | for (var v : vs) { 188 | assert v.equals(f.apply(v)) : MessageFormat.format("{0}({1})", f, v); 189 | } 190 | } 191 | 192 | private static void affirmEnchaine(List vs, Function f, BiFunction equals) { 193 | for (var exp : vs) { 194 | var obs = f.apply(exp); 195 | assert equals.apply(obs, exp) : MessageFormat.format("{0}({1}): observed={2}, expected={3}", f, exp, obs, exp); 196 | } 197 | } 198 | 199 | private static Boolean defaultStringyEquals(String obs, T exp) { 200 | return obs.equals(exp.toString()); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /tests/tests.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use anyhow::{Context, Result, bail}; 6 | use camino::{Utf8Path, Utf8PathBuf}; 7 | use cargo_metadata::{CrateType, MetadataCommand, Package, Target}; 8 | use std::env::consts::ARCH; 9 | use std::io::{Read, Write}; 10 | use std::process::Command; 11 | use std::time::{SystemTime, UNIX_EPOCH}; 12 | use std::{env, fs}; 13 | use uniffi_bindgen::library_mode::generate_bindings; 14 | use uniffi_bindgen_java::JavaBindingGenerator; 15 | use uniffi_testing::UniFFITestHelper; 16 | 17 | /// Run the test fixtures from UniFFI 18 | fn run_test(fixture_name: &str, test_file: &str) -> Result<()> { 19 | let test_path = Utf8Path::new(".").join("tests").join(test_file); 20 | let test_helper = UniFFITestHelper::new(fixture_name)?; 21 | let out_dir = test_helper.create_out_dir(env!("CARGO_TARGET_TMPDIR"), &test_path)?; 22 | let cdylib_path = test_helper.cdylib_path()?; 23 | 24 | // This whole block in designed to create a new TOML file if there is one in the fixture or a uniffi-extras.toml as a sibling of the test. The extras 25 | // will be concatenated to the end of the base with extra if available. 26 | let maybe_new_uniffi_toml_filename = { 27 | let maybe_base_uniffi_toml_string = 28 | find_uniffi_toml(fixture_name)?.and_then(read_file_contents); 29 | let maybe_extra_uniffi_toml_string = 30 | read_file_contents(test_path.with_file_name("uniffi-extras.toml")); 31 | 32 | // final_string will be "" if there aren't any toml files to read. 33 | let final_string: String = itertools::Itertools::intersperse( 34 | vec![ 35 | maybe_base_uniffi_toml_string, 36 | maybe_extra_uniffi_toml_string, 37 | ] 38 | .into_iter() 39 | .filter_map(|s| s), 40 | "\n".to_string(), 41 | ) 42 | .collect(); 43 | 44 | // If there wasn't anything read from the files, just return none so the default config file can be used. 45 | if final_string == "" { 46 | None 47 | } else { 48 | //Create a unique(ish) filename for the fixture. We'll just accept that nanosecond uniqueness is good enough per fixture_name. 49 | let current_time = SystemTime::now() 50 | .duration_since(UNIX_EPOCH) 51 | .expect("Time went backwards") 52 | .as_nanos(); 53 | let new_filename = 54 | out_dir.with_file_name(format!("{}-{}.toml", fixture_name, current_time)); 55 | write_file_contents(&new_filename, &final_string)?; 56 | Some(new_filename) 57 | } 58 | }; 59 | 60 | let config_supplier = { 61 | use uniffi_bindgen::cargo_metadata::CrateConfigSupplier; 62 | let cmd = cargo_metadata::MetadataCommand::new(); 63 | let metadata = cmd.exec()?; 64 | CrateConfigSupplier::from(metadata) 65 | }; 66 | 67 | // generate the fixture bindings 68 | generate_bindings( 69 | &cdylib_path, 70 | None, 71 | &JavaBindingGenerator, 72 | &config_supplier, 73 | maybe_new_uniffi_toml_filename.as_deref(), 74 | &out_dir, 75 | true, 76 | )?; 77 | 78 | // jna requires a specific resources path inside the jar by default, create that folder 79 | let cdylib_java_resource_folder = if cdylib_path.extension().unwrap() == "dylib" { 80 | format!("darwin-{}", ARCH).replace("_", "-") 81 | } else { 82 | format!("linux-{}", ARCH).replace("_", "-") 83 | }; 84 | let cdylib_java_resource_path = out_dir.join("staging").join(cdylib_java_resource_folder); 85 | fs::create_dir_all(&cdylib_java_resource_path)?; 86 | let cdylib_dest = cdylib_java_resource_path.join(cdylib_path.file_name().unwrap()); 87 | fs::copy(&cdylib_path, &cdylib_dest)?; 88 | 89 | // compile generated bindings and form jar 90 | let jar_file = build_jar(&fixture_name, &out_dir)?; 91 | 92 | // compile test 93 | let status = Command::new("javac") 94 | .arg("-classpath") 95 | .arg(calc_classpath(vec![&out_dir, &jar_file])) 96 | // Our tests should not produce any warnings. 97 | .arg("-Werror") 98 | .arg(&test_path) 99 | .spawn() 100 | .context("Failed to spawn `javac` to compile Java test")? 101 | .wait() 102 | .context("Failed to wait for `javac` when compiling Java test")?; 103 | if !status.success() { 104 | anyhow::bail!("running `javac` failed when compiling the Java test") 105 | } 106 | 107 | // run resulting test 108 | let compiled_path = test_path.file_stem().unwrap(); 109 | let run_status = Command::new("java") 110 | // allow for runtime assertions 111 | .arg("-ea") 112 | .arg("-classpath") 113 | .arg(calc_classpath(vec![ 114 | &out_dir, 115 | &jar_file, 116 | &test_path.parent().unwrap().to_path_buf(), 117 | ])) 118 | .arg(compiled_path) 119 | .spawn() 120 | .context("Failed to spawn `java` to run Java test")? 121 | .wait() 122 | .context("Failed to wait for `java` when running Java test")?; 123 | if !run_status.success() { 124 | anyhow::bail!("Running the `java` test failed.") 125 | } 126 | 127 | Ok(()) 128 | } 129 | 130 | /// Get the uniffi_toml of the fixture if it exists. 131 | /// It looks for it in the root directory of the project `name`. 132 | fn find_uniffi_toml(name: &str) -> Result> { 133 | let metadata = MetadataCommand::new() 134 | .exec() 135 | .expect("error running cargo metadata"); 136 | let matching: Vec<&Package> = metadata 137 | .packages 138 | .iter() 139 | .filter(|p| p.name == name) 140 | .collect(); 141 | let package = match matching.len() { 142 | 1 => matching[0].clone(), 143 | n => bail!("cargo metadata return {n} packages named {name}"), 144 | }; 145 | let cdylib_targets: Vec<&Target> = package 146 | .targets 147 | .iter() 148 | .filter(|t| t.crate_types.iter().any(|t| t == &CrateType::CDyLib)) 149 | .collect(); 150 | let target = match cdylib_targets.len() { 151 | 1 => cdylib_targets[0], 152 | n => bail!("Found {n} cdylib targets for {}", package.name), 153 | }; 154 | let maybe_uniffi_toml = target 155 | .src_path 156 | .parent() 157 | .map(|uniffi_toml_dir| uniffi_toml_dir.with_file_name("uniffi.toml")); 158 | Ok(maybe_uniffi_toml) 159 | } 160 | 161 | /// Generate java bindings for the given namespace, then use the Java 162 | /// command-line tools to compile them into a .jar file. 163 | fn build_jar(fixture_name: &str, out_dir: &Utf8PathBuf) -> Result { 164 | let mut jar_file = Utf8PathBuf::from(out_dir); 165 | jar_file.push(format!("{}.jar", fixture_name)); 166 | let staging_dir = out_dir.join("staging"); 167 | 168 | let status = Command::new("javac") 169 | // Our generated bindings should not produce any warnings; fail tests if they do. 170 | .arg("-Werror") 171 | .arg("-d") 172 | .arg(&staging_dir) 173 | .arg("-classpath") 174 | // JNA must already be in the system classpath 175 | .arg(calc_classpath(vec![])) 176 | .args( 177 | glob::glob(&out_dir.join("**/*.java").into_string())? 178 | .flatten() 179 | .map(|p| String::from(p.to_string_lossy())), 180 | ) 181 | .spawn() 182 | .context("Failed to spawn `javac` to compile the bindings")? 183 | .wait() 184 | .context("Failed to wait for `javac` when compiling the bindings")?; 185 | if !status.success() { 186 | bail!("running `javac` failed when compiling the bindings") 187 | } 188 | 189 | let jar_status = Command::new("jar") 190 | .current_dir(out_dir) 191 | .arg("cf") 192 | .arg(jar_file.file_name().unwrap()) 193 | .arg("-C") 194 | .arg(&staging_dir) 195 | .arg(".") 196 | .spawn() 197 | .context("Failed to spawn `jar` to package the bindings")? 198 | .wait() 199 | .context("Failed to wait for `jar` when packaging the bindings")?; 200 | if !jar_status.success() { 201 | bail!("running `jar` failed") 202 | } 203 | 204 | Ok(jar_file) 205 | } 206 | 207 | fn calc_classpath(extra_paths: Vec<&Utf8PathBuf>) -> String { 208 | extra_paths 209 | .into_iter() 210 | .map(|p| p.to_string()) 211 | // Add the system classpath as a component, using the fact that env::var returns an Option, 212 | // which implement Iterator 213 | .chain(env::var("CLASSPATH")) 214 | .collect::>() 215 | .join(":") 216 | } 217 | 218 | /// Read the contents of the file. Any errors will be turned into None. 219 | fn read_file_contents(path: Utf8PathBuf) -> Option { 220 | if let Ok(metadata) = fs::metadata(&path) { 221 | if metadata.is_file() { 222 | let mut content = String::new(); 223 | std::fs::File::open(path) 224 | .ok()? 225 | .read_to_string(&mut content) 226 | .ok()?; 227 | Some(content) 228 | } else { 229 | None 230 | } 231 | } else { 232 | None 233 | } 234 | } 235 | 236 | fn write_file_contents(path: &Utf8PathBuf, contents: &str) -> Result<()> { 237 | std::fs::File::create(path)?.write_all(contents.as_bytes())?; 238 | Ok(()) 239 | } 240 | 241 | macro_rules! fixture_tests { 242 | { 243 | $(($test_name:ident, $fixture_name:expr, $test_script:expr),)* 244 | } => { 245 | $( 246 | #[test] 247 | fn $test_name() -> Result<()> { 248 | run_test($fixture_name, $test_script) 249 | } 250 | )* 251 | } 252 | } 253 | 254 | fixture_tests! { 255 | (test_arithmetic, "uniffi-example-arithmetic", "scripts/TestArithmetic.java"), 256 | (test_geometry, "uniffi-example-geometry", "scripts/TestGeometry.java"), 257 | (test_rondpoint, "uniffi-example-rondpoint", "scripts/TestRondpoint.java"), 258 | // (test_todolist, "uniffi-example-todolist", "scripts/test_todolist.java"), 259 | // (test_sprites, "uniffi-example-sprites", "scripts/test_sprites.java"), 260 | (test_coverall, "uniffi-fixture-coverall", "scripts/TestFixtureCoverall.java"), 261 | (test_chronological, "uniffi-fixture-time", "scripts/TestChronological.java"), 262 | (test_custom_types, "uniffi-example-custom-types", "scripts/TestCustomTypes/TestCustomTypes.java"), 263 | // (test_callbacks, "uniffi-fixture-callbacks", "scripts/test_callbacks.java"), 264 | (test_external_types, "uniffi-fixture-ext-types", "scripts/TestImportedTypes/TestImportedTypes.java"), 265 | (test_futures, "uniffi-example-futures", "scripts/TestFutures.java"), 266 | (test_futures_fixtures, "uniffi-fixture-futures", "scripts/TestFixtureFutures/TestFixtureFutures.java"), 267 | } 268 | -------------------------------------------------------------------------------- /src/templates/Async.java: -------------------------------------------------------------------------------- 1 | package {{ config.package_name() }}; 2 | 3 | import java.util.concurrent.CompletableFuture; 4 | import java.util.concurrent.ExecutionException; 5 | import java.util.function.BiConsumer; 6 | import java.util.function.BiFunction; 7 | import java.util.function.Consumer; 8 | import java.util.function.Function; 9 | import java.util.function.Supplier; 10 | 11 | public final class UniffiAsyncHelpers { 12 | // Async return type handlers 13 | static final byte UNIFFI_RUST_FUTURE_POLL_READY = (byte) 0; 14 | static final byte UNIFFI_RUST_FUTURE_POLL_MAYBE_READY = (byte) 1; 15 | static final UniffiHandleMap> uniffiContinuationHandleMap = new UniffiHandleMap<>(); 16 | static final UniffiHandleMap uniffiForeignFutureHandleMap = new UniffiHandleMap<>(); 17 | 18 | // FFI type for Rust future continuations 19 | enum UniffiRustFutureContinuationCallbackImpl implements UniffiRustFutureContinuationCallback { 20 | INSTANCE; 21 | 22 | @Override 23 | public void callback(long data, byte pollResult) { 24 | uniffiContinuationHandleMap.remove(data).complete(pollResult); 25 | } 26 | } 27 | 28 | @FunctionalInterface 29 | interface PollingFunction { 30 | void apply(long rustFuture, UniffiRustFutureContinuationCallback callback, long continuationHandle); 31 | } 32 | 33 | static class UniffiFreeingFuture extends CompletableFuture { 34 | private Consumer freeFunc; 35 | private long rustFuture; 36 | 37 | public UniffiFreeingFuture(long rustFuture, Consumer freeFunc) { 38 | this.freeFunc = freeFunc; 39 | this.rustFuture = rustFuture; 40 | } 41 | 42 | @Override 43 | public boolean cancel(boolean ignored) { 44 | boolean cancelled = super.cancel(ignored); 45 | if (cancelled) { 46 | freeFunc.accept(rustFuture); 47 | } 48 | return cancelled; 49 | } 50 | } 51 | 52 | // helper so both the Java completable future and the job that handles it finishing and reports to Rust can be 53 | // retrieved (and potentially cancelled) by handle. This allows our FreeImpl to be a parameterless singleton, 54 | // preventing #19, which was caused by our FreeImpls being GCd before Rust called back into them. 55 | static class CancelableForeignFuture { 56 | private CompletableFuture childFuture; 57 | private CompletableFuture childFutureHandler; 58 | 59 | CancelableForeignFuture(CompletableFuture childFuture, CompletableFuture childFutureHandler) { 60 | this.childFuture = childFuture; 61 | this.childFutureHandler = childFutureHandler; 62 | } 63 | 64 | public void cancel() { 65 | var successfullyCancelled = this.childFutureHandler.cancel(true); 66 | if(successfullyCancelled) { 67 | childFuture.cancel(true); 68 | } 69 | } 70 | } 71 | 72 | static CompletableFuture uniffiRustCallAsync( 73 | long rustFuture, 74 | PollingFunction pollFunc, 75 | BiFunction completeFunc, 76 | Consumer freeFunc, 77 | Function liftFunc, 78 | UniffiRustCallStatusErrorHandler errorHandler 79 | ){ 80 | CompletableFuture future = new UniffiFreeingFuture<>(rustFuture, freeFunc); 81 | 82 | CompletableFuture.runAsync(() -> { 83 | try { 84 | byte pollResult; 85 | do { 86 | pollResult = poll(rustFuture, pollFunc); 87 | } while (pollResult != UNIFFI_RUST_FUTURE_POLL_READY); 88 | 89 | if (!future.isCancelled()) { 90 | F result = UniffiHelpers.uniffiRustCallWithError(errorHandler, status -> { 91 | return completeFunc.apply(rustFuture, status); 92 | }); 93 | T liftedResult = liftFunc.apply(result); 94 | future.complete(liftedResult); 95 | } 96 | } catch (Exception e) { 97 | future.completeExceptionally(e); 98 | } finally { 99 | if (!future.isCancelled()) { 100 | freeFunc.accept(rustFuture); 101 | } 102 | } 103 | }); 104 | 105 | return future; 106 | } 107 | 108 | 109 | // overload specifically for Void cases, which aren't within the Object type. 110 | // This is only necessary because of Java's lack of proper Any/Unit 111 | static CompletableFuture uniffiRustCallAsync( 112 | long rustFuture, 113 | PollingFunction pollFunc, 114 | BiConsumer completeFunc, 115 | Consumer freeFunc, 116 | Runnable liftFunc, 117 | UniffiRustCallStatusErrorHandler errorHandler 118 | ){ 119 | CompletableFuture future = new UniffiFreeingFuture<>(rustFuture, freeFunc); 120 | 121 | CompletableFuture.runAsync(() -> { 122 | try { 123 | byte pollResult; 124 | do { 125 | pollResult = poll(rustFuture, pollFunc); 126 | } while (pollResult != UNIFFI_RUST_FUTURE_POLL_READY); 127 | 128 | // even though the outer `future` has been cancelled, this inner `runAsync` is unsupervised 129 | // and keeps running. When it calls `completeFunc` after being cancelled, it's status is `SUCCESS` 130 | // (assuming the Rust part succeeded), and the function being called can lead to a core dump. 131 | // Guarding with `isCancelled` here makes everything work, but feels like a cludge. 132 | if (!future.isCancelled()) { 133 | UniffiHelpers.uniffiRustCallWithError(errorHandler, status -> { 134 | completeFunc.accept(rustFuture, status); 135 | }); 136 | future.complete(null); 137 | } 138 | } catch (Throwable e) { 139 | future.completeExceptionally(e); 140 | } finally { 141 | if (!future.isCancelled()) { 142 | freeFunc.accept(rustFuture); 143 | } 144 | } 145 | }); 146 | 147 | return future; 148 | } 149 | 150 | private static byte poll(long rustFuture, PollingFunction pollFunc) throws InterruptedException, ExecutionException { 151 | CompletableFuture pollFuture = new CompletableFuture<>(); 152 | var handle = uniffiContinuationHandleMap.insert(pollFuture); 153 | pollFunc.apply(rustFuture, UniffiRustFutureContinuationCallbackImpl.INSTANCE, handle); 154 | do {} while (!pollFuture.isDone()); // removing this makes futures not cancel (sometimes) 155 | return pollFuture.get(); 156 | } 157 | 158 | {%- if ci.has_async_callback_interface_definition() %} 159 | static UniffiForeignFuture uniffiTraitInterfaceCallAsync( 160 | Supplier> makeCall, 161 | Consumer handleSuccess, 162 | Consumer handleError 163 | ){ 164 | // Uniffi does its best to support structured concurrency across the FFI. 165 | // If the Rust future is dropped, `UniffiForeignFutureFreeImpl` is called, which will cancel the Java completable future if it's still running. 166 | var foreignFutureCf = makeCall.get(); 167 | CompletableFuture ffHandler = CompletableFuture.supplyAsync(() -> { 168 | try { 169 | foreignFutureCf.thenAcceptAsync(handleSuccess).get(); 170 | } catch(Throwable e) { 171 | // if we errored inside the CF, it's that error we want to send to Rust, not the wrapper 172 | if (e instanceof ExecutionException) { 173 | e = e.getCause(); 174 | } 175 | handleError.accept( 176 | UniffiRustCallStatus.create( 177 | UniffiRustCallStatus.UNIFFI_CALL_UNEXPECTED_ERROR, 178 | {{ Type::String.borrow()|lower_fn(config, ci) }}(e.toString()) 179 | ) 180 | ); 181 | } 182 | 183 | return null; 184 | }); 185 | long handle = uniffiForeignFutureHandleMap.insert(new CancelableForeignFuture(foreignFutureCf, ffHandler)); 186 | return new UniffiForeignFuture(handle, UniffiForeignFutureFreeImpl.INSTANCE); 187 | } 188 | 189 | @SuppressWarnings("unchecked") 190 | static UniffiForeignFuture uniffiTraitInterfaceCallAsyncWithError( 191 | Supplier> makeCall, 192 | Consumer handleSuccess, 193 | Consumer handleError, 194 | Function lowerError, 195 | Class errorClass 196 | ){ 197 | var foreignFutureCf = makeCall.get(); 198 | CompletableFuture ffHandler = CompletableFuture.supplyAsync(() -> { 199 | try { 200 | foreignFutureCf.thenAcceptAsync(handleSuccess).get(); 201 | } catch (Throwable e) { 202 | // if we errored inside the CF, it's that error we want to send to Rust, not the wrapper 203 | if (e instanceof ExecutionException) { 204 | e = e.getCause(); 205 | } 206 | if (errorClass.isInstance(e)) { 207 | handleError.accept( 208 | UniffiRustCallStatus.create( 209 | UniffiRustCallStatus.UNIFFI_CALL_ERROR, 210 | lowerError.apply((E) e) 211 | ) 212 | ); 213 | } else { 214 | handleError.accept( 215 | UniffiRustCallStatus.create( 216 | UniffiRustCallStatus.UNIFFI_CALL_UNEXPECTED_ERROR, 217 | {{ Type::String.borrow()|lower_fn(config, ci) }}(e.getMessage()) 218 | ) 219 | ); 220 | } 221 | } 222 | 223 | return null; 224 | }); 225 | 226 | long handle = uniffiForeignFutureHandleMap.insert(new CancelableForeignFuture(foreignFutureCf, ffHandler)); 227 | return new UniffiForeignFuture(handle, UniffiForeignFutureFreeImpl.INSTANCE); 228 | } 229 | 230 | enum UniffiForeignFutureFreeImpl implements UniffiForeignFutureFree { 231 | INSTANCE; 232 | 233 | @Override 234 | public void callback(long handle) { 235 | var futureWithHandler = uniffiForeignFutureHandleMap.remove(handle); 236 | futureWithHandler.cancel(); 237 | } 238 | } 239 | 240 | // For testing 241 | public static int uniffiForeignFutureHandleCount() { 242 | return uniffiForeignFutureHandleMap.size(); 243 | } 244 | {%- endif %} 245 | } 246 | 247 | 248 | -------------------------------------------------------------------------------- /src/templates/ObjectTemplate.java: -------------------------------------------------------------------------------- 1 | // This template implements a class for working with a Rust struct via a Pointer/Arc 2 | // to the live Rust struct on the other side of the FFI. 3 | // 4 | // Each instance implements core operations for working with the Rust `Arc` and the 5 | // Kotlin Pointer to work with the live Rust struct on the other side of the FFI. 6 | // 7 | // There's some subtlety here, because we have to be careful not to operate on a Rust 8 | // struct after it has been dropped, and because we must expose a public API for freeing 9 | // the Java wrapper object in lieu of reliable finalizers. The core requirements are: 10 | // 11 | // * Each instance holds an opaque pointer to the underlying Rust struct. 12 | // Method calls need to read this pointer from the object's state and pass it in to 13 | // the Rust FFI. 14 | // 15 | // * When an instance is no longer needed, its pointer should be passed to a 16 | // special destructor function provided by the Rust FFI, which will drop the 17 | // underlying Rust struct. 18 | // 19 | // * Given an instance, calling code is expected to call the special 20 | // `close` method in order to free it after use, either by calling it explicitly 21 | // or by using a higher-level helper like `try-with-resources`. Failing to do so risks 22 | // leaking the underlying Rust struct. 23 | // 24 | // * We can't assume that calling code will do the right thing, and must be prepared 25 | // to handle Java method calls executing concurrently with or even after a call to 26 | // `close`, and to handle multiple (possibly concurrent!) calls to `close`. 27 | // 28 | // * We must never allow Rust code to operate on the underlying Rust struct after 29 | // the destructor has been called, and must never call the destructor more than once. 30 | // Doing so may trigger memory unsafety. 31 | // 32 | // * To mitigate many of the risks of leaking memory and use-after-free unsafety, a `Cleaner` 33 | // is implemented to call the destructor when the Java object becomes unreachable. 34 | // This is done in a background thread. This is not a panacea, and client code should be aware that 35 | // 1. the thread may starve if some there are objects that have poorly performing 36 | // `drop` methods or do significant work in their `drop` methods. 37 | // 2. the thread is shared across the whole library. This can be tuned by using `android_cleaner = true`, 38 | // or `android = true` in the [`java` section of the `uniffi.toml` file, like the Kotlin one](https://mozilla.github.io/uniffi-rs/kotlin/configuration.html). 39 | // 40 | // If we try to implement this with mutual exclusion on access to the pointer, there is the 41 | // possibility of a race between a method call and a concurrent call to `close`: 42 | // 43 | // * Thread A starts a method call, reads the value of the pointer, but is interrupted 44 | // before it can pass the pointer over the FFI to Rust. 45 | // * Thread B calls `close` and frees the underlying Rust struct. 46 | // * Thread A resumes, passing the already-read pointer value to Rust and triggering 47 | // a use-after-free. 48 | // 49 | // One possible solution would be to use a `ReadWriteLock`, with each method call taking 50 | // a read lock (and thus allowed to run concurrently) and the special `close` method 51 | // taking a write lock (and thus blocking on live method calls). However, we aim not to 52 | // generate methods with any hidden blocking semantics, and a `close` method that might 53 | // block if called incorrectly seems to meet that bar. 54 | // 55 | // So, we achieve our goals by giving each instance an associated `AtomicLong` counter to track 56 | // the number of in-flight method calls, and an `AtomicBoolean` flag to indicate whether `close` 57 | // has been called. These are updated according to the following rules: 58 | // 59 | // * The initial value of the counter is 1, indicating a live object with no in-flight calls. 60 | // The initial value for the flag is false. 61 | // 62 | // * At the start of each method call, we atomically check the counter. 63 | // If it is 0 then the underlying Rust struct has already been destroyed and the call is aborted. 64 | // If it is nonzero them we atomically increment it by 1 and proceed with the method call. 65 | // 66 | // * At the end of each method call, we atomically decrement and check the counter. 67 | // If it has reached zero then we destroy the underlying Rust struct. 68 | // 69 | // * When `close` is called, we atomically flip the flag from false to true. 70 | // If the flag was already true we silently fail. 71 | // Otherwise we atomically decrement and check the counter. 72 | // If it has reached zero then we destroy the underlying Rust struct. 73 | // 74 | // Astute readers may observe that this all sounds very similar to the way that Rust's `Arc` works, 75 | // and indeed it is, with the addition of a flag to guard against multiple calls to `close`. 76 | // 77 | // The overall effect is that the underlying Rust struct is destroyed only when `close` has been 78 | // called *and* all in-flight method calls have completed, avoiding violating any of the expectations 79 | // of the underlying Rust code. 80 | // 81 | // This makes a cleaner a better alternative to _not_ calling `close()` as 82 | // and when the object is finished with, but the abstraction is not perfect: if the Rust object's `drop` 83 | // method is slow, and/or there are many objects to cleanup, and it's on a low end Android device, then the cleaner 84 | // thread may be starved, and the app will leak memory. 85 | // 86 | // In this case, `close`ing manually may be a better solution. 87 | // 88 | // The cleaner can live side by side with the manual calling of `close`. In the order of responsiveness, uniffi objects 89 | // with Rust peers are reclaimed: 90 | // 91 | // 1. By calling the `close` method of the object, which calls `rustObject.free()`. If that doesn't happen: 92 | // 2. When the object becomes unreachable, AND the Cleaner thread gets to call `rustObject.free()`. If the thread is starved then: 93 | // 3. The memory is reclaimed when the process terminates. 94 | // 95 | // [1] https://stackoverflow.com/questions/24376768/can-java-finalize-an-object-when-it-is-still-in-scope/24380219 96 | // 97 | 98 | {%- if self.include_once_check("interface-support") %} 99 | {%- include "ObjectCleanerHelper.java" %} 100 | {%- endif %} 101 | 102 | {%- let obj = ci.get_object_definition(name).unwrap() %} 103 | {%- let (interface_name, impl_class_name) = obj|object_names(ci) %} 104 | {%- let methods = obj.methods() %} 105 | {%- let interface_docstring = obj.docstring() %} 106 | {%- let is_error = ci.is_name_used_as_error(name) %} 107 | {%- let ffi_converter_name = obj|ffi_converter_name %} 108 | 109 | {%- include "Interface.java" %} 110 | 111 | package {{ config.package_name() }}; 112 | 113 | import java.util.List; 114 | import java.util.Map; 115 | import java.util.concurrent.atomic.AtomicBoolean; 116 | import java.util.concurrent.atomic.AtomicLong; 117 | import java.util.function.Function; 118 | import java.util.function.Consumer; 119 | import com.sun.jna.Pointer; 120 | import java.util.concurrent.CompletableFuture; 121 | 122 | {%- call java::docstring(obj, 0) %} 123 | {% if (is_error) %} 124 | public class {{ impl_class_name }} extends Exception implements AutoCloseable, {{ interface_name }} { 125 | {% else -%} 126 | public class {{ impl_class_name }} implements AutoCloseable, {{ interface_name }} { 127 | {%- endif %} 128 | protected Pointer pointer; 129 | protected UniffiCleaner.Cleanable cleanable; 130 | 131 | private AtomicBoolean wasDestroyed = new AtomicBoolean(false); 132 | private AtomicLong callCounter = new AtomicLong(1); 133 | 134 | public {{ impl_class_name }}(Pointer pointer) { 135 | this.pointer = pointer; 136 | this.cleanable = UniffiLib.CLEANER.register(this, new UniffiCleanAction(pointer)); 137 | } 138 | 139 | /** 140 | * This constructor can be used to instantiate a fake object. Only used for tests. Any 141 | * attempt to actually use an object constructed this way will fail as there is no 142 | * connected Rust object. 143 | */ 144 | public {{ impl_class_name }}(NoPointer noPointer) { 145 | this.pointer = null; 146 | this.cleanable = UniffiLib.CLEANER.register(this, new UniffiCleanAction(pointer)); 147 | } 148 | 149 | {% match obj.primary_constructor() %} 150 | {%- when Some(cons) %} 151 | {%- if cons.is_async() %} 152 | // Note no constructor generated for this object as it is async. 153 | {%- else %} 154 | {%- call java::docstring(cons, 4) %} 155 | public {{ impl_class_name }}({% call java::arg_list(cons, true) -%}) {% match cons.throws_type() %}{% when Some(throwable) %}throws {{ throwable|type_name(ci, config) }}{% else %}{% endmatch %}{ 156 | this((Pointer){%- call java::to_ffi_call(cons) -%}); 157 | } 158 | {%- endif %} 159 | {%- when None %} 160 | {%- endmatch %} 161 | 162 | @Override 163 | public synchronized void close() { 164 | // Only allow a single call to this method. 165 | // TODO(uniffi): maybe we should log a warning if called more than once? 166 | if (this.wasDestroyed.compareAndSet(false, true)) { 167 | // This decrement always matches the initial count of 1 given at creation time. 168 | if (this.callCounter.decrementAndGet() == 0L) { 169 | cleanable.clean(); 170 | } 171 | } 172 | } 173 | 174 | public R callWithPointer(Function block) { 175 | // Check and increment the call counter, to keep the object alive. 176 | // This needs a compare-and-set retry loop in case of concurrent updates. 177 | long c; 178 | do { 179 | c = this.callCounter.get(); 180 | if (c == 0L) { 181 | throw new IllegalStateException("{{ impl_class_name }} object has already been destroyed"); 182 | } 183 | if (c == Long.MAX_VALUE) { 184 | throw new IllegalStateException("{{ impl_class_name }} call counter would overflow"); 185 | } 186 | } while (! this.callCounter.compareAndSet(c, c + 1L)); 187 | // Now we can safely do the method call without the pointer being freed concurrently. 188 | try { 189 | return block.apply(this.uniffiClonePointer()); 190 | } finally { 191 | // This decrement always matches the increment we performed above. 192 | if (this.callCounter.decrementAndGet() == 0L) { 193 | cleanable.clean(); 194 | } 195 | } 196 | } 197 | 198 | public void callWithPointer(Consumer block) { 199 | callWithPointer((Pointer p) -> { 200 | block.accept(p); 201 | return (Void)null; 202 | }); 203 | } 204 | 205 | private class UniffiCleanAction implements Runnable { 206 | private final Pointer pointer; 207 | 208 | public UniffiCleanAction(Pointer pointer) { 209 | this.pointer = pointer; 210 | } 211 | 212 | @Override 213 | public void run() { 214 | if (pointer != null) { 215 | UniffiHelpers.uniffiRustCall(status -> { 216 | UniffiLib.INSTANCE.{{ obj.ffi_object_free().name() }}(pointer, status); 217 | return null; 218 | }); 219 | } 220 | } 221 | } 222 | 223 | Pointer uniffiClonePointer() { 224 | return UniffiHelpers.uniffiRustCall(status -> { 225 | if (pointer == null) { 226 | throw new NullPointerException(); 227 | } 228 | return UniffiLib.INSTANCE.{{ obj.ffi_object_clone().name() }}(pointer, status); 229 | }); 230 | } 231 | 232 | {% for meth in obj.methods() -%} 233 | {%- call java::func_decl("public", "Override", meth, 4) %} 234 | {% endfor %} 235 | 236 | {%- for tm in obj.uniffi_traits() %} 237 | {%- match tm %} 238 | {% when UniffiTrait::Display { fmt } %} 239 | @Override 240 | public String toString() { 241 | return {{ fmt.return_type().unwrap()|lift_fn(config, ci) }}({% call java::to_ffi_call(fmt) %}); 242 | } 243 | {% when UniffiTrait::Eq { eq, ne } %} 244 | {# only equals used #} 245 | @Override 246 | public Boolean equals(Object other) { 247 | if (this === other) { 248 | return true; 249 | } 250 | if (!(other instanceof {{ impl_class_name}})) { 251 | return false; 252 | } 253 | return {{ eq.return_type().unwrap()|lift_fn(config, ci) }}({% call java::to_ffi_call(eq) %}); 254 | } 255 | {% when UniffiTrait::Hash { hash } %} 256 | @Override 257 | public Integer hashCode() { 258 | return {{ hash.return_type().unwrap()|lift_fn(config, ci) }}({%- call java::to_ffi_call(hash) %}).toInt(); 259 | } 260 | {%- else %} 261 | {%- endmatch %} 262 | {%- endfor %} 263 | 264 | {% if !obj.alternate_constructors().is_empty() -%} 265 | {% for cons in obj.alternate_constructors() -%} 266 | {% call java::func_decl("public static", "", cons, 4) %} 267 | {% endfor %} 268 | {% endif %} 269 | } 270 | 271 | {% if is_error %} 272 | package {{ config.package_name() }}; 273 | 274 | public class {{ impl_class_name }}ErrorHandler implements UniffiRustCallStatusErrorHandler<{{ impl_class_name }}> { 275 | @Override 276 | public {{ impl_class_name }} lift(RustBuffer.ByValue error_buf) { 277 | // Due to some mismatches in the ffi converter mechanisms, errors are a RustBuffer. 278 | var bb = error_buf.asByteBuffer(); 279 | if (bb == null) { 280 | throw new InternalException("?"); 281 | } 282 | return {{ ffi_converter_instance }}.read(bb); 283 | } 284 | } 285 | {% endif %} 286 | 287 | {%- if obj.has_callback_interface() %} 288 | {%- let vtable = obj.vtable().expect("trait interface should have a vtable") %} 289 | {%- let vtable_methods = obj.vtable_methods() %} 290 | {%- let ffi_init_callback = obj.ffi_init_callback() %} 291 | {% include "CallbackInterfaceImpl.java" %} 292 | {%- endif %} 293 | 294 | package {{ config.package_name() }}; 295 | 296 | import java.nio.ByteBuffer; 297 | import com.sun.jna.Pointer; 298 | 299 | public enum {{ ffi_converter_name }} implements FfiConverter<{{ type_name }}, Pointer> { 300 | INSTANCE; 301 | 302 | {%- if obj.has_callback_interface() %} 303 | public final UniffiHandleMap<{{ type_name }}> handleMap = new UniffiHandleMap<>(); 304 | {%- endif %} 305 | 306 | @Override 307 | public Pointer lower({{ type_name }} value) { 308 | {%- if obj.has_callback_interface() %} 309 | return new Pointer(handleMap.insert(value)); 310 | {%- else %} 311 | return value.uniffiClonePointer(); 312 | {%- endif %} 313 | } 314 | 315 | @Override 316 | public {{ type_name }} lift(Pointer value) { 317 | return new {{ impl_class_name }}(value); 318 | } 319 | 320 | @Override 321 | public {{ type_name }} read(ByteBuffer buf) { 322 | // The Rust code always writes pointers as 8 bytes, and will 323 | // fail to compile if they don't fit. 324 | return lift(new Pointer(buf.getLong())); 325 | } 326 | 327 | @Override 328 | public long allocationSize({{ type_name }} value) { 329 | return 8L; 330 | } 331 | 332 | @Override 333 | public void write({{ type_name }} value, ByteBuffer buf) { 334 | // The Rust code always expects pointers written as 8 bytes, 335 | // and will fail to compile if they don't fit. 336 | buf.putLong(Pointer.nativeValue(lower(value))); 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /tests/scripts/TestFixtureFutures/TestFixtureFutures.java: -------------------------------------------------------------------------------- 1 | import uniffi.fixture.futures.*; 2 | 3 | import java.text.MessageFormat; 4 | import java.util.concurrent.CompletableFuture; 5 | import java.util.concurrent.ExecutionException; 6 | import java.util.concurrent.Executors; 7 | import java.util.concurrent.ScheduledExecutorService; 8 | import java.util.concurrent.TimeUnit; 9 | 10 | public class TestFixtureFutures { 11 | private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); 12 | 13 | // emulating Kotlin's `delay` non-blocking sleep 14 | public static CompletableFuture delay(long milliseconds) { 15 | CompletableFuture f = new CompletableFuture<>(); 16 | scheduler.schedule(() -> f.complete(null), milliseconds, TimeUnit.MILLISECONDS); 17 | return f; 18 | } 19 | 20 | // runnable but rethrowing the exceptions CompletableFuture execution throws 21 | public interface FutureRunnable { 22 | void run() throws InterruptedException, ExecutionException; 23 | } 24 | 25 | static long nano_to_millis = 1_000_000; 26 | public static long measureTimeMillis(FutureRunnable r) { 27 | long startTimeNanos = System.nanoTime(); 28 | try { 29 | r.run(); 30 | } catch (Exception e) { 31 | assert false : "unexpected future run failure"; 32 | } 33 | long endTimeNanos = System.nanoTime(); 34 | long elapsedTimeMillis = (endTimeNanos - startTimeNanos) / nano_to_millis; 35 | 36 | return elapsedTimeMillis; 37 | } 38 | 39 | public static void assertReturnsImmediately(long actualTime, String testName) { 40 | // TODO(java): 4ms limit in Kotlin 41 | assert actualTime <= 40 : MessageFormat.format("unexpected {0} time: {1}ms", testName, actualTime); 42 | } 43 | 44 | public static void assertApproximateTime(long actualTime, int expectedTime, String testName) { 45 | assert actualTime >= expectedTime && actualTime <= expectedTime + 100 : MessageFormat.format("unexpected {0} time: {1}ms", testName, actualTime); 46 | } 47 | 48 | public static void main(String[] args) throws Exception { 49 | try { 50 | // init UniFFI to get good measurements after that 51 | { 52 | var time = measureTimeMillis(() -> { 53 | Futures.alwaysReady().get(); 54 | }); 55 | 56 | System.out.println(MessageFormat.format("init time: {0}ms", time)); 57 | } 58 | 59 | // Test `always_ready` 60 | { 61 | var time = measureTimeMillis(() -> { 62 | var result = Futures.alwaysReady().get(); 63 | assert result.equals(true); 64 | }); 65 | 66 | assertReturnsImmediately(time, "always_ready"); 67 | } 68 | 69 | // Test `void`. 70 | { 71 | var time = measureTimeMillis(() -> { 72 | var result = Futures._void().get(); 73 | 74 | assert result == null; 75 | }); 76 | 77 | assertReturnsImmediately(time, "void"); 78 | } 79 | 80 | // Test `sleep`. 81 | { 82 | var time = measureTimeMillis(() -> { 83 | Futures.sleep((short)200).get(); 84 | }); 85 | 86 | assertApproximateTime(time, 200, "sleep"); 87 | } 88 | 89 | // Test sequential futures. 90 | { 91 | var time = measureTimeMillis(() -> { 92 | var aliceResult = Futures.sayAfter((short)100, "Alice").get(); 93 | var bobResult = Futures.sayAfter((short)200, "Bob").get(); 94 | 95 | assert aliceResult.equals("Hello, Alice!"); 96 | assert bobResult.equals("Hello, Bob!"); 97 | }); 98 | 99 | assertApproximateTime(time, 300, "sequential future"); 100 | } 101 | 102 | // Test concurrent futures. 103 | { 104 | var time = measureTimeMillis(() -> { 105 | var alice = Futures.sayAfter((short)100, "Alice"); 106 | var bob = Futures.sayAfter((short)200, "Bob"); 107 | 108 | assert alice.get().equals("Hello, Alice!"); 109 | assert bob.get().equals("Hello, Bob!"); 110 | }); 111 | 112 | assertApproximateTime(time, 200, "concurrent future"); 113 | } 114 | 115 | // Test async methods. 116 | { 117 | var megaphone = Futures.newMegaphone(); 118 | var time = measureTimeMillis(() -> { 119 | var resultAlice = megaphone.sayAfter((short)200, "Alice").get(); 120 | 121 | assert resultAlice.equals("HELLO, ALICE!"); 122 | }); 123 | 124 | assertApproximateTime(time, 200, "async methods"); 125 | } 126 | 127 | { 128 | var megaphone = Futures.newMegaphone(); 129 | var time = measureTimeMillis(() -> { 130 | var resultAlice = Futures.sayAfterWithMegaphone(megaphone, (short)200, "Alice").get(); 131 | 132 | assert resultAlice.equals("HELLO, ALICE!"); 133 | }); 134 | 135 | assertApproximateTime(time, 200, "async methods"); 136 | } 137 | 138 | // Test async constructors 139 | { 140 | var megaphone = Megaphone.secondary().get(); 141 | assert megaphone.sayAfter((short)1, "hi").get().equals("HELLO, HI!"); 142 | } 143 | 144 | // Test async method returning optional object 145 | { 146 | var megaphone = Futures.asyncMaybeNewMegaphone(true).get(); 147 | assert megaphone != null; 148 | 149 | var not_megaphone = Futures.asyncMaybeNewMegaphone(false).get(); 150 | assert not_megaphone == null; 151 | } 152 | 153 | // Test async methods in trait interfaces 154 | { 155 | var traits = Futures.getSayAfterTraits(); 156 | var time = measureTimeMillis(() -> { 157 | var result1 = traits.get(0).sayAfter((short)100, "Alice").get(); 158 | var result2 = traits.get(1).sayAfter((short)100, "Bob").get(); 159 | 160 | assert result1.equals("Hello, Alice!"); 161 | assert result2.equals("Hello, Bob!"); 162 | }); 163 | 164 | assertApproximateTime(time, 200, "async trait methods"); 165 | } 166 | 167 | // Test async methods in UDL-defined trait interfaces 168 | { 169 | var traits = Futures.getSayAfterUdlTraits(); 170 | var time = measureTimeMillis(() -> { 171 | var result1 = traits.get(0).sayAfter((short)100, "Alice").get(); 172 | var result2 = traits.get(1).sayAfter((short)100, "Bob").get(); 173 | 174 | assert result1.equals("Hello, Alice!"); 175 | assert result2.equals("Hello, Bob!"); 176 | }); 177 | 178 | assertApproximateTime(time, 200, "async UDL methods"); 179 | } 180 | 181 | // Test foreign implemented async trait methods 182 | { 183 | class JavaAsyncParser implements AsyncParser { 184 | int completedDelays = 0; 185 | 186 | @Override 187 | public CompletableFuture asString(Integer delayMs, Integer value) { 188 | return TestFixtureFutures.delay((long)delayMs).thenApply(nothing -> { 189 | return value.toString(); 190 | }); 191 | } 192 | 193 | @Override 194 | public CompletableFuture tryFromString(Integer delayMs, String value) { 195 | return TestFixtureFutures.delay((long)delayMs).thenCompose((Void nothing) -> { 196 | CompletableFuture f = new CompletableFuture<>(); 197 | if (value.equals("force-unexpected-exception")) { 198 | f.completeExceptionally(new RuntimeException("UnexpectedException")); 199 | return f; 200 | } 201 | try { 202 | f.complete(Integer.parseInt(value)); 203 | } catch (NumberFormatException e) { 204 | f.completeExceptionally(new ParserException.NotAnInt()); 205 | } 206 | return f; 207 | }); 208 | } 209 | 210 | @Override 211 | public CompletableFuture delay(Integer delayMs) { 212 | return TestFixtureFutures.delay((long)delayMs).thenRun(() -> { 213 | completedDelays += 1; 214 | }); 215 | } 216 | 217 | @Override 218 | public CompletableFuture tryDelay(String delayMs) { 219 | try { 220 | var parsed = Long.parseLong(delayMs); 221 | return TestFixtureFutures.delay(parsed).thenRun(() -> { 222 | completedDelays += 1; 223 | }); 224 | } catch (NumberFormatException e) { 225 | var f = new CompletableFuture(); 226 | f.completeExceptionally(new ParserException.NotAnInt()); 227 | return f; 228 | } 229 | } 230 | } 231 | 232 | var traitObj = new JavaAsyncParser(); 233 | var startingHandleCount = UniffiAsyncHelpers.uniffiForeignFutureHandleCount(); 234 | assert startingHandleCount == 0 : MessageFormat.format("{0} starting handle count != 0", startingHandleCount); 235 | assert Futures.asStringUsingTrait(traitObj, 1, 42).get().equals("42"); 236 | assert Futures.tryFromStringUsingTrait(traitObj, 1, "42").get().equals(42); 237 | try { 238 | Futures.tryFromStringUsingTrait(traitObj, 1, "fourty-two").get(); 239 | throw new RuntimeException("Expected last statement to throw"); 240 | } catch (ExecutionException e) { 241 | if (e.getCause() instanceof ParserException.NotAnInt) { 242 | // Expected 243 | } else { 244 | throw e; 245 | } 246 | } 247 | try { 248 | Futures.tryFromStringUsingTrait(traitObj, 1, "force-unexpected-exception").get(); 249 | throw new RuntimeException("Expected last statement to throw"); 250 | } catch (ExecutionException e) { 251 | if (e.getCause() instanceof ParserException.UnexpectedException) { 252 | // Expected 253 | } else { 254 | throw e; 255 | } 256 | } 257 | Futures.delayUsingTrait(traitObj, 1).get(); 258 | try { 259 | Futures.tryDelayUsingTrait(traitObj, "one").get(); 260 | throw new RuntimeException("Expected last statement to throw"); 261 | } catch (ExecutionException e) { 262 | if (e.getCause() instanceof ParserException.NotAnInt) { 263 | // Expected 264 | } else { 265 | throw e; 266 | } 267 | } 268 | var completedDelaysBefore = traitObj.completedDelays; 269 | Futures.cancelDelayUsingTrait(traitObj, 50).get(); 270 | // sleep long enough so that the `delay()` call would finish if it wasn't cancelled. 271 | TestFixtureFutures.delay(200).get(); 272 | // If the task was cancelled, then completedDelays won't have increased 273 | assert traitObj.completedDelays == completedDelaysBefore : MessageFormat.format("{0} current delays != {1} delays before", traitObj.completedDelays, completedDelaysBefore); 274 | 275 | // Test that all handles were cleaned up 276 | var endingHandleCount = UniffiAsyncHelpers.uniffiForeignFutureHandleCount(); 277 | assert endingHandleCount == 0 : MessageFormat.format("{0} current handle count != 0", endingHandleCount); 278 | } 279 | 280 | // Test with the Tokio runtime. 281 | { 282 | var time = measureTimeMillis(() -> { 283 | var resultAlice = Futures.sayAfterWithTokio((short)200, "Alice").get(); 284 | 285 | assert resultAlice.equals("Hello, Alice (with Tokio)!"); 286 | }); 287 | 288 | assertApproximateTime(time, 200, "with tokio runtime"); 289 | } 290 | 291 | // Test fallible function/method 292 | { 293 | var time1 = measureTimeMillis(() -> { 294 | try { 295 | Futures.fallibleMe(false).get(); 296 | assert true; 297 | } catch (Exception e) { 298 | assert false; // should never be reached 299 | } 300 | }); 301 | 302 | System.out.print(MessageFormat.format("fallible function (with result): {0}ms", time1)); 303 | assert time1 < 100; 304 | System.out.println(" ... ok"); 305 | 306 | var time2 = measureTimeMillis(() -> { 307 | try { 308 | Futures.fallibleMe(true).get(); 309 | assert false; // should never be reached 310 | } catch (Exception e) { 311 | assert true; 312 | } 313 | }); 314 | 315 | System.out.print(MessageFormat.format("fallible function (with exception): {0}ms", time2)); 316 | assert time2 < 100; 317 | System.out.println(" ... ok"); 318 | 319 | var megaphone = Futures.newMegaphone(); 320 | 321 | var time3 = measureTimeMillis(() -> { 322 | try { 323 | megaphone.fallibleMe(false).get(); 324 | assert true; 325 | } catch (Exception e) { 326 | assert false; // should never be reached 327 | } 328 | }); 329 | 330 | System.out.print(MessageFormat.format("fallible method (with result): {0}ms", time3)); 331 | assert time3 < 100; 332 | System.out.println(" ... ok"); 333 | 334 | var time4 = measureTimeMillis(() -> { 335 | try { 336 | megaphone.fallibleMe(true).get(); 337 | assert false; // should never be reached 338 | } catch (Exception e) { 339 | assert true; 340 | } 341 | }); 342 | 343 | System.out.print(MessageFormat.format("fallible method (with exception): {0}ms", time4)); 344 | assert time4 < 100; 345 | System.out.println(" ... ok"); 346 | 347 | Futures.fallibleStruct(false).get(); 348 | try { 349 | Futures.fallibleStruct(true).get(); 350 | assert false; // should never be reached 351 | } catch (Exception e) { 352 | assert true; 353 | } 354 | } 355 | 356 | // Test record. 357 | { 358 | var time = measureTimeMillis(() -> { 359 | var result = Futures.newMyRecord("foo", 42).get(); 360 | 361 | assert result.a().equals("foo"); 362 | assert result.b() == 42; 363 | }); 364 | 365 | System.out.print(MessageFormat.format("record: {0}ms", time)); 366 | assert time < 100; 367 | System.out.println(" ... ok"); 368 | } 369 | 370 | // Test a broken sleep. 371 | { 372 | var time = measureTimeMillis(() -> { 373 | Futures.brokenSleep((short)100, (short)0).get(); // calls the waker twice immediately 374 | Futures.sleep((short)100).get(); // wait for possible failure 375 | 376 | Futures.brokenSleep((short)100, (short)100).get(); // calls the waker a second time after 1s 377 | Futures.sleep((short)200).get(); // wait for possible failure 378 | }); 379 | 380 | assertApproximateTime(time, 500, "broken sleep"); 381 | } 382 | 383 | // Test a future that uses a lock and that is cancelled. 384 | { 385 | var time = measureTimeMillis(() -> { 386 | var job = Futures.useSharedResource(new SharedResourceOptions((short)5000, (short)100)); 387 | 388 | // Wait some time to ensure the task has locked the shared resource 389 | TestFixtureFutures.delay(50).get(); 390 | // Cancel the job before the shared resource has been released. 391 | job.cancel(true); 392 | 393 | // Try accessing the shared resource again. The initial task should release the shared resource before the 394 | // timeout expires. 395 | Futures.useSharedResource(new SharedResourceOptions((short)0, (short)1000)).get(); 396 | }); 397 | 398 | System.out.println(MessageFormat.format("useSharedResource: {0}ms", time)); 399 | } 400 | 401 | // Test a future that uses a lock and that is not cancelled. 402 | { 403 | var time = measureTimeMillis(() -> { 404 | // spawn both at the same time so they contend for the resource 405 | var f1 = Futures.useSharedResource(new SharedResourceOptions((short)100, (short)1000)); 406 | var f2 = Futures.useSharedResource(new SharedResourceOptions((short)0, (short)1000)); 407 | 408 | f1.get(); 409 | f2.get(); 410 | }); 411 | 412 | System.out.println(MessageFormat.format("useSharedResource (not cancelled): {0}ms", time)); 413 | } 414 | } finally { 415 | // bring down the scheduler, if it's not shut down it'll hold the main thread open. 416 | scheduler.shutdown(); 417 | } 418 | } 419 | } 420 | --------------------------------------------------------------------------------