use anyhow::{Context, Result};
use askama::Template;
use core::fmt::Debug;
use heck::{ToLowerCamelCase, ToShoutySnakeCase, ToUpperCamelCase};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::{
borrow::Borrow,
cell::RefCell,
collections::{HashMap, HashSet},
};
use uniffi_bindgen::{interface::*, to_askama_error};
mod callback_interface;
mod compounds;
mod custom;
mod enum_;
mod miscellany;
mod object;
mod primitives;
mod record;
mod variant;
pub fn potentially_add_external_package(
config: &Config,
ci: &ComponentInterface,
type_name: &str,
display_name: String,
) -> String {
match ci.get_type(type_name) {
Some(typ) => {
if ci.is_external(&typ) {
format!(
"{}.{}",
config.external_type_package_name(typ.module_path().unwrap(), &display_name),
display_name
)
} else {
display_name
}
}
None => display_name,
}
}
trait CodeType: Debug {
fn type_label(&self, ci: &ComponentInterface, config: &Config) -> String;
fn type_label_primitive(&self) -> Option<String> {
None
}
fn canonical_name(&self) -> String;
fn ffi_converter_name(&self) -> String {
format!("FfiConverter{}", self.canonical_name())
}
fn ffi_converter_instance(&self, _config: &Config, _ci: &ComponentInterface) -> String {
format!("{}.INSTANCE", self.ffi_converter_name())
}
fn initialization_fn(&self) -> Option<String> {
None
}
}
static KEYWORDS: Lazy<HashSet<String>> = Lazy::new(|| {
let kwlist = vec![
"abstract",
"continue",
"for",
"new",
"switch",
"assert",
"default",
"if",
"package",
"synchronized",
"boolean",
"do",
"goto",
"private",
"this",
"break",
"double",
"implements",
"protected",
"throw",
"byte",
"else",
"import",
"public",
"throws",
"case",
"enum",
"instanceof",
"return",
"transient",
"catch",
"extends",
"int",
"short",
"try",
"char",
"final",
"interface",
"static",
"void",
"class",
"finally",
"long",
"strictfp",
"volatile",
"const",
"float",
"native",
"super",
"while",
"_",
];
HashSet::from_iter(kwlist.into_iter().map(|s| s.to_string()))
});
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Config {
pub(super) package_name: Option<String>,
pub(super) cdylib_name: Option<String>,
generate_immutable_records: Option<bool>,
#[serde(default)]
omit_checksums: bool,
#[serde(default)]
custom_types: HashMap<String, CustomTypeConfig>,
#[serde(default)]
pub(super) external_packages: HashMap<String, String>,
#[serde(default)]
android: bool,
#[serde(default)]
pub(super) rename: toml::Table,
}
impl Config {
pub fn omit_checksums(&self) -> bool {
self.omit_checksums
}
pub fn android(&self) -> bool {
self.android
}
}
impl Config {
pub fn package_name(&self) -> String {
if let Some(package_name) = &self.package_name {
package_name.clone()
} else {
"uniffi".into()
}
}
pub fn cdylib_name(&self) -> String {
if let Some(cdylib_name) = &self.cdylib_name {
cdylib_name.clone()
} else {
"uniffi".into()
}
}
pub fn generate_immutable_records(&self) -> bool {
self.generate_immutable_records.unwrap_or(false)
}
pub fn external_type_package_name(&self, module_path: &str, namespace: &str) -> String {
let crate_name = module_path.split("::").next().unwrap();
match self.external_packages.get(crate_name) {
Some(name) => name.clone(),
None => format!("uniffi.{namespace}"),
}
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct CustomTypeConfig {
imports: Option<Vec<String>>,
type_name: Option<String>,
into_custom: String, lift: String,
from_custom: String, lower: String,
}
impl CustomTypeConfig {
fn lift(&self, name: &str) -> String {
let converter = if self.lift.is_empty() {
&self.into_custom
} else {
&self.lift
};
converter.replace("{}", name)
}
fn lower(&self, name: &str) -> String {
let converter = if self.lower.is_empty() {
&self.from_custom
} else {
&self.lower
};
converter.replace("{}", name)
}
}
pub fn generate_bindings(config: &Config, ci: &ComponentInterface) -> Result<String> {
let output = JavaWrapper::new(config.clone(), ci)
.render()
.context("failed to render java bindings")?;
if config.android() {
Ok(output
.replace("java.lang.foreign.", "com.v7878.foreign.")
.replace("java.lang.invoke.VarHandle", "com.v7878.invoke.VarHandle"))
} else {
Ok(output)
}
}
#[derive(Template)]
#[template(syntax = "java", escape = "none", path = "wrapper.java")]
pub struct JavaWrapper<'a> {
config: Config,
ci: &'a ComponentInterface,
type_helper_code: String,
}
impl<'a> JavaWrapper<'a> {
pub fn new(config: Config, ci: &'a ComponentInterface) -> Self {
let type_renderer = TypeRenderer::new(&config, ci);
let type_helper_code = type_renderer.render().unwrap();
Self {
config,
ci,
type_helper_code,
}
}
pub fn initialization_fns(&self) -> Vec<String> {
self.ci
.iter_local_types()
.map(|t| JavaCodeOracle.find(t))
.filter_map(|ct| ct.initialization_fn())
.collect()
}
pub fn namespace_class_name(&self) -> String {
let base_name = JavaCodeOracle.class_name(self.ci, self.ci.namespace());
let type_names: HashSet<String> = self
.ci
.iter_local_types()
.map(|t| JavaCodeOracle.find(t).type_label(self.ci, &self.config))
.collect();
let mut candidate = base_name;
while type_names.contains(&candidate) {
candidate = format!("{}Lib", candidate);
}
candidate
}
}
#[derive(Template)]
#[template(syntax = "java", escape = "none", path = "Types.java")]
pub struct TypeRenderer<'a> {
config: &'a Config,
ci: &'a ComponentInterface,
include_once_names: RefCell<HashSet<String>>,
}
impl<'a> TypeRenderer<'a> {
fn new(config: &'a Config, ci: &'a ComponentInterface) -> Self {
Self {
config,
ci,
include_once_names: RefCell::new(HashSet::new()),
}
}
fn include_once_check(&self, name: &str) -> bool {
self.include_once_names
.borrow_mut()
.insert(name.to_string())
}
fn external_type_package_name(&self, module_path: &str, namespace: &str) -> String {
self.config
.external_type_package_name(module_path, namespace)
}
}
fn fixup_keyword(name: String) -> String {
if KEYWORDS.contains(&name) {
format!("_{name}")
} else {
name
}
}
#[derive(Clone)]
pub struct JavaCodeOracle;
impl JavaCodeOracle {
fn find(&self, type_: &Type) -> Box<dyn CodeType> {
type_.clone().as_type().as_codetype()
}
fn class_name(&self, ci: &ComponentInterface, nm: &str) -> String {
let name = nm.to_string().to_upper_camel_case();
fixup_keyword(if ci.is_name_used_as_error(nm) {
self.convert_error_suffix(&name)
} else {
name
})
}
fn convert_error_suffix(&self, nm: &str) -> String {
match nm.strip_suffix("Error") {
Some(stripped) if !stripped.is_empty() => format!("{stripped}Exception"),
_ => nm.to_string(),
}
}
fn fn_name(&self, nm: &str) -> String {
fixup_keyword(nm.to_string().to_lower_camel_case())
}
pub fn var_name(&self, nm: &str) -> String {
fixup_keyword(self.var_name_raw(nm))
}
pub fn var_name_raw(&self, nm: &str) -> String {
nm.to_string().to_lower_camel_case()
}
pub fn setter(&self, nm: &str) -> String {
format!("set{}", fixup_keyword(nm.to_string().to_upper_camel_case()))
}
fn enum_variant_name(&self, nm: &str) -> String {
nm.to_string().to_shouty_snake_case()
}
fn ffi_callback_name(&self, nm: &str) -> String {
format!("Uniffi{}", nm.to_upper_camel_case())
}
fn ffi_struct_name(&self, nm: &str) -> String {
format!("Uniffi{}", nm.to_upper_camel_case())
}
fn ffi_type_label(
&self,
ffi_type: &FfiType,
_config: &Config,
_ci: &ComponentInterface,
) -> String {
match ffi_type {
FfiType::Int8 | FfiType::UInt8 => "byte".to_string(),
FfiType::Int16 | FfiType::UInt16 => "short".to_string(),
FfiType::Int32 | FfiType::UInt32 => "int".to_string(),
FfiType::Int64 | FfiType::UInt64 => "long".to_string(),
FfiType::Float32 => "float".to_string(),
FfiType::Float64 => "double".to_string(),
FfiType::Handle => "long".to_string(),
FfiType::RustBuffer(_)
| FfiType::RustCallStatus
| FfiType::ForeignBytes
| FfiType::Callback(_)
| FfiType::Struct(_)
| FfiType::VoidPointer
| FfiType::Reference(_)
| FfiType::MutReference(_) => "java.lang.foreign.MemorySegment".to_string(),
}
}
fn ffi_type_label_boxed(&self, ffi_type: &FfiType) -> String {
match ffi_type {
FfiType::Int8 | FfiType::UInt8 => "java.lang.Byte".to_string(),
FfiType::Int16 | FfiType::UInt16 => "java.lang.Short".to_string(),
FfiType::Int32 | FfiType::UInt32 => "java.lang.Integer".to_string(),
FfiType::Int64 | FfiType::UInt64 => "java.lang.Long".to_string(),
FfiType::Float32 => "java.lang.Float".to_string(),
FfiType::Float64 => "java.lang.Double".to_string(),
FfiType::Handle => "java.lang.Long".to_string(),
_ => "java.lang.foreign.MemorySegment".to_string(),
}
}
fn ffi_value_layout(&self, ffi_type: &FfiType) -> String {
match ffi_type {
FfiType::Int8 | FfiType::UInt8 => "java.lang.foreign.ValueLayout.JAVA_BYTE".to_string(),
FfiType::Int16 | FfiType::UInt16 => {
"java.lang.foreign.ValueLayout.JAVA_SHORT".to_string()
}
FfiType::Int32 | FfiType::UInt32 => {
"java.lang.foreign.ValueLayout.JAVA_INT".to_string()
}
FfiType::Int64 | FfiType::UInt64 => {
"java.lang.foreign.ValueLayout.JAVA_LONG".to_string()
}
FfiType::Float32 => "java.lang.foreign.ValueLayout.JAVA_FLOAT".to_string(),
FfiType::Float64 => "java.lang.foreign.ValueLayout.JAVA_DOUBLE".to_string(),
FfiType::Handle => "java.lang.foreign.ValueLayout.JAVA_LONG".to_string(),
FfiType::RustBuffer(_) => "RustBuffer.LAYOUT".to_string(),
FfiType::RustCallStatus => "java.lang.foreign.ValueLayout.ADDRESS".to_string(),
FfiType::ForeignBytes => "ForeignBytes.LAYOUT".to_string(),
FfiType::Struct(name) => format!("{}.LAYOUT", self.ffi_struct_name(name)),
FfiType::Callback(_)
| FfiType::VoidPointer
| FfiType::Reference(_)
| FfiType::MutReference(_) => "java.lang.foreign.ValueLayout.ADDRESS".to_string(),
}
}
fn ffi_type_alignment(&self, ffi_type: &FfiType) -> usize {
match ffi_type {
FfiType::Int8 | FfiType::UInt8 => 1,
FfiType::Int16 | FfiType::UInt16 => 2,
FfiType::Int32 | FfiType::UInt32 | FfiType::Float32 => 4,
FfiType::Int64 | FfiType::UInt64 | FfiType::Float64 | FfiType::Handle => 8,
FfiType::Callback(_)
| FfiType::VoidPointer
| FfiType::Reference(_)
| FfiType::MutReference(_) => 8,
FfiType::RustBuffer(_)
| FfiType::RustCallStatus
| FfiType::ForeignBytes
| FfiType::Struct(_) => 8,
}
}
fn ffi_type_size(&self, ffi_type: &FfiType) -> usize {
match ffi_type {
FfiType::Int8 | FfiType::UInt8 => 1,
FfiType::Int16 | FfiType::UInt16 => 2,
FfiType::Int32 | FfiType::UInt32 | FfiType::Float32 => 4,
FfiType::Int64 | FfiType::UInt64 | FfiType::Float64 | FfiType::Handle => 8,
FfiType::Callback(_)
| FfiType::VoidPointer
| FfiType::Reference(_)
| FfiType::MutReference(_) => 8,
FfiType::RustBuffer(_) => 24, FfiType::RustCallStatus => 32, FfiType::ForeignBytes => 16, FfiType::Struct(_) => 0, }
}
fn ffi_struct_field_layout(&self, ffi_type: &FfiType) -> String {
match ffi_type {
FfiType::RustBuffer(_) => "RustBuffer.LAYOUT".to_string(),
FfiType::RustCallStatus => "UniffiRustCallStatus.LAYOUT".to_string(),
FfiType::ForeignBytes => "ForeignBytes.LAYOUT".to_string(),
FfiType::Struct(name) => format!("{}.LAYOUT", self.ffi_struct_name(name)),
_ => self.ffi_value_layout(ffi_type),
}
}
fn ffi_value_layout_unaligned(&self, ffi_type: &FfiType) -> String {
match ffi_type {
FfiType::Int8 | FfiType::UInt8 => "java.lang.foreign.ValueLayout.JAVA_BYTE".to_string(), FfiType::Int16 | FfiType::UInt16 => {
"java.lang.foreign.ValueLayout.JAVA_SHORT_UNALIGNED".to_string()
}
FfiType::Int32 | FfiType::UInt32 => {
"java.lang.foreign.ValueLayout.JAVA_INT_UNALIGNED".to_string()
}
FfiType::Int64 | FfiType::UInt64 => {
"java.lang.foreign.ValueLayout.JAVA_LONG_UNALIGNED".to_string()
}
FfiType::Float32 => "java.lang.foreign.ValueLayout.JAVA_FLOAT_UNALIGNED".to_string(),
FfiType::Float64 => "java.lang.foreign.ValueLayout.JAVA_DOUBLE_UNALIGNED".to_string(),
FfiType::Handle => "java.lang.foreign.ValueLayout.JAVA_LONG_UNALIGNED".to_string(),
FfiType::Callback(_)
| FfiType::VoidPointer
| FfiType::Reference(_)
| FfiType::MutReference(_) => {
"java.lang.foreign.ValueLayout.ADDRESS_UNALIGNED".to_string()
}
_ => self.ffi_value_layout(ffi_type),
}
}
fn ffi_invoke_exact_cast(&self, ffi_type: &FfiType) -> String {
match ffi_type {
FfiType::Int8 | FfiType::UInt8 => "(byte) ".to_string(),
FfiType::Int16 | FfiType::UInt16 => "(short) ".to_string(),
FfiType::Int32 | FfiType::UInt32 => "(int) ".to_string(),
FfiType::Int64 | FfiType::UInt64 => "(long) ".to_string(),
FfiType::Float32 => "(float) ".to_string(),
FfiType::Float64 => "(double) ".to_string(),
FfiType::Handle => "(long) ".to_string(),
FfiType::RustBuffer(_)
| FfiType::RustCallStatus
| FfiType::ForeignBytes
| FfiType::Callback(_)
| FfiType::Struct(_)
| FfiType::VoidPointer
| FfiType::Reference(_)
| FfiType::MutReference(_) => "(java.lang.foreign.MemorySegment) ".to_string(),
}
}
fn ffi_type_is_struct(&self, ffi_type: &FfiType) -> bool {
matches!(
ffi_type,
FfiType::RustBuffer(_) | FfiType::ForeignBytes | FfiType::Struct(_)
)
}
fn ffi_type_is_embedded_struct(&self, ffi_type: &FfiType) -> bool {
matches!(
ffi_type,
FfiType::RustBuffer(_)
| FfiType::RustCallStatus
| FfiType::ForeignBytes
| FfiType::Struct(_)
)
}
fn ffi_struct_type_name(&self, ffi_type: &FfiType) -> String {
match ffi_type {
FfiType::Struct(name) => self.ffi_struct_name(name),
FfiType::RustBuffer(_) => "RustBuffer".to_string(),
FfiType::RustCallStatus => "UniffiRustCallStatus".to_string(),
FfiType::ForeignBytes => "ForeignBytes".to_string(),
_ => panic!("ffi_struct_type_name called on non-struct type: {ffi_type:?}"),
}
}
fn object_names(&self, ci: &ComponentInterface, obj: &Object) -> (String, String) {
let class_name = self.class_name(ci, obj.name());
if obj.has_callback_interface() {
let impl_name = format!("{class_name}Impl");
(class_name, impl_name)
} else {
(format!("{class_name}Interface"), class_name)
}
}
}
trait AsCodeType {
fn as_codetype(&self) -> Box<dyn CodeType>;
}
impl AsCodeType for Type {
fn as_codetype(&self) -> Box<dyn CodeType> {
match self.as_type() {
Type::UInt8 | Type::Int8 => Box::new(primitives::Int8CodeType),
Type::UInt16 | Type::Int16 => Box::new(primitives::Int16CodeType),
Type::UInt32 | Type::Int32 => Box::new(primitives::Int32CodeType),
Type::UInt64 | Type::Int64 => Box::new(primitives::Int64CodeType),
Type::Float32 => Box::new(primitives::Float32CodeType),
Type::Float64 => Box::new(primitives::Float64CodeType),
Type::Boolean => Box::new(primitives::BooleanCodeType),
Type::String => Box::new(primitives::StringCodeType),
Type::Bytes => Box::new(primitives::BytesCodeType),
Type::Timestamp => Box::new(miscellany::TimestampCodeType),
Type::Duration => Box::new(miscellany::DurationCodeType),
Type::Enum { name, .. } => Box::new(enum_::EnumCodeType::new(name.clone())),
Type::Object { name, imp, .. } => {
Box::new(object::ObjectCodeType::new(name.clone(), imp))
}
Type::Record { name, .. } => Box::new(record::RecordCodeType::new(name.clone())),
Type::CallbackInterface { name, .. } => Box::new(
callback_interface::CallbackInterfaceCodeType::new(name.clone()),
),
Type::Optional { inner_type } => {
Box::new(compounds::OptionalCodeType::new((*inner_type).clone()))
}
Type::Sequence { inner_type } => match inner_type.as_ref() {
Type::Int16 | Type::UInt16 => Box::new(compounds::Int16ArrayCodeType),
Type::Int32 | Type::UInt32 => Box::new(compounds::Int32ArrayCodeType),
Type::Int64 | Type::UInt64 => Box::new(compounds::Int64ArrayCodeType),
Type::Float32 => Box::new(compounds::Float32ArrayCodeType),
Type::Float64 => Box::new(compounds::Float64ArrayCodeType),
Type::Boolean => Box::new(compounds::BooleanArrayCodeType),
_ => Box::new(compounds::SequenceCodeType::new((*inner_type).clone())),
},
Type::Map {
key_type,
value_type,
} => Box::new(compounds::MapCodeType::new(
(*key_type).clone(),
(*value_type).clone(),
)),
Type::Custom { name, .. } => Box::new(custom::CustomCodeType::new(name.clone())),
}
}
}
impl AsCodeType for &'_ Type {
fn as_codetype(&self) -> Box<dyn CodeType> {
(*self).as_codetype()
}
}
impl AsCodeType for &&'_ Type {
fn as_codetype(&self) -> Box<dyn CodeType> {
(**self).as_codetype()
}
}
impl AsCodeType for &'_ Field {
fn as_codetype(&self) -> Box<dyn CodeType> {
self.as_type().as_codetype()
}
}
impl AsCodeType for &'_ uniffi_bindgen::interface::Enum {
fn as_codetype(&self) -> Box<dyn CodeType> {
self.as_type().as_codetype()
}
}
impl AsCodeType for &'_ uniffi_bindgen::interface::Object {
fn as_codetype(&self) -> Box<dyn CodeType> {
self.as_type().as_codetype()
}
}
impl AsCodeType for &'_ Box<uniffi_meta::Type> {
fn as_codetype(&self) -> Box<dyn CodeType> {
self.as_type().as_codetype()
}
}
impl AsCodeType for &'_ Argument {
fn as_codetype(&self) -> Box<dyn CodeType> {
self.as_type().as_codetype()
}
}
impl AsCodeType for &'_ uniffi_bindgen::interface::Record {
fn as_codetype(&self) -> Box<dyn CodeType> {
self.as_type().as_codetype()
}
}
impl AsCodeType for &'_ uniffi_bindgen::interface::CallbackInterface {
fn as_codetype(&self) -> Box<dyn CodeType> {
self.as_type().as_codetype()
}
}
mod filters {
#![allow(unused_variables)]
use super::*;
use uniffi_meta::AsType;
pub(super) fn ffi_type(
type_: &impl AsType,
_v: &dyn askama::Values,
) -> Result<FfiType, askama::Error> {
Ok(type_.as_type().into())
}
pub(super) fn type_name(
as_ct: &impl AsCodeType,
_v: &dyn askama::Values,
ci: &ComponentInterface,
config: &Config,
) -> Result<String, askama::Error> {
Ok(as_ct.as_codetype().type_label(ci, config))
}
pub(super) fn qualified_type_name<T>(
as_type: &T,
_v: &dyn askama::Values,
ci: &ComponentInterface,
config: &Config,
) -> Result<String, askama::Error>
where
T: AsCodeType + AsType,
{
fully_qualified_type_label(&as_type.as_type(), ci, config)
.map_err(|e| askama::Error::Custom(e.into()))
}
fn fully_qualified_type_label(
ty: &Type,
ci: &ComponentInterface,
config: &Config,
) -> anyhow::Result<String> {
match ty {
Type::Optional { inner_type } => {
Ok(fully_qualified_type_label(inner_type, ci, config)?.to_string())
}
Type::Sequence { inner_type } => match inner_type.as_ref() {
Type::Int16 | Type::UInt16 => Ok("short[]".to_string()),
Type::Int32 | Type::UInt32 => Ok("int[]".to_string()),
Type::Int64 | Type::UInt64 => Ok("long[]".to_string()),
Type::Float32 => Ok("float[]".to_string()),
Type::Float64 => Ok("double[]".to_string()),
Type::Boolean => Ok("boolean[]".to_string()),
_ => Ok(format!(
"java.util.List<{}>",
fully_qualified_type_label(inner_type, ci, config)?
)),
},
Type::Map {
key_type,
value_type,
} => Ok(format!(
"java.util.Map<{}, {}>",
fully_qualified_type_label(key_type, ci, config)?,
fully_qualified_type_label(value_type, ci, config)?
)),
Type::Enum { .. }
| Type::Record { .. }
| Type::Object { .. }
| Type::CallbackInterface { .. }
| Type::Custom { .. } => {
let class_name = ty
.name()
.map(|nm| JavaCodeOracle.class_name(ci, nm))
.ok_or_else(|| anyhow::anyhow!("type {:?} has no name", ty))?;
let package_name = package_for_type(ty, ci, config)?;
Ok(format!("{}.{}", package_name, class_name))
}
_ => Ok(JavaCodeOracle.find(ty).type_label(ci, config)),
}
}
fn package_for_type(
ty: &Type,
ci: &ComponentInterface,
config: &Config,
) -> anyhow::Result<String> {
if ci.is_external(ty) {
let module_path = ty
.module_path()
.ok_or_else(|| anyhow::anyhow!("external type {:?} missing module path", ty))?;
let namespace = ci.namespace_for_module_path(module_path)?;
Ok(config.external_type_package_name(module_path, namespace))
} else {
Ok(config.package_name())
}
}
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,
) -> Result<String, askama::Error> {
Ok(as_ct.as_codetype().canonical_name())
}
pub(super) fn is_external(
as_type: &impl AsType,
_v: &dyn askama::Values,
ci: &ComponentInterface,
) -> Result<bool, askama::Error> {
Ok(ci.is_external(&as_type.as_type()))
}
pub(super) fn ffi_converter_instance(
as_ct: &impl AsCodeType,
_v: &dyn askama::Values,
config: &Config,
ci: &ComponentInterface,
) -> Result<String, askama::Error> {
Ok(as_ct.as_codetype().ffi_converter_instance(config, ci))
}
pub(super) fn ffi_converter_name(
as_ct: &impl AsCodeType,
_v: &dyn askama::Values,
) -> Result<String, askama::Error> {
Ok(as_ct.as_codetype().ffi_converter_name())
}
pub(super) fn lower_fn(
as_ct: &impl AsCodeType,
_v: &dyn askama::Values,
config: &Config,
ci: &ComponentInterface,
) -> Result<String, askama::Error> {
Ok(format!(
"{}.lower",
as_ct.as_codetype().ffi_converter_instance(config, ci)
))
}
pub(super) fn allocation_size_fn(
as_ct: &impl AsCodeType,
_v: &dyn askama::Values,
config: &Config,
ci: &ComponentInterface,
) -> Result<String, askama::Error> {
Ok(format!(
"{}.allocationSize",
as_ct.as_codetype().ffi_converter_instance(config, ci)
))
}
pub(super) fn write_fn(
as_ct: &impl AsCodeType,
_v: &dyn askama::Values,
config: &Config,
ci: &ComponentInterface,
) -> Result<String, askama::Error> {
Ok(format!(
"{}.write",
as_ct.as_codetype().ffi_converter_instance(config, ci)
))
}
pub(super) fn lift_fn(
as_ct: &impl AsCodeType,
_v: &dyn askama::Values,
config: &Config,
ci: &ComponentInterface,
) -> Result<String, askama::Error> {
Ok(format!(
"{}.lift",
as_ct.as_codetype().ffi_converter_instance(config, ci)
))
}
pub(super) fn read_fn(
as_ct: &impl AsCodeType,
_v: &dyn askama::Values,
config: &Config,
ci: &ComponentInterface,
) -> Result<String, askama::Error> {
Ok(format!(
"{}.read",
as_ct.as_codetype().ffi_converter_instance(config, ci)
))
}
fn int_literal(t: &Option<Type>, base10: String) -> Result<String, askama::Error> {
if let Some(t) = t {
match t {
Type::Int8 | Type::UInt8 => Ok(format!("(byte){}", base10)),
Type::Int16 | Type::UInt16 => Ok(format!("(short){}", base10)),
Type::Int32 | Type::UInt32 => Ok(base10),
Type::Int64 | Type::UInt64 => Ok(base10),
_ => Err(to_askama_error("Only ints are supported.")),
}
} else {
Err(to_askama_error("Enum hasn't defined a repr"))
}
}
pub fn variant_discr_literal(
e: &Enum,
_v: &dyn askama::Values,
index: &usize,
) -> Result<String, askama::Error> {
let literal = e.variant_discr(*index).expect("invalid index");
match literal {
Literal::UInt(v, _, _) => int_literal(e.variant_discr_type(), v.to_string()),
Literal::Int(v, _, _) => int_literal(e.variant_discr_type(), v.to_string()),
_ => Err(to_askama_error("Only ints are supported.")),
}
}
pub fn ffi_type_name(
type_: &FfiType,
_v: &dyn askama::Values,
config: &Config,
ci: &ComponentInterface,
) -> Result<String, askama::Error> {
Ok(JavaCodeOracle.ffi_type_label(type_, config, ci))
}
pub fn primitive_call_suffix(
as_ct: &impl AsCodeType,
_v: &dyn askama::Values,
) -> Result<String, askama::Error> {
Ok(
match as_ct.as_codetype().type_label_primitive().as_deref() {
Some("byte") => "Byte",
Some("short") => "Short",
Some("int") => "Int",
Some("long") => "Long",
Some("float") => "Float",
Some("double") => "Double",
_ => "",
}
.to_string(),
)
}
pub fn has_primitive_ffi_type(
as_ct: &impl AsCodeType,
_v: &dyn askama::Values,
) -> Result<bool, askama::Error> {
Ok(matches!(
as_ct.as_codetype().type_label_primitive().as_deref(),
Some("byte" | "short" | "int" | "long" | "float" | "double")
))
}
pub fn ffi_value_layout(
type_: &FfiType,
_v: &dyn askama::Values,
) -> Result<String, askama::Error> {
Ok(JavaCodeOracle.ffi_value_layout(type_))
}
pub fn ffi_struct_layout_body(
ffi_struct: &uniffi_bindgen::interface::FfiStruct,
_v: &dyn askama::Values,
) -> Result<String, askama::Error> {
let mut parts = Vec::new();
let mut offset: usize = 0;
for field in ffi_struct.fields() {
let field_type = field.type_();
let alignment = JavaCodeOracle.ffi_type_alignment(&field_type);
let padding = if !offset.is_multiple_of(alignment) {
alignment - (offset % alignment)
} else {
0
};
if padding > 0 {
parts.push(format!(
"java.lang.foreign.MemoryLayout.paddingLayout({})",
padding
));
offset += padding;
}
let layout = JavaCodeOracle.ffi_struct_field_layout(&field_type);
let field_name = JavaCodeOracle.var_name_raw(field.name());
parts.push(format!("{}.withName(\"{}\")", layout, field_name));
let size = JavaCodeOracle.ffi_type_size(&field_type);
if size > 0 {
offset += size;
} else {
offset = 0; }
}
Ok(parts.join(",\n "))
}
pub fn ffi_value_layout_unaligned(
type_: &FfiType,
_v: &dyn askama::Values,
) -> Result<String, askama::Error> {
Ok(JavaCodeOracle.ffi_value_layout_unaligned(type_))
}
pub fn ffi_invoke_exact_cast(
type_: &FfiType,
_v: &dyn askama::Values,
) -> Result<String, askama::Error> {
Ok(JavaCodeOracle.ffi_invoke_exact_cast(type_))
}
pub fn ffi_type_is_struct(
type_: &FfiType,
_v: &dyn askama::Values,
) -> Result<bool, askama::Error> {
Ok(JavaCodeOracle.ffi_type_is_struct(type_))
}
pub fn ffi_type_is_embedded_struct(
type_: &FfiType,
_v: &dyn askama::Values,
) -> Result<bool, askama::Error> {
Ok(JavaCodeOracle.ffi_type_is_embedded_struct(type_))
}
pub fn ffi_struct_type_name(
type_: &FfiType,
_v: &dyn askama::Values,
) -> Result<String, askama::Error> {
Ok(JavaCodeOracle.ffi_struct_type_name(type_))
}
pub fn ffi_type_name_boxed(
type_: &impl AsType,
_v: &dyn askama::Values,
) -> Result<String, askama::Error> {
let ffi_type: FfiType = type_.as_type().into();
Ok(JavaCodeOracle.ffi_type_label_boxed(&ffi_type))
}
pub fn trait_interface_name(
trait_ty: &Type,
_v: &dyn askama::Values,
ci: &ComponentInterface,
) -> Result<String, askama::Error> {
let Some(module_path) = trait_ty.module_path() else {
return Err(to_askama_error(&format!(
"Invalid trait_type: {trait_ty:?}"
)));
};
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
)));
};
let (obj_name, has_callback_interface) = match trait_ty {
Type::Object { name, .. } => {
let Some(obj) = ci_look.get_object_definition(name) else {
return Err(to_askama_error(&format!(
"trait interface not found: {}",
name
)));
};
(name, obj.has_callback_interface())
}
Type::CallbackInterface { name, .. } => (name, true),
_ => {
return Err(to_askama_error(&format!(
"Invalid trait_type: {trait_ty:?}"
)));
}
};
let class_name = JavaCodeOracle.class_name(ci_look, obj_name);
if has_callback_interface {
Ok(class_name)
} else {
Ok(format!("{}Interface", class_name))
}
}
pub fn class_name<S: AsRef<str>>(
nm: S,
_v: &dyn askama::Values,
ci: &ComponentInterface,
) -> Result<String, askama::Error> {
Ok(JavaCodeOracle.class_name(ci, nm.as_ref()))
}
pub fn class_name_from_type(
as_type: &impl AsType,
_v: &dyn askama::Values,
ci: &ComponentInterface,
) -> Result<String, askama::Error> {
let type_ = as_type.as_type();
let name = match &type_ {
Type::Enum { name, .. } => name,
Type::Object { name, .. } => name,
Type::Record { name, .. } => name,
Type::Custom { name, .. } => name,
Type::CallbackInterface { name, .. } => name,
_ => {
return Err(to_askama_error(&format!(
"class_name_from_type: unsupported type {:?}",
type_
)));
}
};
Ok(JavaCodeOracle.class_name(ci, name))
}
pub fn fn_name<S: AsRef<str>>(nm: S, _v: &dyn askama::Values) -> Result<String, askama::Error> {
Ok(JavaCodeOracle.fn_name(nm.as_ref()))
}
pub fn var_name<S: AsRef<str>>(
nm: S,
_v: &dyn askama::Values,
) -> Result<String, askama::Error> {
Ok(JavaCodeOracle.var_name(nm.as_ref()))
}
pub fn var_name_raw<S: AsRef<str>>(
nm: S,
_v: &dyn askama::Values,
) -> Result<String, askama::Error> {
Ok(JavaCodeOracle.var_name_raw(nm.as_ref()))
}
pub fn setter<S: AsRef<str>>(nm: S, _v: &dyn askama::Values) -> Result<String, askama::Error> {
Ok(JavaCodeOracle.setter(nm.as_ref()))
}
pub fn variant_name(
variant: &Variant,
_v: &dyn askama::Values,
) -> Result<String, askama::Error> {
Ok(JavaCodeOracle.enum_variant_name(variant.name()))
}
pub fn error_variant_name(
variant: &Variant,
_v: &dyn askama::Values,
) -> Result<String, askama::Error> {
let name = variant.name().to_string().to_upper_camel_case();
Ok(JavaCodeOracle.convert_error_suffix(&name))
}
pub fn ffi_callback_name<S: AsRef<str>>(
nm: S,
_v: &dyn askama::Values,
) -> Result<String, askama::Error> {
Ok(JavaCodeOracle.ffi_callback_name(nm.as_ref()))
}
pub fn ffi_struct_name<S: AsRef<str>>(
nm: S,
_v: &dyn askama::Values,
) -> Result<String, askama::Error> {
Ok(JavaCodeOracle.ffi_struct_name(nm.as_ref()))
}
pub fn object_names(
obj: &Object,
_v: &dyn askama::Values,
ci: &ComponentInterface,
) -> Result<(String, String), askama::Error> {
Ok(JavaCodeOracle.object_names(ci, obj))
}
pub fn async_inner_return_type(
callable: impl Callable,
_v: &dyn askama::Values,
ci: &ComponentInterface,
config: &Config,
) -> Result<String, askama::Error> {
callable
.return_type()
.map_or(Ok("java.lang.Void".to_string()), |t| {
type_name(t, _v, ci, config)
})
}
pub fn async_return_type(
callable: impl Callable,
_v: &dyn askama::Values,
ci: &ComponentInterface,
config: &Config,
) -> Result<String, askama::Error> {
let is_async = callable.is_async();
let inner_type = async_inner_return_type(callable, _v, ci, config)?;
if is_async {
Ok(format!(
"java.util.concurrent.CompletableFuture<{inner_type}>"
))
} else {
Ok(inner_type)
}
}
pub fn async_poll(
callable: impl Callable,
_v: &dyn askama::Values,
ci: &ComponentInterface,
) -> Result<String, askama::Error> {
let ffi_func = callable.ffi_rust_future_poll(ci);
Ok(format!(
"(future, callback, continuationHandle) -> UniffiLib.{ffi_func}(future, callback, continuationHandle)"
))
}
pub fn async_complete(
callable: impl Callable,
_v: &dyn askama::Values,
ci: &ComponentInterface,
_config: &Config,
) -> Result<String, askama::Error> {
let ffi_func = callable.ffi_rust_future_complete(ci);
let needs_allocator = callable.return_type().is_some_and(|t| {
let ffi_type: FfiType = t.into();
JavaCodeOracle.ffi_type_is_struct(&ffi_type)
});
let allocator_arg = if needs_allocator { "_allocator, " } else { "" };
let call = format!("UniffiLib.{ffi_func}({allocator_arg}future, continuation)");
Ok(format!("(_allocator, future, continuation) -> {call}"))
}
pub fn async_free(
callable: impl Callable,
_v: &dyn askama::Values,
ci: &ComponentInterface,
) -> Result<String, askama::Error> {
let ffi_func = callable.ffi_rust_future_free(ci);
Ok(format!("(future) -> UniffiLib.{ffi_func}(future)"))
}
pub fn unquote<S: AsRef<str>>(nm: S, _v: &dyn askama::Values) -> Result<String, askama::Error> {
Ok(nm.as_ref().trim_matches('`').to_string())
}
pub fn docstring<S: AsRef<str>>(
docstring: S,
_v: &dyn askama::Values,
spaces: &i32,
) -> Result<String, askama::Error> {
let middle = textwrap::indent(&textwrap::dedent(docstring.as_ref()), " * ");
let wrapped = format!("/**\n{middle}\n */");
let spaces = usize::try_from(*spaces).unwrap_or_default();
Ok(textwrap::indent(&wrapped, &" ".repeat(spaces)))
}
pub fn type_name_for_field(
as_ct: &impl AsCodeType,
_v: &dyn askama::Values,
ci: &ComponentInterface,
config: &Config,
) -> Result<String, askama::Error> {
let codetype = as_ct.as_codetype();
if let Some(primitive) = codetype.type_label_primitive() {
return Ok(primitive);
}
Ok(codetype.type_label(ci, config))
}
pub fn boxed_type_name(
as_ct: &impl AsCodeType,
_v: &dyn askama::Values,
ci: &ComponentInterface,
config: &Config,
) -> Result<String, askama::Error> {
Ok(as_ct.as_codetype().type_label(ci, config))
}
pub fn equals_expr<T: AsCodeType, L: std::fmt::Display, R: std::fmt::Display>(
field: &T,
_v: &dyn askama::Values,
left: L,
right: R,
) -> Result<String, askama::Error> {
if field.as_codetype().type_label_primitive().is_some() {
Ok(format!("{} == {}", left, right))
} else {
Ok(format!("java.util.Objects.equals({}, {})", left, right))
}
}
pub fn hash_code_expr<T: AsCodeType + AsType, V: std::fmt::Display>(
field: &T,
_v: &dyn askama::Values,
value: V,
) -> Result<String, askama::Error> {
match field.as_type() {
Type::Boolean => Ok(format!("java.lang.Boolean.hashCode({})", value)),
Type::Int8 | Type::UInt8 => Ok(format!("java.lang.Byte.hashCode({})", value)),
Type::Int16 | Type::UInt16 => Ok(format!("java.lang.Short.hashCode({})", value)),
Type::Int32 | Type::UInt32 => Ok(format!("java.lang.Integer.hashCode({})", value)),
Type::Int64 | Type::UInt64 => Ok(format!("java.lang.Long.hashCode({})", value)),
Type::Float32 => Ok(format!("java.lang.Float.hashCode({})", value)),
Type::Float64 => Ok(format!("java.lang.Double.hashCode({})", value)),
_ => Ok(format!("java.util.Objects.hashCode({})", value)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use uniffi_bindgen::interface::ComponentInterface;
use uniffi_meta::{
CallbackInterfaceMetadata, EnumMetadata, EnumShape, FnMetadata, FnParamMetadata, Metadata,
MetadataGroup, NamespaceMetadata, ObjectImpl, ObjectMetadata, ObjectTraitImplMetadata,
TraitMethodMetadata, Type, VariantMetadata,
};
#[test]
fn preserves_error_type_named_error() {
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::Enum(EnumMetadata {
module_path: "test".to_string(),
name: "Error".to_string(),
shape: EnumShape::Error { flat: true },
remote: false,
variants: vec![VariantMetadata {
name: "Oops".to_string(),
discr: None,
fields: vec![],
docstring: None,
}],
discr_type: None,
non_exhaustive: false,
docstring: None,
}));
group.add_item(Metadata::Func(FnMetadata {
module_path: "test".to_string(),
name: "always_fails".to_string(),
is_async: false,
inputs: vec![],
return_type: None,
throws: Some(Type::Enum {
module_path: "test".to_string(),
name: "Error".to_string(),
}),
checksum: None,
docstring: None,
}));
let ci = ComponentInterface::from_metadata(group).unwrap();
let bindings = generate_bindings(&Config::default(), &ci).unwrap();
let relevant_lines = bindings
.lines()
.filter(|line| {
line.contains("class Error extends java.lang.Exception")
|| line.contains("class Exception extends java.lang.Exception")
|| line.contains("throws Error")
|| line.contains("throws Exception")
})
.collect::<Vec<_>>()
.join("\n");
assert!(
bindings.contains("public class Error extends java.lang.Exception"),
"expected generated bindings to preserve the `Error` type name:\n{relevant_lines}"
);
assert!(
bindings.contains("public static void alwaysFails() throws Error"),
"expected generated bindings to preserve `throws Error`:\n{relevant_lines}"
);
}
#[test]
fn converts_error_suffix_but_not_bare_error() {
assert_eq!(
JavaCodeOracle.convert_error_suffix("ExampleError"),
"ExampleException"
);
assert_eq!(JavaCodeOracle.convert_error_suffix("Error"), "Error");
}
fn create_primitive_array_test_group() -> MetadataGroup {
let mut group = MetadataGroup {
namespace: NamespaceMetadata {
crate_name: "test".to_string(),
name: "test".to_string(),
},
namespace_docstring: None,
items: Default::default(),
};
let primitive_types = [
("process_floats", Type::Float32),
("process_doubles", Type::Float64),
("process_shorts", Type::Int16),
("process_ints", Type::Int32),
("process_longs", Type::Int64),
("process_bools", Type::Boolean),
];
for (name, inner_type) in primitive_types {
group.add_item(Metadata::Func(FnMetadata {
module_path: "test".to_string(),
name: name.to_string(),
is_async: false,
inputs: vec![FnParamMetadata {
name: "data".to_string(),
ty: Type::Sequence {
inner_type: Box::new(inner_type.clone()),
},
by_ref: false,
optional: false,
default: None,
}],
return_type: Some(Type::Sequence {
inner_type: Box::new(inner_type),
}),
throws: None,
checksum: None,
docstring: None,
}));
}
group
}
#[test]
fn generates_float32_primitive_array() {
let group = create_primitive_array_test_group();
let ci = ComponentInterface::from_metadata(group).unwrap();
let bindings = generate_bindings(&Config::default(), &ci).unwrap();
assert!(
bindings.contains("float[]"),
"expected float[] in generated bindings"
);
assert!(
bindings.contains("FfiConverterFloat32Array"),
"expected FfiConverterFloat32Array in generated bindings"
);
assert!(
bindings.contains("public static float[] processFloats(float[] data)"),
"expected processFloats method with float[] signature"
);
}
#[test]
fn generates_float64_primitive_array() {
let group = create_primitive_array_test_group();
let ci = ComponentInterface::from_metadata(group).unwrap();
let bindings = generate_bindings(&Config::default(), &ci).unwrap();
assert!(
bindings.contains("double[]"),
"expected double[] in generated bindings"
);
assert!(
bindings.contains("FfiConverterFloat64Array"),
"expected FfiConverterFloat64Array in generated bindings"
);
assert!(
bindings.contains("public static double[] processDoubles(double[] data)"),
"expected processDoubles method with double[] signature"
);
}
#[test]
fn generates_int32_primitive_array() {
let group = create_primitive_array_test_group();
let ci = ComponentInterface::from_metadata(group).unwrap();
let bindings = generate_bindings(&Config::default(), &ci).unwrap();
assert!(
bindings.contains("int[]"),
"expected int[] in generated bindings"
);
assert!(
bindings.contains("FfiConverterInt32Array"),
"expected FfiConverterInt32Array in generated bindings"
);
assert!(
bindings.contains("public static int[] processInts(int[] data)"),
"expected processInts method with int[] signature"
);
}
#[test]
fn generates_int64_primitive_array() {
let group = create_primitive_array_test_group();
let ci = ComponentInterface::from_metadata(group).unwrap();
let bindings = generate_bindings(&Config::default(), &ci).unwrap();
assert!(
bindings.contains("long[]"),
"expected long[] in generated bindings"
);
assert!(
bindings.contains("FfiConverterInt64Array"),
"expected FfiConverterInt64Array in generated bindings"
);
assert!(
bindings.contains("public static long[] processLongs(long[] data)"),
"expected processLongs method with long[] signature"
);
}
#[test]
fn generates_int16_primitive_array() {
let group = create_primitive_array_test_group();
let ci = ComponentInterface::from_metadata(group).unwrap();
let bindings = generate_bindings(&Config::default(), &ci).unwrap();
assert!(
bindings.contains("short[]"),
"expected short[] in generated bindings"
);
assert!(
bindings.contains("FfiConverterInt16Array"),
"expected FfiConverterInt16Array in generated bindings"
);
assert!(
bindings.contains("public static short[] processShorts(short[] data)"),
"expected processShorts method with short[] signature"
);
}
#[test]
fn generates_boolean_primitive_array() {
let group = create_primitive_array_test_group();
let ci = ComponentInterface::from_metadata(group).unwrap();
let bindings = generate_bindings(&Config::default(), &ci).unwrap();
assert!(
bindings.contains("boolean[]"),
"expected boolean[] in generated bindings"
);
assert!(
bindings.contains("FfiConverterBooleanArray"),
"expected FfiConverterBooleanArray in generated bindings"
);
assert!(
bindings.contains("public static boolean[] processBools(boolean[] data)"),
"expected processBools method with boolean[] signature"
);
}
#[test]
fn does_not_generate_list_for_primitive_sequences() {
let group = create_primitive_array_test_group();
let ci = ComponentInterface::from_metadata(group).unwrap();
let bindings = generate_bindings(&Config::default(), &ci).unwrap();
assert!(
!bindings.contains("List<java.lang.Float>"),
"should not contain List<java.lang.Float>"
);
assert!(
!bindings.contains("List<java.lang.Double>"),
"should not contain List<java.lang.Double>"
);
assert!(
!bindings.contains("List<java.lang.Integer>"),
"should not contain List<java.lang.Integer>"
);
assert!(
!bindings.contains("List<java.lang.Long>"),
"should not contain List<java.lang.Long>"
);
assert!(
!bindings.contains("List<java.lang.Short>"),
"should not contain List<java.lang.Short>"
);
assert!(
!bindings.contains("List<java.lang.Boolean>"),
"should not contain List<java.lang.Boolean>"
);
}
#[test]
fn android_replaces_ffm_package() {
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::Func(FnMetadata {
module_path: "test".to_string(),
name: "noop".to_string(),
is_async: false,
inputs: vec![],
return_type: None,
throws: None,
checksum: None,
docstring: None,
}));
let mut ci = ComponentInterface::from_metadata(group).unwrap();
ci.derive_ffi_funcs().unwrap();
let android_config: Config = toml::from_str("android = true").unwrap();
let bindings = generate_bindings(&android_config, &ci).unwrap();
assert!(
!bindings.contains("java.lang.foreign."),
"android bindings should not contain java.lang.foreign"
);
assert!(
bindings.contains("com.v7878.foreign."),
"android bindings should contain com.v7878.foreign"
);
assert!(
bindings.contains("java.lang.invoke.MethodHandle"),
"android bindings should preserve java.lang.invoke.MethodHandle"
);
assert!(
bindings.contains("java.lang.Exception"),
"android bindings should preserve java.lang.Exception"
);
}
#[test]
fn trait_impl_with_submodule_path() {
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(),
};
group.add_item(Metadata::Object(ObjectMetadata {
module_path: submodule_path.to_string(),
name: "MyTrait".to_string(),
remote: false,
imp: ObjectImpl::CallbackTrait,
docstring: None,
}));
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();
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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.join("\n")
);
}
}