diff --git a/src/gen_java/mod.rs b/src/gen_java/mod.rs index 8398c93..1db1616 100644 --- a/src/gen_java/mod.rs +++ b/src/gen_java/mod.rs @@ -862,6 +862,24 @@ mod filters { } } + /// Returns the [`ComponentInterface`] that owns `module_path`. + /// + /// If `module_path` belongs to the current crate, returns `ci` directly. + /// Otherwise looks up an external component interface by the full module + /// path first, then by the crate name (first `::` segment). + fn component_interface_for_module_path<'a>( + ci: &'a ComponentInterface, + module_path: &str, + ) -> Option<&'a ComponentInterface> { + let crate_name = module_path.split("::").next().unwrap_or(module_path); + if crate_name == ci.crate_name() { + Some(ci) + } else { + ci.find_component_interface(module_path) + .or_else(|| ci.find_component_interface(crate_name)) + } + } + pub(super) fn canonical_name( as_ct: &impl AsCodeType, _v: &dyn askama::Values, @@ -1139,7 +1157,7 @@ mod filters { "Invalid trait_type: {trait_ty:?}" ))); }; - let Some(ci_look) = ci.find_component_interface(module_path) else { + let Some(ci_look) = component_interface_for_module_path(ci, module_path) else { return Err(to_askama_error(&format!( "no interface with module_path: {}", module_path @@ -1432,8 +1450,9 @@ mod tests { use super::*; use uniffi_bindgen::interface::ComponentInterface; use uniffi_meta::{ - EnumMetadata, EnumShape, FnMetadata, FnParamMetadata, Metadata, MetadataGroup, - NamespaceMetadata, Type, VariantMetadata, + CallbackInterfaceMetadata, EnumMetadata, EnumShape, FnMetadata, FnParamMetadata, Metadata, + MetadataGroup, NamespaceMetadata, ObjectImpl, ObjectMetadata, ObjectTraitImplMetadata, + TraitMethodMetadata, Type, VariantMetadata, }; #[test] @@ -1757,4 +1776,205 @@ mod tests { "android bindings should preserve java.lang.Exception" ); } + + #[test] + fn trait_impl_with_submodule_path() { + // Regression test: when a crate has multiple modules, module_path is + // "crate_name::submodule". component_interface_for_module_path() handles + // this by checking the local CI's crate_name first before falling back + // to find_component_interface() for external crates. + let submodule_path = "mycrate::inner"; + let mut group = MetadataGroup { + namespace: NamespaceMetadata { + crate_name: "mycrate".to_string(), + name: "mycrate".to_string(), + }, + namespace_docstring: None, + items: Default::default(), + }; + + // A trait object defined in a submodule + group.add_item(Metadata::Object(ObjectMetadata { + module_path: submodule_path.to_string(), + name: "MyTrait".to_string(), + remote: false, + imp: ObjectImpl::CallbackTrait, + docstring: None, + })); + + // A concrete object that implements the trait, also in the submodule + group.add_item(Metadata::Object(ObjectMetadata { + module_path: submodule_path.to_string(), + name: "MyObj".to_string(), + remote: false, + imp: ObjectImpl::Struct, + docstring: None, + })); + + group.add_item(Metadata::ObjectTraitImpl(ObjectTraitImplMetadata { + ty: Type::Object { + module_path: submodule_path.to_string(), + name: "MyObj".to_string(), + imp: ObjectImpl::Struct, + }, + trait_ty: Type::Object { + module_path: submodule_path.to_string(), + name: "MyTrait".to_string(), + imp: ObjectImpl::CallbackTrait, + }, + })); + + let mut ci = ComponentInterface::from_metadata(group).unwrap(); + ci.derive_ffi_funcs().unwrap(); + let bindings = generate_bindings(&Config::default(), &ci).unwrap(); + + // The object should implement the trait interface + assert!( + bindings.contains("implements AutoCloseable, MyObjInterface, MyTrait"), + "MyObj should implement MyTrait via trait_interface_name even with submodule path:\n{}", + bindings + .lines() + .filter(|l| l.contains("MyObj") || l.contains("MyTrait")) + .collect::>() + .join("\n") + ); + } + + #[test] + fn local_trait_impls_accept_submodule_module_paths() { + let mut group = MetadataGroup { + namespace: NamespaceMetadata { + crate_name: "test".to_string(), + name: "test".to_string(), + }, + namespace_docstring: None, + items: Default::default(), + }; + group.add_item(Metadata::Object(ObjectMetadata { + module_path: "test".to_string(), + name: "DefaultMetricsRecorder".to_string(), + remote: false, + imp: ObjectImpl::Struct, + docstring: None, + })); + group.add_item(Metadata::CallbackInterface(CallbackInterfaceMetadata { + module_path: "test::metrics".to_string(), + name: "MetricsRecorder".to_string(), + docstring: None, + })); + group.add_item(Metadata::ObjectTraitImpl(ObjectTraitImplMetadata { + ty: Type::Object { + module_path: "test".to_string(), + name: "DefaultMetricsRecorder".to_string(), + imp: ObjectImpl::Struct, + }, + trait_ty: Type::CallbackInterface { + module_path: "test::metrics".to_string(), + name: "MetricsRecorder".to_string(), + }, + })); + + let mut ci = ComponentInterface::from_metadata(group).unwrap(); + ci.derive_ffi_funcs().unwrap(); + + let interface_name = super::filters::trait_interface_name( + &Type::CallbackInterface { + module_path: "test::metrics".to_string(), + name: "MetricsRecorder".to_string(), + }, + &(), + &ci, + ) + .unwrap(); + assert_eq!(interface_name, "MetricsRecorder"); + + let bindings = generate_bindings(&Config::default(), &ci).unwrap(); + assert!( + bindings.contains("DefaultMetricsRecorderInterface, MetricsRecorder"), + "expected local callback trait impls with submodule paths to render successfully:\n{}", + bindings + .lines() + .filter(|line| line.contains("class DefaultMetricsRecorder")) + .collect::>() + .join("\n") + ); + } + + #[test] + fn callback_interface_helpers_use_class_style_names() { + let mut group = MetadataGroup { + namespace: NamespaceMetadata { + crate_name: "test".to_string(), + name: "test".to_string(), + }, + namespace_docstring: None, + items: Default::default(), + }; + group.add_item(Metadata::CallbackInterface(CallbackInterfaceMetadata { + module_path: "test".to_string(), + name: "Histogram".to_string(), + docstring: None, + })); + group.add_item(Metadata::TraitMethod(TraitMethodMetadata { + module_path: "test".to_string(), + trait_name: "Histogram".to_string(), + index: 0, + name: "record".to_string(), + is_async: false, + inputs: vec![FnParamMetadata { + name: "value".to_string(), + ty: Type::Float64, + by_ref: false, + optional: false, + default: None, + }], + return_type: None, + throws: None, + takes_self_by_arc: false, + checksum: None, + docstring: None, + })); + + let mut ci = ComponentInterface::from_metadata(group).unwrap(); + ci.derive_ffi_funcs().unwrap(); + + let bindings = generate_bindings(&Config::default(), &ci).unwrap(); + + assert!( + bindings.contains("public void record(double value);"), + "expected callback interface API to preserve the Rust method name:\n{}", + bindings + .lines() + .filter(|l| l.contains("double value")) + .collect::>() + .join("\n") + ); + assert!( + bindings.contains("public static final class RecordCallback implements UniffiCallbackInterfaceHistogramMethod0.Fn"), + "expected callback helper class to use a Java class-style name:\n{}", + bindings + .lines() + .filter(|l| l.contains("implements UniffiCallbackInterfaceHistogramMethod0.Fn")) + .collect::>() + .join("\n") + ); + assert!( + bindings.contains("RecordCallback.INSTANCE"), + "expected generated callback helper references to use the renamed helper class:\n{}", + bindings + .lines() + .filter(|l| l.contains(".INSTANCE")) + .collect::>() + .join("\n") + ); + assert!( + !bindings.contains("public static class record implements"), + "unexpected lowercase helper class leaked into generated bindings:\n{}", + bindings + .lines() + .filter(|l| l.contains("class record")) + .collect::>() + .join("\n") + ); + } } diff --git a/src/templates/CallbackInterfaceImpl.java b/src/templates/CallbackInterfaceImpl.java index a228fb2..66c8bd1 100644 --- a/src/templates/CallbackInterfaceImpl.java +++ b/src/templates/CallbackInterfaceImpl.java @@ -17,7 +17,7 @@ public class {{ trait_impl }} { {{ vtable|ffi_struct_type_name }}.setuniffiFree(vtable, {{ "CallbackInterfaceFree"|ffi_callback_name }}.toUpcallStub(UniffiFree.INSTANCE, java.lang.foreign.Arena.global())); {{ vtable|ffi_struct_type_name }}.setuniffiClone(vtable, {{ "CallbackInterfaceClone"|ffi_callback_name }}.toUpcallStub(UniffiClone.INSTANCE, java.lang.foreign.Arena.global())); {%- for (ffi_callback, meth) in vtable_methods.iter() %} - {{ vtable|ffi_struct_type_name }}.set{{ meth.name()|var_name_raw }}(vtable, {{ ffi_callback.name()|ffi_callback_name }}.toUpcallStub({{ meth.name()|var_name }}.INSTANCE, java.lang.foreign.Arena.global())); + {{ vtable|ffi_struct_type_name }}.set{{ meth.name()|var_name_raw }}(vtable, {{ ffi_callback.name()|ffi_callback_name }}.toUpcallStub({{ meth.name()|class_name(ci) }}Callback.INSTANCE, java.lang.foreign.Arena.global())); {%- endfor %} } @@ -27,10 +27,10 @@ void register() { } {%- for (ffi_callback, meth) in vtable_methods.iter() %} - {% let inner_method_class = meth.name()|var_name %} - public static class {{ inner_method_class }} implements {{ ffi_callback.name()|ffi_callback_name }}.Fn { - public static final {{ inner_method_class }} INSTANCE = new {{ inner_method_class }}(); - private {{ inner_method_class }}() {} + {% let callback_class = meth.name()|class_name(ci) %} + public static final class {{ callback_class }}Callback implements {{ ffi_callback.name()|ffi_callback_name }}.Fn { + public static final {{ callback_class }}Callback INSTANCE = new {{ callback_class }}Callback(); + private {{ callback_class }}Callback() {} @Override public {% match ffi_callback.return_type() %}{% when Some(return_type) %}{{ return_type|ffi_type_name(config, ci) }}{% when None %}void{% endmatch %} callback(