From 2abc1cbba098950166be672d1358ab0104336757 Mon Sep 17 00:00:00 2001 From: Murph Murphy Date: Wed, 16 Sep 2026 12:05:49 -0600 Subject: [PATCH] Fix name collisions, add test coverage for them --- CHANGELOG.md | 4 + Cargo.lock | 10 + Cargo.toml | 1 + fixtures/name-collisions/Cargo.toml | 13 ++ fixtures/name-collisions/src/lib.rs | 194 ++++++++++++++++++++ src/gen_java/mod.rs | 26 +++ src/templates/CallbackInterfaceImpl.java | 90 ++++----- src/templates/EnumTemplate.java | 10 +- src/templates/NamespaceLibraryTemplate.java | 14 +- src/templates/macros.java | 33 ++-- tests/scripts/TestNameCollisions.java | 130 +++++++++++++ tests/tests.rs | 1 + 12 files changed, 449 insertions(+), 77 deletions(-) create mode 100644 fixtures/name-collisions/Cargo.toml create mode 100644 fixtures/name-collisions/src/lib.rs create mode 100644 tests/scripts/TestNameCollisions.java diff --git a/CHANGELOG.md b/CHANGELOG.md index adc484b..60df7b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.2 + +- fix generated Java failing to compile when a Rust field or parameter name matched a name the generator used in the same scope, such as an enum variant field named `value` ([#63](https://github.com/IronCoreLabs/uniffi-bindgen-java/issues/63)). Enum and error variant field names, and function, method, constructor and callback-interface parameter names can no longer collide with anything the Java generator emits. Names uniffi-rs itself reserves on the Rust side, such as a field named `buf` or a callback-interface parameter named `uniffi_handle`, are rejected by the Rust derive before bindings are generated and remain unavailable. + ## 0.5.1 - adjust dependency requirements to allow consumption of `uniffi` patch bumps diff --git a/Cargo.lock b/Cargo.lock index 889149e..5e3e365 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1610,6 +1610,7 @@ dependencies = [ "uniffi-fixture-enum-types", "uniffi-fixture-ext-types", "uniffi-fixture-futures", + "uniffi-fixture-name-collisions", "uniffi-fixture-primitive-arrays", "uniffi-fixture-proc-macro", "uniffi-fixture-rename", @@ -1786,6 +1787,15 @@ dependencies = [ "uniffi", ] +[[package]] +name = "uniffi-fixture-name-collisions" +version = "0.1.0" +dependencies = [ + "async-trait", + "thiserror", + "uniffi", +] + [[package]] name = "uniffi-fixture-primitive-arrays" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 48a4ca8..b650f2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ uniffi-fixture-coverall = { git = "https://github.com/mozilla/uniffi-rs.git", br uniffi-fixture-enum-types = { git = "https://github.com/mozilla/uniffi-rs.git", branch = "release-v0.32.x" } uniffi-fixture-ext-types = { git = "https://github.com/mozilla/uniffi-rs.git", branch = "release-v0.32.x" } uniffi-fixture-futures = { git = "https://github.com/mozilla/uniffi-rs.git", branch = "release-v0.32.x" } +uniffi-fixture-name-collisions = { path = "fixtures/name-collisions" } uniffi-fixture-primitive-arrays = { path = "fixtures/primitive-arrays" } uniffi-fixture-proc-macro = { git = "https://github.com/mozilla/uniffi-rs.git", branch = "release-v0.32.x" } uniffi-fixture-rename = { git = "https://github.com/mozilla/uniffi-rs.git", branch = "release-v0.32.x" } diff --git a/fixtures/name-collisions/Cargo.toml b/fixtures/name-collisions/Cargo.toml new file mode 100644 index 0000000..d52ff8c --- /dev/null +++ b/fixtures/name-collisions/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "uniffi-fixture-name-collisions" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib", "lib"] +name = "uniffi_fixture_name_collisions" + +[dependencies] +uniffi = { git = "https://github.com/mozilla/uniffi-rs.git", branch = "release-v0.32.x" } +async-trait = "0.1" +thiserror = "2" diff --git a/fixtures/name-collisions/src/lib.rs b/fixtures/name-collisions/src/lib.rs new file mode 100644 index 0000000..f3133c2 --- /dev/null +++ b/fixtures/name-collisions/src/lib.rs @@ -0,0 +1,194 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! User-chosen identifiers that equal names the Java templates introduce in the same scope. +//! +//! Absent here, because uniffi-rs's own Rust derives and FFI callback signatures already reject +//! them: a field named `buf`; callback-interface parameters named `call_status`, +//! `uniffi_call_status`, `uniffi_handle`, `uniffi_out_return`, `uniffi_future_callback`, +//! `uniffi_callback_data` or `uniffi_out_dropped_callback`. + +use std::sync::Arc; + +uniffi::setup_scaffolding!("name_collisions"); + +/// `value` is the converter's parameter; `v1`/`v2` are its positional pattern bindings. +#[derive(Debug, Clone, PartialEq, uniffi::Enum)] +pub enum Shadowing { + Unit, + Named { value: String, v1: i32, v2: bool }, + Tuple(String, u64), +} + +#[uniffi::export] +fn roundtrip_enum(value: Shadowing) -> Shadowing { + value +} + +#[uniffi::export] +fn enum_named_value(buf: Shadowing) -> Option { + match buf { + Shadowing::Named { value, .. } => Some(value), + _ => None, + } +} + +/// `x` is the error converter's type-pattern binding. +#[derive(Debug, Clone, PartialEq, thiserror::Error, uniffi::Error)] +pub enum ShadowError { + #[error("named {value} {x}")] + Named { value: String, x: i32 }, +} + +#[uniffi::export] +fn throw_named(value: String, x: i32) -> Result<(), ShadowError> { + Err(ShadowError::Named { value, x }) +} + +/// Parameters named after the locals of the sync call path and the `UniffiLib` wrappers. +#[uniffi::export] +fn sync_fn( + uniffi_out_err: String, + status: String, + allocator: String, + it: String, + e: String, +) -> String { + format!("{uniffi_out_err}|{status}|{allocator}|{it}|{e}") +} + +/// Parameters named after the locals of the async call path. +#[uniffi::export] +async fn async_fn(uniffi_executor: String, it: String, uniffi_result: String) -> String { + format!("{uniffi_executor}|{it}|{uniffi_result}") +} + +#[derive(uniffi::Object)] +pub struct Holder { + tag: String, +} + +#[uniffi::export] +impl Holder { + #[uniffi::constructor] + fn new(uniffi_handle: String) -> Arc { + Arc::new(Self { tag: uniffi_handle }) + } + + fn sync_method(&self, uniffi_handle: String, uniffi_out_err: String) -> String { + format!("{}|{uniffi_handle}|{uniffi_out_err}", self.tag) + } + + async fn async_method( + &self, + uniffi_handle: String, + uniffi_executor: String, + it: String, + ) -> String { + format!("{}|{uniffi_handle}|{uniffi_executor}|{it}", self.tag) + } +} + +/// Parameters named after every local the callback-interface implementation class binds. +#[uniffi::export(with_foreign)] +#[async_trait::async_trait] +pub trait Shadow: Send + Sync { + fn sync_value( + &self, + uniffi_obj: String, + make_call: String, + write_return: String, + uniffi_value: String, + out_return: String, + lowered: String, + status: String, + ) -> String; + + fn sync_primitive(&self, uniffi_value: i32, out_return: i32) -> i32; + + fn sync_void(&self, nothing: String, status: String); + + fn sync_throws(&self, e: String, status: String) -> Result; + + async fn async_value( + &self, + uniffi_completion_descriptor: String, + uniffi_handle_success: String, + return_value: String, + uniffi_result: String, + global_callback: String, + mh: String, + t: String, + uniffi_handle_error: String, + status: String, + lowered: String, + ) -> String; + + async fn async_primitive(&self, return_value: i32, uniffi_result: i32) -> i32; + + async fn async_void(&self, nothing: String, return_value: String); + + async fn async_throws(&self, e: String, t: String) -> Result; +} + +#[uniffi::export] +fn call_sync_value(shadow: Arc, prefix: String) -> String { + shadow.sync_value( + format!("{prefix}obj"), + format!("{prefix}call"), + format!("{prefix}ret"), + format!("{prefix}val"), + format!("{prefix}out"), + format!("{prefix}low"), + format!("{prefix}status"), + ) +} + +#[uniffi::export] +fn call_sync_primitive(shadow: Arc, a: i32, b: i32) -> i32 { + shadow.sync_primitive(a, b) +} + +#[uniffi::export] +fn call_sync_void(shadow: Arc, nothing: String) { + shadow.sync_void(nothing, "status".to_string()) +} + +#[uniffi::export] +fn call_sync_throws(shadow: Arc, e: String) -> Result { + shadow.sync_throws(e, "status".to_string()) +} + +#[uniffi::export] +async fn call_async_value(shadow: Arc, prefix: String) -> String { + shadow + .async_value( + format!("{prefix}desc"), + format!("{prefix}success"), + format!("{prefix}ret"), + format!("{prefix}result"), + format!("{prefix}global"), + format!("{prefix}mh"), + format!("{prefix}t"), + format!("{prefix}error"), + format!("{prefix}status"), + format!("{prefix}low"), + ) + .await +} + +#[uniffi::export] +async fn call_async_primitive(shadow: Arc, a: i32, b: i32) -> i32 { + shadow.async_primitive(a, b).await +} + +#[uniffi::export] +async fn call_async_void(shadow: Arc, nothing: String) { + shadow.async_void(nothing, "ret".to_string()).await +} + +#[uniffi::export] +async fn call_async_throws(shadow: Arc, e: String) -> Result { + shadow.async_throws(e, "t".to_string()).await +} diff --git a/src/gen_java/mod.rs b/src/gen_java/mod.rs index 827728b..296ddf9 100644 --- a/src/gen_java/mod.rs +++ b/src/gen_java/mod.rs @@ -849,6 +849,9 @@ impl JavaCodeOracle { } /// Get the idiomatic Java rendering of a variable name. + /// + /// Output is `[a-z][A-Za-z0-9]*`, or `_` followed by a Java keyword. Templates rely on this: + /// a generated local named `_x` with `x` not a keyword can never equal a user's argument name. pub fn var_name(&self, nm: &str) -> String { fixup_keyword(self.var_name_raw(nm)) } @@ -2172,6 +2175,29 @@ mod tests { RecordMetadata, TraitKind, TraitMethodMetadata, Type, VariantMetadata, }; + #[test] + fn var_name_never_yields_underscore_prefixed_non_keyword() { + for input in [ + "_status", + "__uniffi_handle", + "_", + "uniffi_handle", + "_uniffiHandle", + "make_call", + "_allocator", + "it", + ] { + let name = JavaCodeOracle.var_name(input); + let rest = name.strip_prefix('_'); + assert!( + rest.is_none_or(|rest| KEYWORDS.contains(rest)), + "{input:?} rendered as {name:?}, which a template-owned `_` local could equal" + ); + } + assert_eq!(JavaCodeOracle.var_name("int"), "_int"); + assert_eq!(JavaCodeOracle.var_name("uniffi_handle"), "uniffiHandle"); + } + #[test] fn error_variant_holding_an_object_is_closeable() { let mut group = test_group(); diff --git a/src/templates/CallbackInterfaceImpl.java b/src/templates/CallbackInterfaceImpl.java index a8b1824..84ad014 100644 --- a/src/templates/CallbackInterfaceImpl.java +++ b/src/templates/CallbackInterfaceImpl.java @@ -36,15 +36,15 @@ public static final class {{ callback_class }}Callback implements {{ ffi_callbac {{ arg.type_().borrow()|ffi_type_name(config, ci) }} {{ arg.name().borrow()|var_name }}{% if !loop.last || (loop.last && ffi_callback.has_rust_call_status_arg()) %},{% endif %} {%- endfor -%} {%- if ffi_callback.has_rust_call_status_arg() -%} - java.lang.foreign.MemorySegment uniffiCallStatus + java.lang.foreign.MemorySegment _uniffiCallStatus {%- endif -%} ) { {%- if ffi_callback.has_rust_call_status_arg() %} - uniffiCallStatus = uniffiCallStatus.reinterpret(UniffiRustCallStatus.LAYOUT.byteSize()); + _uniffiCallStatus = _uniffiCallStatus.reinterpret(UniffiRustCallStatus.LAYOUT.byteSize()); {%- endif %} - var uniffiObj = {{ ffi_converter_name }}.INSTANCE.handleMap.get(uniffiHandle); - {% if !meth.is_async() && meth.throws_type().is_some() %}java.util.concurrent.Callable{% else %}java.util.function.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 %}java.lang.Void{% endmatch %}{% endif %}> makeCall = () -> { - {% if meth.return_type().is_some() || meth.is_async() %}return {% endif %}uniffiObj.{{ meth.name()|fn_name() }}( + var _uniffiObj = {{ ffi_converter_name }}.INSTANCE.handleMap.get(uniffiHandle); + {% if !meth.is_async() && meth.throws_type().is_some() %}java.util.concurrent.Callable{% else %}java.util.function.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 %}java.lang.Void{% endmatch %}{% endif %}> _uniffiMakeCall = () -> { + {% if meth.return_type().is_some() || meth.is_async() %}return {% endif %}_uniffiObj.{{ meth.name()|fn_name() }}( {%- for arg in meth.arguments() %} {{ arg|lift_fn(config, ci) }}({{ arg.name()|var_name }}){% if !loop.last %},{% endif %} {%- endfor %} @@ -55,29 +55,29 @@ public static final class {{ callback_class }}Callback implements {{ ffi_callbac {%- match meth.return_type() %} {%- when Some(return_type) %} {%- let ffi_return_type = return_type|ffi_type %} - java.util.function.Consumer<{{ return_type|type_name(ci, config)}}> writeReturn = ({{ return_type|type_name(ci, config) }} uniffiValue) -> { + java.util.function.Consumer<{{ return_type|type_name(ci, config)}}> _uniffiWriteReturn = ({{ return_type|type_name(ci, config) }} _uniffiValue) -> { {%- if ffi_return_type.borrow()|ffi_type_is_embedded_struct %} - java.lang.foreign.MemorySegment outReturn = uniffiOutReturn.reinterpret({{ ffi_return_type.borrow()|ffi_struct_type_name }}.LAYOUT.byteSize()); - java.lang.foreign.MemorySegment lowered = {{ return_type|lower_fn(config, ci) }}(uniffiValue); - java.lang.foreign.MemorySegment.copy(lowered, 0, outReturn, 0, {{ ffi_return_type.borrow()|ffi_struct_type_name }}.LAYOUT.byteSize()); + java.lang.foreign.MemorySegment _uniffiOut = uniffiOutReturn.reinterpret({{ ffi_return_type.borrow()|ffi_struct_type_name }}.LAYOUT.byteSize()); + java.lang.foreign.MemorySegment _uniffiLowered = {{ return_type|lower_fn(config, ci) }}(_uniffiValue); + java.lang.foreign.MemorySegment.copy(_uniffiLowered, 0, _uniffiOut, 0, {{ ffi_return_type.borrow()|ffi_struct_type_name }}.LAYOUT.byteSize()); {%- else %} - java.lang.foreign.MemorySegment outReturn = uniffiOutReturn.reinterpret({{ ffi_return_type.borrow()|ffi_value_layout }}.byteSize()); - outReturn.set({{ ffi_return_type.borrow()|ffi_value_layout_unaligned }}, 0, {{ return_type|lower_fn(config, ci) }}(uniffiValue)); + java.lang.foreign.MemorySegment _uniffiOut = uniffiOutReturn.reinterpret({{ ffi_return_type.borrow()|ffi_value_layout }}.byteSize()); + _uniffiOut.set({{ ffi_return_type.borrow()|ffi_value_layout_unaligned }}, 0, {{ return_type|lower_fn(config, ci) }}(_uniffiValue)); {%- endif %} }; {%- when None %} - java.util.function.Consumer writeReturn = (nothing) -> {}; + java.util.function.Consumer _uniffiWriteReturn = (_uniffiNothing) -> {}; {%- endmatch %} {%- match meth.throws_type() %} {%- when None %} - UniffiHelpers.uniffiTraitInterfaceCall(uniffiCallStatus, makeCall, writeReturn); + UniffiHelpers.uniffiTraitInterfaceCall(_uniffiCallStatus, _uniffiMakeCall, _uniffiWriteReturn); {%- when Some(error_type) %} UniffiHelpers.uniffiTraitInterfaceCallWithError( - uniffiCallStatus, - makeCall, - writeReturn, - ({{error_type|type_name(ci, config) }} e) -> { return {{ error_type|lower_fn(config, ci) }}(e); }, + _uniffiCallStatus, + _uniffiMakeCall, + _uniffiWriteReturn, + ({{error_type|type_name(ci, config) }} _uniffiErr) -> { return {{ error_type|lower_fn(config, ci) }}(_uniffiErr); }, {{error_type|type_name(ci, config)}}.class ); {%- endmatch %} @@ -86,60 +86,60 @@ public static final class {{ callback_class }}Callback implements {{ ffi_callbac {#- Async callback interface method -#} {%- let result_struct_name = meth.foreign_future_ffi_result_struct().name()|ffi_struct_name %} // The completion callback always has signature: (long callbackData, java.lang.foreign.MemorySegment result) -> void - java.lang.foreign.FunctionDescriptor uniffiCompletionDescriptor = java.lang.foreign.FunctionDescriptor.ofVoid( + java.lang.foreign.FunctionDescriptor _uniffiCompletionDescriptor = java.lang.foreign.FunctionDescriptor.ofVoid( java.lang.foreign.ValueLayout.JAVA_LONG, {{ result_struct_name }}.LAYOUT ); - java.util.function.Consumer<{{ meth|async_inner_return_type(ci, config) }}> uniffiHandleSuccess = ({% match meth.return_type() %}{%- when Some(return_type) %}returnValue{%- when None %}nothing{% endmatch %}) -> { - java.lang.foreign.MemorySegment uniffiResult = java.lang.foreign.Arena.ofAuto().allocate({{ result_struct_name }}.LAYOUT); + java.util.function.Consumer<{{ meth|async_inner_return_type(ci, config) }}> _uniffiHandleSuccess = ({% match meth.return_type() %}{%- when Some(return_type) %}_uniffiReturnValue{%- when None %}_uniffiNothing{% endmatch %}) -> { + java.lang.foreign.MemorySegment _uniffiResult = java.lang.foreign.Arena.ofAuto().allocate({{ result_struct_name }}.LAYOUT); {%- match meth.return_type() %} {%- when Some(return_type) %} {%- let ffi_return_type = return_type|ffi_type %} {%- if ffi_return_type.borrow()|ffi_type_is_embedded_struct %} - java.lang.foreign.MemorySegment lowered = {{ return_type|lower_fn(config, ci) }}(returnValue); - {{ result_struct_name }}.setreturnValue(uniffiResult, lowered); + java.lang.foreign.MemorySegment _uniffiLowered = {{ return_type|lower_fn(config, ci) }}(_uniffiReturnValue); + {{ result_struct_name }}.setreturnValue(_uniffiResult, _uniffiLowered); {%- else %} - {{ result_struct_name }}.setreturnValue(uniffiResult, {{ return_type|lower_fn(config, ci) }}(returnValue)); + {{ result_struct_name }}.setreturnValue(_uniffiResult, {{ return_type|lower_fn(config, ci) }}(_uniffiReturnValue)); {%- endif %} {%- when None %} {%- endmatch %} // Set status to success (zeroed out already) try { // Convert the upcall java.lang.foreign.MemorySegment to a globally-scoped one for cross-thread use - java.lang.foreign.MemorySegment globalCallback = java.lang.foreign.MemorySegment.ofAddress(uniffiFutureCallback.address()); - java.lang.invoke.MethodHandle mh = java.lang.foreign.Linker.nativeLinker().downcallHandle( - globalCallback, uniffiCompletionDescriptor); - mh.invokeExact(uniffiCallbackData, uniffiResult); - } catch (Throwable t) { - throw new AssertionError("invokeExact failed", t); + java.lang.foreign.MemorySegment _uniffiGlobalCallback = java.lang.foreign.MemorySegment.ofAddress(uniffiFutureCallback.address()); + java.lang.invoke.MethodHandle _uniffiMh = java.lang.foreign.Linker.nativeLinker().downcallHandle( + _uniffiGlobalCallback, _uniffiCompletionDescriptor); + _uniffiMh.invokeExact(uniffiCallbackData, _uniffiResult); + } catch (Throwable _uniffiThrowable) { + throw new AssertionError("invokeExact failed", _uniffiThrowable); } }; - java.util.function.Consumer uniffiHandleError = (callStatus) -> { - java.lang.foreign.MemorySegment uniffiResult = java.lang.foreign.Arena.ofAuto().allocate({{ result_struct_name }}.LAYOUT); - {{ result_struct_name }}.setcallStatus(uniffiResult, callStatus); + java.util.function.Consumer _uniffiHandleError = (_uniffiErrStatus) -> { + java.lang.foreign.MemorySegment _uniffiResult = java.lang.foreign.Arena.ofAuto().allocate({{ result_struct_name }}.LAYOUT); + {{ result_struct_name }}.setcallStatus(_uniffiResult, _uniffiErrStatus); try { - java.lang.foreign.MemorySegment globalCallback = java.lang.foreign.MemorySegment.ofAddress(uniffiFutureCallback.address()); - java.lang.invoke.MethodHandle mh = java.lang.foreign.Linker.nativeLinker().downcallHandle( - globalCallback, uniffiCompletionDescriptor); - mh.invokeExact(uniffiCallbackData, uniffiResult); - } catch (Throwable t) { - throw new AssertionError("invokeExact failed", t); + java.lang.foreign.MemorySegment _uniffiGlobalCallback = java.lang.foreign.MemorySegment.ofAddress(uniffiFutureCallback.address()); + java.lang.invoke.MethodHandle _uniffiMh = java.lang.foreign.Linker.nativeLinker().downcallHandle( + _uniffiGlobalCallback, _uniffiCompletionDescriptor); + _uniffiMh.invokeExact(uniffiCallbackData, _uniffiResult); + } catch (Throwable _uniffiThrowable) { + throw new AssertionError("invokeExact failed", _uniffiThrowable); } }; {%- match meth.throws_type() %} {%- when None %} UniffiAsyncHelpers.uniffiTraitInterfaceCallAsync( - makeCall, - uniffiHandleSuccess, - uniffiHandleError, + _uniffiMakeCall, + _uniffiHandleSuccess, + _uniffiHandleError, uniffiOutDroppedCallback ); {%- when Some(error_type) %} UniffiAsyncHelpers.uniffiTraitInterfaceCallAsyncWithError( - makeCall, - uniffiHandleSuccess, - uniffiHandleError, - ({{error_type|type_name(ci, config) }} e) -> {{ error_type|lower_fn(config, ci) }}(e), + _uniffiMakeCall, + _uniffiHandleSuccess, + _uniffiHandleError, + ({{error_type|type_name(ci, config) }} _uniffiErr) -> {{ error_type|lower_fn(config, ci) }}(_uniffiErr), {{ error_type|type_name(ci, config)}}.class, uniffiOutDroppedCallback ); diff --git a/src/templates/EnumTemplate.java b/src/templates/EnumTemplate.java index f38b26d..9bc5ab7 100644 --- a/src/templates/EnumTemplate.java +++ b/src/templates/EnumTemplate.java @@ -158,14 +158,16 @@ public enum {{ e|ffi_converter_name}} implements FfiConverterRustBuffer<{{ type_ }; } + {#- Pattern bindings are positional so no user field name enters this scope; a field named + `value` or `buf` would otherwise clash with the converter's own parameters. -#} @Override public long allocationSize({{ type_name }} value) { return switch (value) { {%- for variant in e.variants() %} - case {{ type_name }}.{{ variant|type_name(ci, config) }}({%- for field in variant.fields() %}var {% call java::field_name(field, loop.index) %}{% endcall -%}{% if !loop.last%}, {% endif %}{% endfor %}) -> + case {{ type_name }}.{{ variant|type_name(ci, config) }}({%- for field in variant.fields() %}var v{{ loop.index }}{% if !loop.last%}, {% endif %}{% endfor %}) -> (4L {%- for field in variant.fields() %} - + {{ field|allocation_size_fn(config, ci) }}({%- call java::field_name(field, loop.index) %}{% endcall -%}) + + {{ field|allocation_size_fn(config, ci) }}(v{{ loop.index }}) {%- endfor %}); {%- endfor %} }; @@ -175,10 +177,10 @@ public long allocationSize({{ type_name }} value) { public void write({{ type_name }} value, java.nio.ByteBuffer buf) { switch (value) { {%- for variant in e.variants() %} - case {{ type_name }}.{{ variant|type_name(ci, config) }}({%- for field in variant.fields() %}var {% call java::field_name(field, loop.index) %}{% endcall -%}{% if !loop.last%}, {% endif %}{% endfor %}) -> { + case {{ type_name }}.{{ variant|type_name(ci, config) }}({%- for field in variant.fields() %}var v{{ loop.index }}{% if !loop.last%}, {% endif %}{% endfor %}) -> { buf.putInt({{ loop.index }}); {%- for field in variant.fields() %} - {{ field|write_fn(config, ci) }}({%- call java::field_name(field, loop.index) %}{% endcall -%}, buf); + {{ field|write_fn(config, ci) }}(v{{ loop.index }}, buf); {%- endfor %} } {%- endfor %} diff --git a/src/templates/NamespaceLibraryTemplate.java b/src/templates/NamespaceLibraryTemplate.java index 356a78a..1c5f414 100644 --- a/src/templates/NamespaceLibraryTemplate.java +++ b/src/templates/NamespaceLibraryTemplate.java @@ -57,7 +57,7 @@ public interface Fn { {{ arg.type_().borrow()|ffi_type_name(config, ci) }} {{ arg.name().borrow()|var_name }}{% if !loop.last %},{% endif %} {%- endfor -%} {%- if callback.has_rust_call_status_arg() -%}{% if callback.arguments().len() != 0 %},{% endif %} - java.lang.foreign.MemorySegment uniffiCallStatus + java.lang.foreign.MemorySegment _uniffiCallStatus {%- endif -%} ); } @@ -158,26 +158,26 @@ private static java.lang.invoke.MethodHandle findDowncallHandle(String name, jav {%- if return_type|ffi_type_is_struct %} private static final java.lang.invoke.MethodHandle MH_{{ func.name() }} = findDowncallHandle("{{ func.name() }}", java.lang.foreign.FunctionDescriptor.of({{ return_type|ffi_value_layout }}{% for arg in func.arguments() %}, {{ arg.type_().borrow()|ffi_value_layout }}{% endfor %}{% if func.has_rust_call_status_arg() %}, java.lang.foreign.ValueLayout.ADDRESS{% endif %})); - static java.lang.foreign.MemorySegment {{ func.name() }}(java.lang.foreign.SegmentAllocator _allocator{% for arg in func.arguments() %}, {{ arg.type_().borrow()|ffi_type_name(config, ci) }} {{ arg.name()|var_name }}{% endfor %}{% if func.has_rust_call_status_arg() %}, java.lang.foreign.MemorySegment uniffiOutErr{% endif %}) { + static java.lang.foreign.MemorySegment {{ func.name() }}(java.lang.foreign.SegmentAllocator _allocator{% for arg in func.arguments() %}, {{ arg.type_().borrow()|ffi_type_name(config, ci) }} {{ arg.name()|var_name }}{% endfor %}{% if func.has_rust_call_status_arg() %}, java.lang.foreign.MemorySegment _uniffiOutErr{% endif %}) { try { - return (java.lang.foreign.MemorySegment) MH_{{ func.name() }}.invokeExact(_allocator{% for arg in func.arguments() %}, {{ arg.name()|var_name }}{% endfor %}{% if func.has_rust_call_status_arg() %}, uniffiOutErr{% endif %}); + return (java.lang.foreign.MemorySegment) MH_{{ func.name() }}.invokeExact(_allocator{% for arg in func.arguments() %}, {{ arg.name()|var_name }}{% endfor %}{% if func.has_rust_call_status_arg() %}, _uniffiOutErr{% endif %}); } catch (Throwable _ex) { throw new AssertionError("invokeExact failed", _ex); } } {%- else %} private static final java.lang.invoke.MethodHandle MH_{{ func.name() }} = findDowncallHandle("{{ func.name() }}", java.lang.foreign.FunctionDescriptor.of({{ return_type|ffi_value_layout }}{% for arg in func.arguments() %}, {{ arg.type_().borrow()|ffi_value_layout }}{% endfor %}{% if func.has_rust_call_status_arg() %}, java.lang.foreign.ValueLayout.ADDRESS{% endif %})); - static {{ return_type|ffi_type_name(config, ci) }} {{ func.name() }}({% for arg in func.arguments() %}{{ arg.type_().borrow()|ffi_type_name(config, ci) }} {{ arg.name()|var_name }}{% if !loop.last %}, {% endif %}{% endfor %}{% if func.has_rust_call_status_arg() %}{% if func.arguments().len() != 0 %}, {% endif %}java.lang.foreign.MemorySegment uniffiOutErr{% endif %}) { + static {{ return_type|ffi_type_name(config, ci) }} {{ func.name() }}({% for arg in func.arguments() %}{{ arg.type_().borrow()|ffi_type_name(config, ci) }} {{ arg.name()|var_name }}{% if !loop.last %}, {% endif %}{% endfor %}{% if func.has_rust_call_status_arg() %}{% if func.arguments().len() != 0 %}, {% endif %}java.lang.foreign.MemorySegment _uniffiOutErr{% endif %}) { try { - return {{ return_type|ffi_invoke_exact_cast }}MH_{{ func.name() }}.invokeExact({% for arg in func.arguments() %}{{ arg.name()|var_name }}{% if !loop.last %}, {% endif %}{% endfor %}{% if func.has_rust_call_status_arg() %}{% if func.arguments().len() != 0 %}, {% endif %}uniffiOutErr{% endif %}); + return {{ return_type|ffi_invoke_exact_cast }}MH_{{ func.name() }}.invokeExact({% for arg in func.arguments() %}{{ arg.name()|var_name }}{% if !loop.last %}, {% endif %}{% endfor %}{% if func.has_rust_call_status_arg() %}{% if func.arguments().len() != 0 %}, {% endif %}_uniffiOutErr{% endif %}); } catch (Throwable _ex) { throw new AssertionError("invokeExact failed", _ex); } } {%- endif %} {%- when None %} private static final java.lang.invoke.MethodHandle MH_{{ func.name() }} = findDowncallHandle("{{ func.name() }}", java.lang.foreign.FunctionDescriptor.ofVoid({% for arg in func.arguments() %}{{ arg.type_().borrow()|ffi_value_layout }}{% if !loop.last %}, {% endif %}{% endfor %}{% if func.has_rust_call_status_arg() %}{% if func.arguments().len() != 0 %}, {% endif %}java.lang.foreign.ValueLayout.ADDRESS{% endif %})); - static void {{ func.name() }}({% for arg in func.arguments() %}{{ arg.type_().borrow()|ffi_type_name(config, ci) }} {{ arg.name()|var_name }}{% if !loop.last %}, {% endif %}{% endfor %}{% if func.has_rust_call_status_arg() %}{% if func.arguments().len() != 0 %}, {% endif %}java.lang.foreign.MemorySegment uniffiOutErr{% endif %}) { + static void {{ func.name() }}({% for arg in func.arguments() %}{{ arg.type_().borrow()|ffi_type_name(config, ci) }} {{ arg.name()|var_name }}{% if !loop.last %}, {% endif %}{% endfor %}{% if func.has_rust_call_status_arg() %}{% if func.arguments().len() != 0 %}, {% endif %}java.lang.foreign.MemorySegment _uniffiOutErr{% endif %}) { try { - MH_{{ func.name() }}.invokeExact({% for arg in func.arguments() %}{{ arg.name()|var_name }}{% if !loop.last %}, {% endif %}{% endfor %}{% if func.has_rust_call_status_arg() %}{% if func.arguments().len() != 0 %}, {% endif %}uniffiOutErr{% endif %}); + MH_{{ func.name() }}.invokeExact({% for arg in func.arguments() %}{{ arg.name()|var_name }}{% if !loop.last %}, {% endif %}{% endfor %}{% if func.has_rust_call_status_arg() %}{% if func.arguments().len() != 0 %}, {% endif %}_uniffiOutErr{% endif %}); } catch (Throwable _ex) { throw new AssertionError("invokeExact failed", _ex); } } {%- endmatch %} diff --git a/src/templates/macros.java b/src/templates/macros.java index 4804d37..f97776c 100644 --- a/src/templates/macros.java +++ b/src/templates/macros.java @@ -1,13 +1,15 @@ {# -// Template to call into rust. Used in several places. -// Variable names in `arg_list` should match up with arg lists -// passed to rust via `arg_list_lowered` +// Template to call into rust. Variable names in `arg_list` should match up with arg lists +// passed to rust via `arg_list_lowered`. +// +// Generated locals sharing a scope with user argument names start with `_`; the invariant +// is on `JavaCodeOracle::var_name`. #} {%- macro to_ffi_call(func) -%} {%- match func.self_type() %} {%- when Some with (Type::Object { .. }) %} - callWithHandle(uniffiHandle -> { + callWithHandle(_uniffiHandle -> { try { {% if func.return_type().is_some() %} return {%- call to_raw_ffi_call(func) %}{% endcall %}; @@ -58,7 +60,7 @@ {%- when None %} {%- endmatch %} {%- match func.self_type() %} - {%- when Some with (Type::Object { .. }) %}uniffiHandle, + {%- when Some with (Type::Object { .. }) %}_uniffiHandle, {%- when Some(t) %}{{ t|lower_fn(config, ci) }}(this), {%- when None %} {%- endmatch %} @@ -88,7 +90,7 @@ {#- With-executor overload - does the actual async work -#} {{ func_decl }} java.util.concurrent.CompletableFuture<{% match callable.return_type() -%}{%- when Some with (return_type) -%}{{ return_type|boxed_type_name(ci, config) }}{%- when None %}java.lang.Void{%- endmatch %}> {{ callable.name()|fn_name }}( - {%- call arg_list(callable, !callable.self_type().is_some()) %}{% endcall -%}{% if !callable.arguments().is_empty() %}, {% endif %}java.util.concurrent.Executor uniffiExecutor + {%- call arg_list(callable, !callable.self_type().is_some()) %}{% endcall -%}{% if !callable.arguments().is_empty() %}, {% endif %}java.util.concurrent.Executor _uniffiExecutor ){ return {% call call_async(callable) %}{% endcall %}; } @@ -129,12 +131,12 @@ {%- macro call_async(callable) -%} UniffiAsyncHelpers.uniffiRustCallAsync( - uniffiExecutor, + _uniffiExecutor, {%- match callable.self_type() %} {%- when Some with (Type::Object { .. }) %} - callWithHandle(uniffiHandle -> { + callWithHandle(_uniffiHandle -> { return UniffiLib.{{ callable.ffi_func().name() }}( - uniffiHandle{% if callable.arguments().len() != 0 %},{% endif %} + _uniffiHandle{% if callable.arguments().len() != 0 %},{% endif %} {% call arg_list_lowered(callable) %}{% endcall %} ); }), @@ -152,7 +154,7 @@ // lift function {%- match callable.return_type() %} {%- when Some(return_type) %} - (it) -> {{ return_type|lift_fn(config, ci) }}(it), + (_uniffiResult) -> {{ return_type|lift_fn(config, ci) }}(_uniffiResult), {%- when None %} () -> {}, {%- endmatch %} @@ -205,17 +207,6 @@ {%- endfor %} {%- endmacro %} -{#- -// Arglist as used in the UniffiLib function declarations. -// Note unfiltered name but ffi_type_name filters. --#} -{%- macro arg_list_ffi_decl(func) %} - {%- for arg in func.arguments() %} - {{- arg.type_().borrow()|ffi_type_name(config, ci) }} {{arg.name()|var_name -}}{%- if !loop.last %}, {% endif -%} - {%- endfor %} - {%- if func.has_rust_call_status_arg() %}{% if func.arguments().len() != 0 %}, {% endif %}java.lang.foreign.MemorySegment uniffi_out_errmk{% endif %} -{%- endmacro -%} - {% macro field_name(field, field_num) %} {{- field|field_java_name(field_num) -}} {%- endmacro %} diff --git a/tests/scripts/TestNameCollisions.java b/tests/scripts/TestNameCollisions.java new file mode 100644 index 0000000..2113dfa --- /dev/null +++ b/tests/scripts/TestNameCollisions.java @@ -0,0 +1,130 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import uniffi.name_collisions.*; + +public class TestNameCollisions { + static class JavaShadow implements Shadow { + String lastVoid; + String lastAsyncVoid; + + @Override + public String syncValue(String uniffiObj, String makeCall, String writeReturn, String uniffiValue, + String outReturn, String lowered, String status) { + return String.join(",", uniffiObj, makeCall, writeReturn, uniffiValue, outReturn, lowered, status); + } + + @Override + public int syncPrimitive(int uniffiValue, int outReturn) { + return uniffiValue * 10 + outReturn; + } + + @Override + public void syncVoid(String nothing, String status) { + lastVoid = nothing + "," + status; + } + + @Override + public String syncThrows(String e, String status) throws ShadowException { + if (e.equals("throw")) { + throw new ShadowException.Named("value", 7); + } + return e + "," + status; + } + + @Override + public CompletableFuture asyncValue(String uniffiCompletionDescriptor, String uniffiHandleSuccess, + String returnValue, String uniffiResult, String globalCallback, + String mh, String t, String uniffiHandleError, String status, + String lowered) { + return CompletableFuture.completedFuture(String.join(",", uniffiCompletionDescriptor, uniffiHandleSuccess, + returnValue, uniffiResult, globalCallback, mh, t, uniffiHandleError, status, lowered)); + } + + @Override + public CompletableFuture asyncPrimitive(int returnValue, int uniffiResult) { + return CompletableFuture.completedFuture(returnValue * 10 + uniffiResult); + } + + @Override + public CompletableFuture asyncVoid(String nothing, String returnValue) { + lastAsyncVoid = nothing + "," + returnValue; + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture asyncThrows(String e, String t) { + if (e.equals("throw")) { + CompletableFuture f = new CompletableFuture<>(); + f.completeExceptionally(new ShadowException.Named("value", 9)); + return f; + } + return CompletableFuture.completedFuture(e + "," + t); + } + } + + public static void main(String[] args) throws Exception { + // Enum variant fields named after the converter's parameters and pattern bindings. + Shadowing named = new Shadowing.Named("v", 3, true); + Shadowing back = NameCollisions.roundtripEnum(named); + assert back instanceof Shadowing.Named : "expected Named variant back"; + Shadowing.Named n = (Shadowing.Named) back; + assert n.value().equals("v") : "value field"; + assert n.v1() == 3 : "v1 field"; + assert n.v2() : "v2 field"; + assert NameCollisions.enumNamedValue(named).equals("v") : "enumNamedValue"; + assert NameCollisions.roundtripEnum(new Shadowing.Unit()) instanceof Shadowing.Unit : "unit variant"; + Shadowing.Tuple tuple = (Shadowing.Tuple) NameCollisions.roundtripEnum(new Shadowing.Tuple("t", 5L)); + assert tuple.v1().equals("t") && tuple.v2() == 5L : "tuple variant"; + assert NameCollisions.enumNamedValue(new Shadowing.Unit()) == null : "unit has no value"; + + // Error variant fields named after the converter's parameter and binding. + try { + NameCollisions.throwNamed("value", 42); + assert false : "throwNamed should throw"; + } catch (ShadowException.Named e) { + assert e.value().equals("value") : "error value field"; + assert e.x() == 42 : "error x field"; + } + + // Function and method parameters named after call-path locals. + assert NameCollisions.syncFn("a", "b", "c", "d", "e").equals("a|b|c|d|e") : "syncFn"; + assert NameCollisions.asyncFn("a", "b", "c").get().equals("a|b|c") : "asyncFn"; + try (Holder holder = new Holder("tag")) { + assert holder.syncMethod("h", "o").equals("tag|h|o") : "syncMethod"; + assert holder.asyncMethod("h", "x", "i").get().equals("tag|h|x|i") : "asyncMethod"; + } + + // Callback interface parameters named after the implementation class's locals. + JavaShadow shadow = new JavaShadow(); + assert NameCollisions.callSyncValue(shadow, "p-") + .equals("p-obj,p-call,p-ret,p-val,p-out,p-low,p-status") : "callSyncValue"; + assert NameCollisions.callSyncPrimitive(shadow, 4, 2) == 42 : "callSyncPrimitive"; + NameCollisions.callSyncVoid(shadow, "n"); + assert shadow.lastVoid.equals("n,status") : "callSyncVoid"; + assert NameCollisions.callSyncThrows(shadow, "ok").equals("ok,status") : "callSyncThrows ok"; + try { + NameCollisions.callSyncThrows(shadow, "throw"); + assert false : "callSyncThrows should throw"; + } catch (ShadowException.Named e) { + assert e.x() == 7 : "sync error propagated"; + } + + assert NameCollisions.callAsyncValue(shadow, "q-").get() + .equals("q-desc,q-success,q-ret,q-result,q-global,q-mh,q-t,q-error,q-status,q-low") : "callAsyncValue"; + assert NameCollisions.callAsyncPrimitive(shadow, 4, 2).get() == 42 : "callAsyncPrimitive"; + NameCollisions.callAsyncVoid(shadow, "n").get(); + assert shadow.lastAsyncVoid.equals("n,ret") : "callAsyncVoid"; + assert NameCollisions.callAsyncThrows(shadow, "ok").get().equals("ok,t") : "callAsyncThrows ok"; + try { + NameCollisions.callAsyncThrows(shadow, "throw").get(); + assert false : "callAsyncThrows should throw"; + } catch (ExecutionException e) { + assert e.getCause() instanceof ShadowException.Named : "async error propagated: " + e.getCause(); + assert ((ShadowException.Named) e.getCause()).x() == 9 : "async error field"; + } + } +} diff --git a/tests/tests.rs b/tests/tests.rs index 894d0a0..fa09ace 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -392,6 +392,7 @@ fixture_tests! { (test_rename, "uniffi-fixture-rename", "scripts/TestRename/TestRename.java"), (test_primitive_arrays, "uniffi-fixture-primitive-arrays", "scripts/TestPrimitiveArrays.java"), (test_zero_copy, "uniffi-fixture-zero-copy", "scripts/TestZeroCopy.java"), + (test_name_collisions, "uniffi-fixture-name-collisions", "scripts/TestNameCollisions.java"), } #[test]