use prebindgen_registry::{
flat::{self, TypeRef},
Conversions,
};
use super::*;
use crate::jni::trait_impl::{build_through_erased_wrappers, build_through_wrappers};
pub(crate) fn struct_input_body(
ext: &Declarations,
s: &flat::Struct,
registry: &impl Conversions<KotlinMeta>,
emit: &prebindgen_registry::Emit,
) -> Option<(syn::Type, syn::Expr)> {
let struct_name = s.name.to_string();
let struct_module = struct_module_path(ext, registry, &s.name);
let struct_ident = &s.name;
let mut field_preludes: Vec<TokenStream> = Vec::new();
let mut field_init: Vec<TokenStream> = Vec::new();
for field in &s.fields {
let fname_ident = field.name.clone()?;
let camel = kotlin_property_name(&fname_ident);
let err_prefix = format!("{struct_name}.{camel}: {{}}");
let raw_ident = format_ident!("__{}_raw", fname_ident);
let field_entry = registry.input_entry(&field.ty)?;
let field_optional = field.ty.optional_inner().is_some();
let inner = field.ty.optional_inner().unwrap_or(&field.ty);
let field_wire = field_entry.destination.clone();
let field_conv = composed_entry_decode(field_entry, &raw_ident, &fname_ident);
if let Some(proj) = &field_entry.metadata.projection {
match proj.kind {
ProjectionKind::Handle => {
let java_path = handle_field_fqn(ext, proj).replace('.', "/");
let sig = format!("L{};", java_path);
let tmp_ident = format_ident!("__{}_jobj", fname_ident);
let field_ty = emit.spell(&field.ty);
let decode = if field_optional {
quote! { let #fname_ident = #field_conv; }
} else {
quote! {
if #raw_ident == 0 || (#raw_ident & 1) == 1 {
return ::core::result::Result::Err(
<__JniErr as ::core::convert::From<String>>::from(
"Operation on a closed native handle.".to_string(),
),
);
}
let #fname_ident: #field_ty = unsafe {
*std::boxed::Box::from_raw(#raw_ident as *mut #field_ty)
};
}
};
field_preludes.push(quote! {
let #tmp_ident: jni::objects::JObject = env.get_field(v, #camel, #sig)
.and_then(|val| val.l())
.map_err(|e| <__JniErr as ::core::convert::From<String>>::from(format!(#err_prefix, e)))?;
let #raw_ident: jni::sys::jlong = if #tmp_ident.is_null() {
0
} else {
env.call_method(&#tmp_ident, "peek", "()J", &[])
.and_then(|val| val.j())
.map_err(|e| <__JniErr as ::core::convert::From<String>>::from(format!(#err_prefix, e)))?
};
#decode
});
}
ProjectionKind::Unsigned64 => {
if field_optional {
let niche = matches!(
proj.strategy,
FoldStrategy::Optional(NullableKind::Niche, _)
);
let inner_conv = composed_entry_decode(
registry.input_entry(inner)?,
&raw_ident,
&fname_ident,
);
let tmp_ident = format_ident!("__{}_jobj", fname_ident);
let decode = if niche {
quote! { #field_conv }
} else {
quote! {
::core::option::Option::Some(#inner_conv)
}
};
field_preludes.push(quote! {
let #tmp_ident: jni::objects::JObject = env
.get_field(v, #camel, "Lkotlin/ULong;")
.and_then(|val| val.l())
.map_err(|e| <__JniErr as ::core::convert::From<String>>::from(format!(#err_prefix, e)))?;
let #fname_ident = if #tmp_ident.is_null() {
::core::option::Option::None
} else {
let #raw_ident: jni::sys::jlong = env
.call_method(&#tmp_ident, "unbox-impl", "()J", &[])
.and_then(|val| val.j())
.map_err(|e| <__JniErr as ::core::convert::From<String>>::from(format!(#err_prefix, e)))?;
#decode
};
});
} else {
field_preludes.push(quote! {
let #raw_ident: jni::sys::jlong = env
.get_field(v, #camel, "J")
.and_then(|val| val.j())
.map_err(|e| <__JniErr as ::core::convert::From<String>>::from(format!(#err_prefix, e)))?;
let #fname_ident = #field_conv;
});
}
}
}
field_init.push(quote!(#fname_ident));
continue;
}
if ext.is_kotlin_enum_reading(inner) {
if let Some(fqn) = match inner.unwrapped().kind() {
flat::TypeKind::Named { id, .. } => id.ident(),
_ => None,
}
.and_then(|n| ext.kotlin_fqn(&TypeKey::from_ident(&n)))
.map(|v| v.to_string())
{
let sig = format!("L{};", fqn.replace('.', "/"));
let inner_conv =
composed_entry_decode(registry.input_entry(inner)?, &raw_ident, &fname_ident);
let tmp_ident = format_ident!("__{}_jobj", fname_ident);
let decode = if field_optional {
quote! {
let #fname_ident = if #tmp_ident.is_null() {
::core::option::Option::None
} else {
let #raw_ident: jni::sys::jint = env.call_method(&#tmp_ident, "getValue", "()I", &[])
.and_then(|val| val.i())
.map_err(|e| <__JniErr as ::core::convert::From<String>>::from(format!(#err_prefix, e)))?;
::core::option::Option::Some(#inner_conv)
};
}
} else {
quote! {
let #raw_ident: jni::sys::jint = env.call_method(&#tmp_ident, "getValue", "()I", &[])
.and_then(|val| val.i())
.map_err(|e| <__JniErr as ::core::convert::From<String>>::from(format!(#err_prefix, e)))?;
let #fname_ident = #inner_conv;
}
};
field_preludes.push(quote! {
let #tmp_ident: jni::objects::JObject = env.get_field(v, #camel, #sig)
.and_then(|val| val.l())
.map_err(|e| <__JniErr as ::core::convert::From<String>>::from(format!(#err_prefix, e)))?;
#decode
});
field_init.push(quote!(#fname_ident));
continue;
}
}
match jni_field_access(&field_wire) {
Some((sig, accessor, false)) => {
field_preludes.push(quote! {
let #raw_ident: #field_wire = env.get_field(v, #camel, #sig)
.and_then(|val| val.#accessor())
.map_err(|e| <__JniErr as ::core::convert::From<String>>::from(format!(#err_prefix, e)))? as _;
let #fname_ident = #field_conv;
});
}
Some((sig, _, true)) => {
let tmp_ident = format_ident!("__{}_jobj", fname_ident);
field_preludes.push(quote! {
let #tmp_ident: jni::objects::JObject = env.get_field(v, #camel, #sig)
.and_then(|val| val.l())
.map_err(|e| <__JniErr as ::core::convert::From<String>>::from(format!(#err_prefix, e)))?;
let #raw_ident: #field_wire = #tmp_ident.into();
let #fname_ident = #field_conv;
});
}
None => {
let sig = registry
.input_entry(inner)
.and_then(|e| jni_field_access(&e.destination))
.and_then(|(sig, _, is_obj)| {
if is_obj {
Some(sig.to_string())
} else {
box_descriptor_for_primitive(sig).map(str::to_string)
}
})
.or_else(|| {
match inner.unwrapped().kind() {
flat::TypeKind::Named { id, .. } => id.ident(),
_ => None,
}
.and_then(|name| {
ext.kotlin_fqn(&TypeKey::from_ident(&name))
.map(|v| format!("L{};", v.replace('.', "/")))
})
})
.or_else(|| {
if inner.sequence_elem().is_some() {
Some("Ljava/util/List;".to_string())
} else {
None
}
})
.unwrap_or_else(|| "Ljava/lang/Object;".to_string());
field_preludes.push(quote! {
let #raw_ident: jni::objects::JObject = env.get_field(v, #camel, #sig)
.and_then(|val| val.l())
.map_err(|e| <__JniErr as ::core::convert::From<String>>::from(format!(#err_prefix, e)))?;
let #fname_ident = #field_conv;
});
}
}
field_init.push(quote!(#fname_ident));
}
let ctor = emit.shape(s, quote!(#struct_module::#struct_ident), &field_init);
let body: syn::Expr = syn::parse_quote!({
#(#field_preludes)*
#ctor
});
Some((syn::parse_quote!(jni::objects::JObject), body))
}
pub(crate) fn sum_input_body(
ext: &Declarations,
v: &flat::Variant,
registry: &impl Conversions<KotlinMeta>,
emit: &prebindgen_registry::Emit,
) -> Option<(syn::Type, syn::Expr)> {
let key = TypeKey::from_ident(&v.name);
let cfg = ext.types.get(&key)?;
let sum_cfg = cfg.sum()?;
let iface_fqn = cfg.name_spec.as_ref().map(|s| ext.fqn_of(s))?;
let iface_path = iface_fqn.replace('.', "/");
let source_module = ext.fn_module(registry, &v.name);
let enum_ident = &v.name;
let enum_name = v.name.to_string();
let mut arms: Vec<TokenStream> = Vec::new();
for alt in &v.alternatives {
let vident = &alt.name;
let kotlin_name = ext.sum_variant_class_name(sum_cfg, vident);
let jvm_class = format!("{iface_path}${kotlin_name}");
let mut preludes: Vec<TokenStream> = Vec::new();
let mut inits: Vec<TokenStream> = Vec::new();
for field in &alt.fields {
let prop = crate::jni::struct_plan::sum_field_prop_name(&field.member());
let bind = format_ident!("__p_{}", prop);
let err_prefix = format!("{enum_name}.{kotlin_name}.{prop}: {{}}");
let (pre, value) = read_kotlin_property(
ext,
registry,
"e!(__obj),
&prop,
&field.ty,
&bind,
&err_prefix,
emit,
)?;
preludes.push(pre);
inits.push(field.bind(&value));
}
let ctor = emit.shape(alt, quote!(#source_module::#enum_ident::#vident), &inits);
arms.push(quote! {
if env.is_instance_of(__obj, #jvm_class)
.map_err(|e| <__JniErr as ::core::convert::From<String>>::from(
format!(concat!(#enum_name, ": instanceof ", #jvm_class, ": {}"), e)))?
{
#(#preludes)*
return ::core::result::Result::Ok(#ctor);
}
});
}
let no_match = format!("{enum_name}: value is not one of its declared variants");
let null_msg = format!("{enum_name}: null value where a variant was required");
let body: syn::Expr = syn::parse_quote!({
let __obj = v;
(|| -> ::core::result::Result<#source_module::#enum_ident, __JniErr> {
if __obj.is_null() {
return ::core::result::Result::Err(
<__JniErr as ::core::convert::From<String>>::from(#null_msg.to_string()),
);
}
#(#arms)*
::core::result::Result::Err(
<__JniErr as ::core::convert::From<String>>::from(#no_match.to_string()),
)
})()?
});
Some((syn::parse_quote!(jni::objects::JObject), body))
}
#[allow(clippy::too_many_arguments)]
fn read_kotlin_property(
ext: &Declarations,
registry: &impl Conversions<KotlinMeta>,
receiver: &TokenStream,
prop: &str,
reading: &TypeRef,
bind: &syn::Ident,
err_prefix: &str,
emit: &prebindgen_registry::Emit,
) -> Option<(TokenStream, TokenStream)> {
let entry = registry.input_entry(reading)?;
let ty = emit.spell(reading);
let optional = reading.optional_inner().is_some();
let inner = reading.optional_inner().unwrap_or(reading);
let wire = entry.destination.clone();
let raw = format_ident!("{}_raw", bind);
let conv = composed_property_decode(entry, bind);
if let Some(proj) = &entry.metadata.projection {
if matches!(proj.kind, ProjectionKind::Handle) {
let fqn = handle_field_fqn(ext, proj).replace('.', "/");
let sig = format!("L{fqn};");
let obj = format_ident!("{}_obj", bind);
let closed_msg = "Operation on a closed native handle.";
let decode = if optional {
quote! { let #bind = #conv; }
} else {
quote! {
if #raw == 0 || (#raw & 1) == 1 {
return ::core::result::Result::Err(
<__JniErr as ::core::convert::From<String>>::from(
#closed_msg.to_string(),
),
);
}
let #bind: #ty = unsafe {
*std::boxed::Box::from_raw(#raw as *mut #ty)
};
}
};
return Some((
quote! {
let #obj: jni::objects::JObject = env.get_field(#receiver, #prop, #sig)
.and_then(|val| val.l())
.map_err(|e| <__JniErr as ::core::convert::From<String>>::from(format!(#err_prefix, e)))?;
let #raw: jni::sys::jlong = if #obj.is_null() {
0
} else {
env.call_method(&#obj, "peek", "()J", &[])
.and_then(|val| val.j())
.map_err(|e| <__JniErr as ::core::convert::From<String>>::from(format!(#err_prefix, e)))?
};
#decode
},
quote!(#bind),
));
}
}
if ext.is_kotlin_enum_reading(inner) {
let fqn = match inner.unwrapped().kind() {
flat::TypeKind::Named { id, .. } => id.ident(),
_ => None,
}
.and_then(|n| ext.kotlin_fqn(&TypeKey::from_ident(&n)))
.map(|v| v.to_string())?;
let sig = format!("L{};", fqn.replace('.', "/"));
let obj = format_ident!("{}_obj", bind);
let decode = if optional {
let inner_conv = composed_entry_decode(registry.input_entry(inner)?, &raw, bind);
quote! {
let #bind = if #obj.is_null() {
::core::option::Option::None
} else {
let #raw: jni::sys::jint = env.call_method(&#obj, "getValue", "()I", &[])
.and_then(|val| val.i())
.map_err(|e| <__JniErr as ::core::convert::From<String>>::from(format!(#err_prefix, e)))?;
::core::option::Option::Some(#inner_conv)
};
}
} else {
quote! {
let #raw: jni::sys::jint = env.call_method(&#obj, "getValue", "()I", &[])
.and_then(|val| val.i())
.map_err(|e| <__JniErr as ::core::convert::From<String>>::from(format!(#err_prefix, e)))?;
let #bind = #conv;
}
};
return Some((
quote! {
let #obj: jni::objects::JObject = env.get_field(#receiver, #prop, #sig)
.and_then(|val| val.l())
.map_err(|e| <__JniErr as ::core::convert::From<String>>::from(format!(#err_prefix, e)))?;
#decode
},
quote!(#bind),
));
}
match jni_field_access(&wire) {
Some((sig, accessor, false)) => Some((
quote! {
let #raw: #wire = env.get_field(#receiver, #prop, #sig)
.and_then(|val| val.#accessor())
.map_err(|e| <__JniErr as ::core::convert::From<String>>::from(format!(#err_prefix, e)))? as _;
let #bind = #conv;
},
quote!(#bind),
)),
Some((sig, _, true)) => {
let obj = format_ident!("{}_obj", bind);
Some((
quote! {
let #obj: jni::objects::JObject = env.get_field(#receiver, #prop, #sig)
.and_then(|val| val.l())
.map_err(|e| <__JniErr as ::core::convert::From<String>>::from(format!(#err_prefix, e)))?;
let #raw: #wire = #obj.into();
let #bind = #conv;
},
quote!(#bind),
))
}
None => {
let sig = match inner.unwrapped().kind() {
flat::TypeKind::Named { id, .. } => id.ident(),
_ => None,
}
.and_then(|name| ext.kotlin_fqn(&TypeKey::from_ident(&name)))
.map(|v| format!("L{};", v.replace('.', "/")))
.or_else(|| {
inner
.sequence_elem()
.is_some()
.then(|| "Ljava/util/List;".to_string())
})
.unwrap_or_else(|| "Ljava/lang/Object;".to_string());
Some((
quote! {
let #raw: jni::objects::JObject = env.get_field(#receiver, #prop, #sig)
.and_then(|val| val.l())
.map_err(|e| <__JniErr as ::core::convert::From<String>>::from(format!(#err_prefix, e)))?;
let #bind = #conv;
},
quote!(#bind),
))
}
}
}
fn composed_entry_decode(
entry: &prebindgen_registry::TypeEntry<KotlinMeta>,
raw: &syn::Ident,
stage_base: &syn::Ident,
) -> TokenStream {
let converter = entry.converter_ident();
if entry.pre_stages.is_empty() {
return quote!(#converter(env, &#raw)?);
}
let s0 = format_ident!("{}_s0", stage_base);
let mut body = quote! { let #s0 = #converter(env, &#raw)?; };
let mut previous = s0;
for (order, (_, stage)) in entry.input_stage_order().enumerate() {
let stage_fn = &stage.function.sig.ident;
let next = format_ident!("{}_s{}", stage_base, order + 1);
body.extend(quote! {
let #next = #stage_fn(env, #previous)
.map_err(|__e| <__JniErr as ::core::convert::From<String>>::from(
__e.to_string()))?;
});
previous = next;
}
quote!({ #body #previous })
}
fn composed_property_decode(
entry: &prebindgen_registry::TypeEntry<KotlinMeta>,
bind: &syn::Ident,
) -> TokenStream {
composed_entry_decode(entry, &format_ident!("{}_raw", bind), bind)
}
pub(crate) struct FlatLeaf {
pub native_ident: syn::Ident,
pub native_wire_ty: TokenStream,
pub kt_name: String,
pub kt_wire_ty: String,
pub kt_access_tail: String,
pub kt_access_prefix: String,
pub conv: Option<syn::Ident>,
pub field: Option<syn::Ident>,
pub is_present_flag: bool,
pub entry: Option<prebindgen_registry::TypeEntry<KotlinMeta>>,
pub handle_target_tail: Option<String>,
pub handle_nullable: bool,
}
impl FlatLeaf {
pub fn kt_access(&self, base: &str) -> String {
format!("{}{base}{}", self.kt_access_prefix, self.kt_access_tail)
}
pub fn kt_handle_target(&self, base: &str) -> Option<String> {
let tail = self.handle_target_tail.as_ref()?;
Some(format!("{}{base}{tail}", self.kt_access_prefix))
}
pub fn kt_call_arg(&self, base: &str) -> String {
if self.handle_target_tail.is_some() {
format!("{}_ptr", self.kt_name)
} else {
self.kt_access(base)
}
}
}
pub(crate) struct FlatStructNode {
pub struct_module: syn::Path,
pub struct_ident: syn::Ident,
pub binding: syn::Ident,
pub optional: bool,
pub present_ident: Option<syn::Ident>,
pub fields: Vec<FlatFieldNode>,
}
pub(crate) enum FlatFieldNode {
Value {
field: syn::Ident,
value_leaf: usize,
present_leaf: Option<usize>,
direct_handle: Option<Box<prebindgen_registry::flat::TypeRef>>,
optional_handle: bool,
rust_ty: Box<prebindgen_registry::flat::TypeRef>,
wrappers: Vec<&'static str>,
},
Nested {
field: syn::Ident,
node: Box<FlatStructNode>,
},
Sum {
field: syn::Ident,
tag_leaf: usize,
present_leaf: Option<usize>,
source: syn::Path,
variants: Vec<FlatSumVariant>,
rust_ty: Box<prebindgen_registry::flat::TypeRef>,
wrappers: Vec<&'static str>,
},
}
pub(crate) struct FlatSumVariant {
pub rust_ident: syn::Ident,
pub fields: Vec<(syn::Member, usize)>,
}
pub(crate) struct RebuildTarget {
arg: Vec<&'static str>,
under_borrow: Vec<&'static str>,
core: Vec<&'static str>,
pub by_ref: bool,
pub optional: bool,
}
impl RebuildTarget {
pub fn wrap_core(&self, e: TokenStream) -> TokenStream {
Self::wrap(&self.core, e)
}
pub fn wrap_optional(&self, e: TokenStream) -> TokenStream {
if !self.optional {
return e;
}
Self::wrap(&self.under_borrow, e)
}
pub fn wrap_arg(&self, e: TokenStream) -> TokenStream {
if !self.by_ref {
return e;
}
Self::wrap(&self.arg, e)
}
fn wrap(names: &[&'static str], e: TokenStream) -> TokenStream {
build_through_wrappers(names, e)
.expect("every layer was checked buildable when the plan was built")
}
}
fn rebuildable_target(arg: &TypeRef) -> Option<(RebuildTarget, &TypeRef)> {
let buildable = |t: &TypeRef| build_through_erased_wrappers(t, quote!(__probe)).map(|_| ());
buildable(arg)?;
let by_ref = arg.borrow_target().is_some();
let t1 = arg.borrow_target().unwrap_or(arg);
buildable(t1)?;
let optional = t1.optional_inner().is_some();
let inner = t1.optional_inner().unwrap_or(t1);
buildable(inner)?;
Some((
RebuildTarget {
arg: arg.erased_wrappers(),
under_borrow: t1.erased_wrappers(),
core: inner.erased_wrappers(),
by_ref,
optional,
},
inner,
))
}
pub(crate) struct FlatInputPlan {
pub leaves: Vec<FlatLeaf>,
pub root: FlatStructNode,
pub by_ref: bool,
pub contains_nested: bool,
pub target: RebuildTarget,
}
pub(crate) fn kt_leaf_default(sig: &str, nullable: bool) -> Option<String> {
if nullable {
return None;
}
Some(
match sig {
"Z" => "false",
"B" | "S" | "I" => "0",
"C" => "'\\u0000'",
"J" => "0L",
"F" => "0.0f",
"D" => "0.0",
"Ljava/lang/String;" => "\"\"",
other => {
if let Some(n) = crate::jni::wire_access::kotlin_array_of_descriptor(other) {
return Some(format!("{n}(0)"));
}
"null"
}
}
.to_string(),
)
}
#[derive(Clone, Debug)]
pub(crate) struct FlatInputError {
pub root: TypeKey,
pub path: String,
pub reason: String,
}
impl FlatInputError {
pub fn message(&self) -> String {
format!(
"data-class input `{}` cannot be flattened at `{}`: {} — fixed-layout data classes must flatten completely; declare `data_class!({}).jobject_input()` to opt this type into an explicit JObject boundary",
self.root, self.path, self.reason, self.root
)
}
}
fn flat_error(root: &TypeKey, path: &str, reason: impl Into<String>) -> FlatInputError {
FlatInputError {
root: root.clone(),
path: path.to_string(),
reason: reason.into(),
}
}
fn wire_kotlin_type(entry: &prebindgen_registry::TypeEntry<KotlinMeta>) -> String {
if let Some(p) = JniPrim::from_wire(&entry.destination) {
return p.kotlin_type().to_string();
}
if let syn::Type::Path(tp) = &entry.destination {
if let Some(last) = tp.path.segments.last() {
return match last.ident.to_string().as_str() {
"JString" => "String".to_string(),
"JByteArray" => "ByteArray".to_string(),
_ => entry
.metadata
.kotlin_name
.as_ref()
.map(ToString::to_string)
.unwrap_or_else(|| "Any".to_string()),
};
}
}
if matches!(entry.destination, syn::Type::Ptr(_)) {
"Long".to_string()
} else {
entry
.metadata
.kotlin_name
.as_ref()
.map(ToString::to_string)
.unwrap_or_else(|| "Any".to_string())
}
}
#[allow(clippy::too_many_arguments)]
fn build_flat_sum_field(
ext: &Declarations,
registry: &Registry<KotlinMeta>,
sum_reading: &TypeRef,
field: syn::Ident,
optional: bool,
native_prefix: &str,
field_ref: &str,
nullable_access: bool,
field_reading: &TypeRef,
leaves: &mut Vec<FlatLeaf>,
) -> Option<FlatFieldNode> {
let rust_ty = field_reading;
let ident = match sum_reading.unwrapped().kind() {
flat::TypeKind::Named { id, .. } => id.ident(),
_ => None,
}?;
let flat::Type::Variant(sum) = registry.flat().declared_type(&ident)? else {
return None;
};
let cfg = ext.types.get(&TypeKey::from_ident(&ident))?;
let sum_cfg = cfg.sum()?;
let iface_fqn = cfg.name_spec.as_ref().map(|s| ext.fqn_of(s))?;
struct Planned {
rust_ident: syn::Ident,
kotlin: String,
fields: Vec<(syn::Member, PlannedLeaf)>,
}
struct PlannedLeaf {
native: String,
entry: prebindgen_registry::TypeEntry<KotlinMeta>,
access_tail: String,
nullable_wire: bool,
}
let mut planned: Vec<Planned> = Vec::new();
for alt in &sum.alternatives {
let kotlin = ext.sum_variant_class_name(sum_cfg, &alt.name);
let mut fields = Vec::new();
for field in &alt.fields {
let entry = registry.input_entry(&field.ty)?;
if entry.metadata.projection.is_some() {
return None;
}
let prim = JniPrim::from_wire(&entry.destination);
let is_string_like = matches!(&entry.destination, syn::Type::Path(tp)
if tp.path.segments.last().is_some_and(|s| s.ident == "JString"));
if prim.is_none() && !is_string_like {
return None;
}
let member = field.member();
let prop = crate::jni::struct_plan::sum_field_prop_name(&member);
let slot = crate::jni::struct_plan::sum_slot_fragment(&kotlin, &prop);
let read = if ext.is_kotlin_enum_reading(&field.ty) {
format!("{prop}?.value")
} else {
prop.clone()
};
let cast = format!("{field_ref} as? {iface_fqn}.{kotlin})?.{read}");
let (access_tail, nullable_wire) = match &prim {
Some(p) => (format!("{cast} ?: {}", p.kotlin_zero()), false),
None => (cast, true),
};
fields.push((
member,
PlannedLeaf {
native: format!("{native_prefix}_{slot}"),
entry: entry.clone(),
access_tail,
nullable_wire,
},
));
}
planned.push(Planned {
rust_ident: alt.name.clone(),
kotlin,
fields,
});
}
let present_leaf = optional.then(|| {
push_present_leaf(
leaves,
&format!("{native_prefix}_present"),
format!("{field_ref} != null"),
Some(field.clone()),
)
});
let mut arms: Vec<String> = Vec::new();
if nullable_access || optional {
arms.push("null -> 0".to_string());
}
for (tag, p) in planned.iter().enumerate() {
arms.push(format!("is {iface_fqn}.{} -> {tag}", p.kotlin));
}
let tag_leaf = leaves.len();
leaves.push(FlatLeaf {
native_ident: format_ident!("{}__tag", native_prefix),
native_wire_ty: quote!(jni::sys::jint),
kt_name: snake_to_camel(&format!("{native_prefix}__tag")),
kt_wire_ty: "Int".to_string(),
kt_access_tail: format!("{field_ref}) {{ {} }}", arms.join("; ")),
kt_access_prefix: "when (".to_string(),
conv: None,
field: Some(field.clone()),
is_present_flag: false,
entry: None,
handle_target_tail: None,
handle_nullable: false,
});
let variants = planned
.into_iter()
.map(|p| FlatSumVariant {
rust_ident: p.rust_ident,
fields: p
.fields
.into_iter()
.map(|(member, l)| {
let idx = push_value_leaf(
leaves,
&l.native,
field.clone(),
&l.entry,
l.access_tail,
l.nullable_wire,
);
leaves[idx].kt_access_prefix = "(".to_string();
(member, idx)
})
.collect(),
})
.collect();
let module = ext.fn_module(registry, &ident);
Some(FlatFieldNode::Sum {
wrappers: field_reading.erased_wrappers(),
field,
tag_leaf,
present_leaf,
source: syn::parse_quote!(#module::#ident),
variants,
rust_ty: Box::new(rust_ty.clone()),
})
}
fn push_present_leaf(
leaves: &mut Vec<FlatLeaf>,
native: &str,
access: String,
field: Option<syn::Ident>,
) -> usize {
let index = leaves.len();
leaves.push(FlatLeaf {
native_ident: format_ident!("{native}"),
native_wire_ty: quote!(jni::sys::jboolean),
kt_name: snake_to_camel(native),
kt_wire_ty: "Boolean".to_string(),
kt_access_tail: access,
kt_access_prefix: String::new(),
conv: None,
field,
is_present_flag: true,
entry: None,
handle_target_tail: None,
handle_nullable: false,
});
index
}
fn push_value_leaf(
leaves: &mut Vec<FlatLeaf>,
native: &str,
field: syn::Ident,
entry: &prebindgen_registry::TypeEntry<KotlinMeta>,
access: String,
nullable_wire: bool,
) -> usize {
let wire = &entry.destination;
let mut kt_wire_ty = wire_kotlin_type(entry);
if nullable_wire && !kt_wire_ty.ends_with('?') {
kt_wire_ty.push('?');
}
let index = leaves.len();
leaves.push(FlatLeaf {
native_ident: format_ident!("{native}"),
native_wire_ty: annotate_jobject_with_lifetime(wire, "a").to_token_stream(),
kt_name: snake_to_camel(native),
kt_wire_ty,
kt_access_tail: access,
kt_access_prefix: String::new(),
conv: Some(entry.function.sig.ident.clone()),
field: Some(field),
is_present_flag: false,
entry: Some(entry.clone()),
handle_target_tail: None,
handle_nullable: false,
});
index
}
fn push_handle_leaf(
leaves: &mut Vec<FlatLeaf>,
native: &str,
field: syn::Ident,
target: String,
nullable: bool,
) -> usize {
let index = leaves.len();
leaves.push(FlatLeaf {
native_ident: format_ident!("{native}"),
native_wire_ty: quote!(jni::sys::jlong),
kt_name: snake_to_camel(native),
kt_wire_ty: "Long".to_string(),
kt_access_tail: target.clone(),
kt_access_prefix: String::new(),
conv: None,
field: Some(field),
is_present_flag: false,
entry: None,
handle_target_tail: Some(target),
handle_nullable: nullable,
});
index
}
pub(crate) fn build_flat_input_plan(
ext: &Declarations,
registry: &Registry<KotlinMeta>,
param_name: &syn::Ident,
arg: &TypeRef,
) -> Result<Option<FlatInputPlan>, FlatInputError> {
let Some((target, inner)) = rebuildable_target(arg) else {
return Ok(None);
};
let (by_ref, optional) = (target.by_ref, target.optional);
let flat::TypeKind::Named { id, .. } = inner.unwrapped().kind() else {
return Ok(None);
};
let Some(name) = id.ident() else {
return Ok(None);
};
let Some(st) = registry.flat().struct_type(&name) else {
return Ok(None);
};
let key = inner.stripped_key();
let Some(cfg) = ext.types.get(&key) else {
return Ok(None);
};
if cfg.special_decl() || cfg.name_spec.is_none() || cfg.jobject_input {
return Ok(None);
}
let Some(entry) = registry.input_entry(arg) else {
return Ok(None);
};
if entry.metadata.projection.is_some() {
return Ok(None);
}
let dc_short = cfg
.name_spec
.as_ref()
.map(|s| ext.fqn_of(s))
.map(|fqn| fqn.rsplit('.').next().unwrap_or(&fqn).to_string())
.unwrap_or_else(|| name.to_string());
let entry_short = entry
.metadata
.kotlin_name
.as_ref()
.and_then(|t| t.simple_name());
if entry_short != Some(dc_short.as_str()) {
return Ok(None);
}
let mut leaves: Vec<FlatLeaf> = Vec::new();
let mut stack = Vec::new();
let root = build_flat_struct_node(
ext,
registry,
st,
optional,
¶m_name.to_string(),
"",
optional,
&key,
&mut stack,
&mut leaves,
)?;
let contains_nested = root
.fields
.iter()
.any(|f| matches!(f, FlatFieldNode::Nested { .. }));
Ok(Some(FlatInputPlan {
leaves,
root,
by_ref,
contains_nested,
target,
}))
}
#[allow(clippy::too_many_arguments)]
fn build_flat_struct_node(
ext: &Declarations,
registry: &Registry<KotlinMeta>,
st: &flat::Struct,
optional: bool,
native_prefix: &str,
access_prefix: &str,
nullable_context: bool,
root: &TypeKey,
stack: &mut Vec<TypeKey>,
leaves: &mut Vec<FlatLeaf>,
) -> Result<FlatStructNode, FlatInputError> {
let node_key = TypeKey::from_ident(&st.name);
if stack.contains(&node_key) {
return Err(flat_error(
root,
native_prefix,
"recursive data-class cycle",
));
}
if stack.len() >= 16 {
return Err(flat_error(
root,
native_prefix,
"recursive flattening exceeds depth 16",
));
}
stack.push(node_key);
let present_ident = if optional {
let native = format!("{native_prefix}_present");
push_present_leaf(leaves, &native, format!("{access_prefix} != null"), None);
Some(format_ident!("{native}"))
} else {
None
};
let mut fields = Vec::new();
for field in &st.fields {
let Some(fident) = field.name.clone() else {
return Err(flat_error(
root,
native_prefix,
"only named-field structs can flatten",
));
};
let fcamel = kotlin_property_name(&fident);
let child_native = format!("{native_prefix}_{}", fident);
let field_ref = if nullable_context {
format!("{access_prefix}?.{fcamel}")
} else {
format!("{access_prefix}.{fcamel}")
};
let field_optional = field.ty.optional_inner().is_some();
let nested = field.ty.optional_inner().unwrap_or(&field.ty);
if matches!(ext.type_kind(registry, &nested.key()), TypeKind::Sum) {
if let Some(node) = build_flat_sum_field(
ext,
registry,
nested,
fident.clone(),
field_optional,
&child_native,
&field_ref,
nullable_context,
&field.ty,
leaves,
) {
fields.push(node);
continue;
}
}
if let TypeKind::DataStruct {
st: child,
cfg: Some(cfg),
} = ext.type_kind(registry, &nested.key())
{
if cfg.name_spec.is_some() && !cfg.special_decl() && !cfg.jobject_input {
let child_optional = field_optional;
let node = build_flat_struct_node(
ext,
registry,
child,
child_optional,
&child_native,
&field_ref,
nullable_context || child_optional,
root,
stack,
leaves,
)?;
fields.push(FlatFieldNode::Nested {
field: fident,
node: Box::new(node),
});
continue;
}
}
let path = child_native.clone();
let Some(fentry) = registry.input_entry(&field.ty) else {
return Err(flat_error(
root,
&path,
format!("field type `{}` has no input converter", field.ty.key()),
));
};
if let Some(inner_reading) = field.ty.optional_inner() {
if inner_reading.borrow_target().is_none() {
if let Some(inner) = registry.input_entry(inner_reading) {
if let Some(prim) = JniPrim::from_wire(&inner.destination) {
if inner.niches.clone().carve().is_none()
&& inner.metadata.projection.is_none()
&& inner.pre_stages.is_empty()
{
let present_index = push_present_leaf(
leaves,
&format!("{child_native}_present"),
format!("{field_ref} != null"),
Some(fident.clone()),
);
let value_access = if ext.is_kotlin_enum_reading(inner_reading) {
format!("{field_ref}?.value ?: {}", prim.kotlin_zero())
} else {
format!("{field_ref} ?: {}", prim.kotlin_zero())
};
let value_index = push_value_leaf(
leaves,
&format!("{child_native}_value"),
fident.clone(),
inner,
value_access,
false,
);
fields.push(FlatFieldNode::Value {
field: fident,
value_leaf: value_index,
present_leaf: Some(present_index),
direct_handle: None,
optional_handle: false,
rust_ty: Box::new(field.ty.clone()),
wrappers: field.ty.erased_wrappers(),
});
continue;
}
}
}
}
}
if let Some(proj) = &fentry.metadata.projection {
if proj.kind == ProjectionKind::Unsigned64 {
if let Some(inner_reading) = field.ty.optional_inner() {
if JniPrim::from_wire(&fentry.destination).is_none() {
let inner = registry.input_entry(inner_reading).ok_or_else(|| {
flat_error(
root,
&path,
format!(
"unsigned field representation `{}` has no input converter",
inner_reading.key()
),
)
})?;
let present_index = push_present_leaf(
leaves,
&format!("{child_native}_present"),
format!("{field_ref} != null"),
Some(fident.clone()),
);
let value_index = push_value_leaf(
leaves,
&format!("{child_native}_value"),
fident.clone(),
inner,
format!("{field_ref}?.toLong() ?: 0L"),
false,
);
fields.push(FlatFieldNode::Value {
field: fident,
value_leaf: value_index,
present_leaf: Some(present_index),
direct_handle: None,
optional_handle: false,
rust_ty: Box::new(field.ty.clone()),
wrappers: field.ty.erased_wrappers(),
});
continue;
}
}
}
match proj.kind {
ProjectionKind::Handle => {
if matches!(proj.strategy, FoldStrategy::Iterable(_)) {
return Err(flat_error(
root,
&path,
"collections of handles retain their collection boundary",
));
}
let optional_handle = field_optional;
let value_index = push_handle_leaf(
leaves,
&child_native,
fident.clone(),
field_ref,
nullable_context || optional_handle,
);
fields.push(FlatFieldNode::Value {
field: fident,
value_leaf: value_index,
present_leaf: None,
direct_handle: Some(Box::new(nested.clone())),
optional_handle,
rust_ty: Box::new(field.ty.clone()),
wrappers: field.ty.erased_wrappers(),
});
continue;
}
ProjectionKind::Unsigned64 => {
let is_opt = field_optional;
let access = if is_opt || nullable_context {
let sentinel = proj
.niche_sentinels
.first()
.cloned()
.unwrap_or_else(|| "0L".to_string());
format!("{field_ref}?.toLong() ?: {sentinel}")
} else {
format!("{field_ref}.toLong()")
};
let value_index = push_value_leaf(
leaves,
&child_native,
fident.clone(),
fentry,
access,
false,
);
fields.push(FlatFieldNode::Value {
field: fident,
value_leaf: value_index,
present_leaf: None,
direct_handle: None,
optional_handle: false,
rust_ty: Box::new(field.ty.clone()),
wrappers: field.ty.erased_wrappers(),
});
continue;
}
}
}
let field_is_option = field_optional;
let mut enum_coalesced = false;
let mut access = if ext.is_kotlin_enum_reading(&field.ty) {
if field_is_option || nullable_context {
enum_coalesced = true;
format!("{field_ref}?.value ?: 0")
} else {
format!("{field_ref}.value")
}
} else {
field_ref.clone()
};
if nullable_context && !field_is_option && !enum_coalesced {
if let Some((sig, _, _)) = jni_field_access(&fentry.destination) {
if let Some(default) = kt_leaf_default(sig, false) {
access = format!("{access} ?: {default}");
}
}
}
let value_index = push_value_leaf(
leaves,
&child_native,
fident.clone(),
fentry,
access,
(field_is_option || nullable_context) && is_jobject_shaped_wire(&fentry.destination),
);
fields.push(FlatFieldNode::Value {
field: fident,
value_leaf: value_index,
present_leaf: None,
direct_handle: None,
optional_handle: false,
rust_ty: Box::new(field.ty.clone()),
wrappers: field.ty.erased_wrappers(),
});
}
stack.pop();
Ok(FlatStructNode {
struct_module: struct_module_path(ext, registry, &st.name),
struct_ident: st.name.clone(),
binding: format_ident!("__flat_{native_prefix}"),
optional,
present_ident,
fields,
})
}
pub(crate) fn render_flat_input_decode(
plan: &FlatInputPlan,
arg_ident: &syn::Ident,
on_err: &TokenStream,
emit: &prebindgen_registry::Emit,
) -> (TokenStream, TokenStream) {
let reconstruct = render_flat_struct_node(plan, &plan.root, Some(&plan.target), on_err, emit);
let root_binding = &plan.root.binding;
let prelude = quote! {
#reconstruct
let #arg_ident = #root_binding;
};
let borrowed = if plan.by_ref {
quote!(&#arg_ident)
} else {
quote!(#arg_ident)
};
(prelude, plan.target.wrap_arg(borrowed))
}
fn render_entry_decode(
entry: &prebindgen_registry::TypeEntry<KotlinMeta>,
wire_ident: &syn::Ident,
out_ident: &syn::Ident,
on_err: &TokenStream,
) -> TokenStream {
let conv = entry.converter_ident();
let decode_call = if matches!(entry.destination, syn::Type::Ptr(_)) {
quote!(#conv(&mut env, #wire_ident))
} else {
quote!(#conv(&mut env, &#wire_ident))
};
let route = |expr: TokenStream| {
quote! {
match #expr {
::core::result::Result::Ok(__v) => __v,
::core::result::Result::Err(__e) => {
signal_binding_error(&mut env, &__error_sink, &__SINK_MID, __SINK_FQN, __SINK_DESCR, &__e.to_string());
return #on_err;
}
}
}
};
if entry.pre_stages.is_empty() {
let decoded = route(decode_call);
return quote!(let #out_ident = #decoded;);
}
let stage0 = format_ident!("{}_s0", out_ident);
let decoded = route(decode_call);
let mut body = quote!(let #stage0 = #decoded;);
let mut previous = stage0;
let n = entry.pre_stages.len();
for (idx, stage) in entry.input_stage_order() {
let stage_fn = &stage.function.sig.ident;
let next = if idx == 0 {
out_ident.clone()
} else {
format_ident!("{}_s{}", out_ident, n - idx)
};
let converted = route(quote!(#stage_fn(&mut env, #previous)));
body.extend(quote!(let #next = #converted;));
previous = next;
}
body
}
fn render_flat_struct_node(
plan: &FlatInputPlan,
node: &FlatStructNode,
target: Option<&RebuildTarget>,
on_err: &TokenStream,
emit: &prebindgen_registry::Emit,
) -> TokenStream {
let mut decodes = TokenStream::new();
let mut inits = Vec::new();
for field in &node.fields {
match field {
FlatFieldNode::Nested { field, node: child } => {
decodes.extend(render_flat_struct_node(plan, child, None, on_err, emit));
let child_binding = &child.binding;
inits.push(quote!(#field: #child_binding));
}
FlatFieldNode::Sum {
wrappers,
field,
tag_leaf,
present_leaf,
source,
variants,
rust_ty,
} => {
let tmp = format_ident!("{}_{}", node.binding, field);
let rust_ty = emit.spell(rust_ty);
let tag = &plan.leaves[*tag_leaf].native_ident;
let arms = variants.iter().enumerate().map(|(t, v)| {
let vident = &v.rust_ident;
let tag_lit = proc_macro2::Literal::i32_unsuffixed(t as i32);
let mut pre = TokenStream::new();
let mut inits: Vec<TokenStream> = Vec::new();
for (member, leaf_idx) in &v.fields {
let leaf = &plan.leaves[*leaf_idx];
let entry = leaf.entry.as_ref().expect("sum payload leaf has an entry");
let wire = &leaf.native_ident;
let bind = format_ident!("{}_{}", tmp, wire);
pre.extend(render_entry_decode(entry, wire, &bind, on_err));
match member {
syn::Member::Named(n) => inits.push(quote!(#n: #bind)),
syn::Member::Unnamed(_) => inits.push(quote!(#bind)),
}
}
let ctor = if v.fields.is_empty() {
quote!(#source::#vident)
} else if matches!(v.fields[0].0, syn::Member::Named(_)) {
quote!(#source::#vident { #(#inits),* })
} else {
quote!(#source::#vident(#(#inits),*))
};
quote! { #tag_lit => { #pre #ctor } }
});
let bad_tag = format!(
"{}: invalid tag",
source
.segments
.last()
.map(|s| s.ident.to_string())
.unwrap_or_default()
);
let build = quote! {
match #tag {
#(#arms)*
_ => {
signal_binding_error(&mut env, &__error_sink, &__SINK_MID, __SINK_FQN, __SINK_DESCR, #bad_tag);
return #on_err;
}
}
};
let wrap = |e: TokenStream| {
build_through_wrappers(wrappers, e)
.expect("a field spelling the plan accepted is buildable")
};
if let Some(p) = present_leaf {
let present = &plan.leaves[*p].native_ident;
let gated = wrap(quote! {
if #present != 0u8 {
::core::option::Option::Some(#build)
} else {
::core::option::Option::None
}
});
decodes.extend(quote! { let #tmp: #rust_ty = #gated; });
} else {
let built = wrap(build);
decodes.extend(quote! { let #tmp: #rust_ty = #built; });
}
inits.push(quote!(#field: #tmp));
}
FlatFieldNode::Value {
wrappers,
field,
value_leaf,
present_leaf,
direct_handle,
optional_handle,
rust_ty,
} => {
let leaf = &plan.leaves[*value_leaf];
let wire = &leaf.native_ident;
let tmp = format_ident!("{}_{}", node.binding, field);
let rust_ty = emit.spell(rust_ty);
let wrap = |e: TokenStream| {
build_through_wrappers(wrappers, e)
.expect("a field spelling the plan accepted is buildable")
};
if let Some(target) = direct_handle {
let target_ty = emit.spell(target);
if *optional_handle {
let gated = wrap(quote! {
if #wire == 0 {
::core::option::Option::None
} else {
if (#wire & 1) == 1 {
signal_binding_error(&mut env, &__error_sink, &__SINK_MID, __SINK_FQN, __SINK_DESCR, "Operation on a closed native handle.");
return #on_err;
}
::core::option::Option::Some(unsafe {
*::std::boxed::Box::from_raw(#wire as *mut #target_ty)
})
}
});
decodes.extend(quote! { let #tmp: #rust_ty = #gated; });
} else {
decodes.extend(quote! {
if #wire == 0 || (#wire & 1) == 1 {
signal_binding_error(&mut env, &__error_sink, &__SINK_MID, __SINK_FQN, __SINK_DESCR, "Operation on a closed native handle.");
return #on_err;
}
let #tmp: #rust_ty = unsafe {
*::std::boxed::Box::from_raw(#wire as *mut #rust_ty)
};
});
}
} else {
let entry = leaf
.entry
.as_ref()
.expect("ordinary leaf has converter entry");
if let Some(present_index) = present_leaf {
let present = &plan.leaves[*present_index].native_ident;
let inner_tmp = format_ident!("{}_value", tmp);
let decode = render_entry_decode(entry, wire, &inner_tmp, on_err);
let gated = wrap(quote! {
if #present != 0u8 {
#decode
::core::option::Option::Some(#inner_tmp)
} else {
::core::option::Option::None
}
});
decodes.extend(quote! { let #tmp = #gated; });
} else {
decodes.extend(render_entry_decode(entry, wire, &tmp, on_err));
}
}
inits.push(quote!(#field: #tmp));
}
}
}
let module = &node.struct_module;
let sid = &node.struct_ident;
let binding = &node.binding;
let built = match target {
Some(t) => t.wrap_core(quote!(#module::#sid { #(#inits),* })),
None => quote!(#module::#sid { #(#inits),* }),
};
let outer = |e: TokenStream| match target {
Some(t) => t.wrap_optional(e),
None => e,
};
if node.optional {
let present = node.present_ident.as_ref().expect("optional node has gate");
let gate = outer(quote! {
if #present != 0u8 {
#decodes
::core::option::Option::Some(#built)
} else {
::core::option::Option::None
}
});
quote! {
let #binding = #gate;
}
} else {
let value = outer(built);
quote! {
#decodes
let #binding = #value;
}
}
}
pub(crate) struct OptionScalarInputPlan {
pub present_ident: syn::Ident,
pub value_ident: syn::Ident,
pub value_wire: syn::Type,
pub inner_conv: syn::Ident,
pub present_kt: String,
pub value_kt: String,
pub value_kt_type: String,
pub value_kt_zero: String,
pub arg_wrappers: Vec<&'static str>,
pub is_enum: bool,
}
pub(crate) fn build_option_scalar_input_plan(
ext: &Declarations,
registry: &Registry<KotlinMeta>,
param_name: &syn::Ident,
arg: &TypeRef,
) -> Option<OptionScalarInputPlan> {
build_through_erased_wrappers(arg, quote!(__probe))?;
let inner = arg.optional_inner()?;
if inner.borrow_target().is_some() {
return None;
}
let inner_entry = registry.input_entry(inner)?;
let value_wire = inner_entry.destination.clone();
let prim = JniPrim::from_wire(&value_wire)?;
if inner_entry.niches.clone().carve().is_some() {
return None;
}
if inner_entry.metadata.projection.is_some() {
return None;
}
if !inner_entry.pre_stages.is_empty() {
return None;
}
let is_enum = ext.is_kotlin_enum_reading(inner);
Some(OptionScalarInputPlan {
present_ident: format_ident!("{}_present", param_name),
value_ident: format_ident!("{}_value", param_name),
value_wire,
inner_conv: inner_entry.function.sig.ident.clone(),
present_kt: snake_to_camel(&format!("{}_present", param_name)),
value_kt: snake_to_camel(&format!("{}_value", param_name)),
value_kt_type: prim.kotlin_type().to_string(),
value_kt_zero: prim.kotlin_zero().to_string(),
is_enum,
arg_wrappers: arg.erased_wrappers(),
})
}