use kotlin_codegen::KtType;
use prebindgen_registry::{flat::TypeRef, Conversions};
use super::*;
pub(crate) struct JniFunctionPlan {
pub jni_method: String,
pub native_symbol: String,
pub onerror_iface: Option<ErrorIfaces>,
pub params: Vec<PlanParam>,
pub output: FnOutputPlan,
}
pub(crate) struct PlanParam {
pub ident: syn::Ident,
pub ty: prebindgen_registry::flat::TypeRef,
pub form: ParamForm,
}
pub(crate) enum ParamForm {
Single(Box<PlanLeaf>),
Expanded(Vec<PlanLeaf>),
}
pub(crate) struct PlanLeaf {
pub reading: TypeRef,
pub kt_name: String,
pub kt_public: Option<KtType>,
pub kt_meta: Option<KtType>,
pub optional: bool,
pub as_enum_value: bool,
pub kind: InputKind,
}
pub(crate) enum InputKind {
Callback { iface: Option<Arc<IfaceSpec>> },
VecBuild {
elem: TypeRef,
by_ref: bool,
elem_wrappers: Vec<&'static str>,
},
OptionScalar(OptionScalarInputPlan),
FlattenStruct(FlatInputPlan),
Handle { direct: bool },
Unsigned64 { niche: Option<String> },
Plain,
}
pub(crate) enum FnOutputPlan {
Unfold(UnfoldOutputPlan),
Value(Box<ValueOutputPlan>),
}
pub(crate) struct UnfoldOutputPlan {
pub iterable_fold: bool,
pub optional: bool,
pub fixed_builder: bool,
pub whole_element: bool,
pub generic: Option<&'static str>,
pub iface: Option<Arc<IfaceSpec>>,
}
pub(crate) struct ValueOutputPlan {
pub is_convert: bool,
pub target_ty: prebindgen_registry::flat::TypeRef,
pub wire_ty: syn::Type,
pub surface: ReturnSurface,
pub is_enum: bool,
pub is_option_enum: bool,
}
pub(crate) enum ReturnSurface {
Skip,
Unit,
Projected {
projection: Projection,
leaf_fqn: Option<String>,
},
Plain { kt: KtType },
}
#[derive(Debug)]
pub(crate) enum PlanError {
Unresolved { ty: Box<TypeRef> },
UnresolvedLeaf { ty: Box<TypeRef>, param: syn::Ident },
UnresolvedOutput { ty: Box<TypeRef> },
UnknownOutputType { ty: TypeKey },
UnflattenableDataClass(FlatInputError),
JvmParameterLimit { slots: usize },
}
impl PlanError {
fn location_suffix(&self) -> String {
let reading = match self {
PlanError::Unresolved { ty }
| PlanError::UnresolvedLeaf { ty, .. }
| PlanError::UnresolvedOutput { ty } => ty,
PlanError::UnknownOutputType { .. }
| PlanError::UnflattenableDataClass(_)
| PlanError::JvmParameterLimit { .. } => return String::new(),
};
let loc = reading.location();
if loc.has_position() {
format!(" (declared at {loc})")
} else {
String::new()
}
}
pub fn message(&self, fn_ident: &syn::Ident) -> String {
let at = self.location_suffix();
match self {
PlanError::Unresolved { ty } => format!(
"JniGen::on_function: input type `{}` for `{}` is unresolved{at}",
ty.key(),
fn_ident,
),
PlanError::UnresolvedLeaf { ty, param } => format!(
"JniGen expand: leaf type `{}` (parameter `{}`) is unresolved{at}",
ty.key(),
param,
),
PlanError::UnresolvedOutput { ty } => format!(
"JniGen::on_function: return type `{}` of `{}` has no registered output \
converter — register one via `Declarations::output_wrapper(pat, |…| Some((ty, exc, body)))` \
(exc = `None` for non-throwing, `Some(parse_quote!(<full path>))` \
to bind a domain exception){at}",
ty.key(),
fn_ident,
),
PlanError::UnknownOutputType { ty } => format!(
"JniGen::on_function: return type `{}` of `{}` is not registered — the \
resolver never saw this type, so no converter can be selected for it. \
Declare the type (or the function that produces it) before binding `{}`",
ty, fn_ident, fn_ident,
),
PlanError::UnflattenableDataClass(error) => {
format!("JniGen::on_function `{fn_ident}`: {}", error.message())
}
PlanError::JvmParameterLimit { slots } => format!(
"JniGen::on_function `{fn_ident}`: flattened JNI signature uses {slots} JVM parameter slots (maximum 255, including the JNINative receiver); reduce the data-class shape or declare an intentional `data_class!(T).jobject_input()` boundary"
),
}
}
}
pub(crate) fn validate_bindings(
ext: &Declarations,
registry: &Registry<KotlinMeta>,
) -> Result<(), String> {
let mut errors: Vec<String> = Vec::new();
if let Err(e) = ext.validate_split_declarations(registry) {
errors.push(e);
}
let mut native: std::collections::BTreeMap<NativeSymbol, String> = Default::default();
let mut record_symbol = |sym: &str, origin: String, errors: &mut Vec<String>| {
let key = NativeSymbol::new(sym);
if let Some(prev) = native.insert(key, origin.clone()) {
errors.push(format!(
"duplicate native symbol `{sym}`: produced by both `{prev}` and `{origin}` \
— a name mangle hook or `.name()` collapsed two distinct methods onto one \
JNI export",
));
}
};
let declared = ext.declared_functions();
let mut fns: Vec<&prebindgen_registry::flat::Function> = registry.flat().functions().collect();
fns.sort_by(|a, b| a.name.cmp(&b.name));
for f in fns {
let ident = &f.name;
if !declared.contains(ident) {
continue;
}
match ext.fn_plan(registry, f) {
Ok(plan) => record_symbol(&plan.native_symbol, ident.to_string(), &mut errors),
Err(e) => errors.push(e.message(ident)),
}
}
if let Some(declared_consts) = ext.declared_consts() {
let mut consts: Vec<&prebindgen_registry::flat::Constant> =
registry.flat().constants().collect();
consts.sort_by(|a, b| a.name.cmp(&b.name));
for c in consts {
let ident = &c.name;
if !declared_consts.contains(ident) {
continue;
}
let getter = const_getter_fn(c);
match ext.fn_plan(registry, &getter) {
Ok(plan) => record_symbol(&plan.native_symbol, ident.to_string(), &mut errors),
Err(e) => errors.push(e.message(&getter.name)),
}
}
}
let mut expr_decls: Vec<_> = ext
.packages
.values()
.flat_map(|p| &p.constant_exprs)
.collect();
expr_decls.sort_by(|a, b| a.kotlin_name.cmp(&b.kotlin_name));
for decl in expr_decls {
let getter = const_expr_getter_fn(&decl.kotlin_name, &decl.ty, registry);
match ext.fn_plan(registry, &getter) {
Ok(plan) => record_symbol(&plan.native_symbol, decl.kotlin_name.clone(), &mut errors),
Err(e) => errors.push(e.message(&getter.name)),
}
}
errors.extend(validate_symbols(ext, registry));
if errors.is_empty() {
Ok(())
} else {
Err(errors.join("\n"))
}
}
impl FnOutputPlan {
pub fn wire_ty(&self) -> syn::Type {
match self {
FnOutputPlan::Unfold(_) => syn::parse_quote!(jni::objects::JObject),
FnOutputPlan::Value(v) => v.wire_ty.clone(),
}
}
}
impl Declarations {
pub(crate) fn fn_plan(
&self,
registry: &Registry<KotlinMeta>,
f: &prebindgen_registry::flat::Function,
) -> Result<std::rc::Rc<JniFunctionPlan>, PlanError> {
if let Some(hit) = self.fn_plans.borrow().get(&f.name).cloned() {
return Ok(hit);
}
let plan = std::rc::Rc::new(JniFunctionPlan::build(self, registry, f)?);
self.fn_plans
.borrow_mut()
.insert(f.name.clone(), plan.clone());
Ok(plan)
}
}
impl JniFunctionPlan {
pub fn build(
ext: &Declarations,
registry: &Registry<KotlinMeta>,
f: &prebindgen_registry::flat::Function,
) -> Result<Self, PlanError> {
let jni_method = ext.mangle_jni_method(&kt_snake_to_camel(&f.name.to_string()));
let native_symbol = ext.native_method_symbol(&jni_method);
let onerror_iface = onerror_iface_spec(ext, registry, &f.name);
let output = build_output(ext, registry, f)?;
let mut params = Vec::new();
for param in &f.params {
let ident = param.name.clone();
let ty = param.ty.clone();
let form = if let Some(plan) = registry
.expansion_plans()
.get(&(f.name.clone(), ident.clone()))
{
let mut leaves = Vec::new();
for leaf in &plan.leaves {
leaves.push(classify_leaf(
ext, registry, &leaf.name, &leaf.ty, true, &ident,
)?);
}
ParamForm::Expanded(leaves)
} else {
ParamForm::Single(Box::new(classify_leaf(
ext, registry, &ident, ¶m.ty, false, &ident,
)?))
};
params.push(PlanParam { ident, ty, form });
}
let result = Self {
jni_method,
native_symbol,
onerror_iface,
params,
output,
};
let slots = result.jvm_parameter_slots(registry, f);
if slots > 255 {
return Err(PlanError::JvmParameterLimit { slots });
}
Ok(result)
}
pub fn leaves(&self) -> impl Iterator<Item = &PlanLeaf> {
self.params.iter().flat_map(|p| match &p.form {
ParamForm::Single(l) => std::slice::from_ref(&**l).iter(),
ParamForm::Expanded(ls) => ls.iter(),
})
}
fn jvm_parameter_slots(
&self,
registry: &Registry<KotlinMeta>,
f: &prebindgen_registry::flat::Function,
) -> usize {
let mut slots = 1usize;
for leaf in self.leaves() {
slots += match &leaf.kind {
InputKind::FlattenStruct(plan) => plan
.leaves
.iter()
.map(|l| kotlin_jvm_slots(&l.kt_wire_ty))
.sum(),
InputKind::OptionScalar(plan) => 1 + kotlin_jvm_slots(&plan.value_kt_type),
InputKind::Handle { .. } | InputKind::VecBuild { .. } => 2,
InputKind::Callback { .. } => 1,
InputKind::Unsigned64 { .. } | InputKind::Plain => registry
.input_entry(&leaf.reading)
.and_then(|entry| JniPrim::from_wire(&entry.destination))
.map_or(1, |prim| match prim {
JniPrim::Long | JniPrim::Double => 2,
_ => 1,
}),
};
}
slots += match &self.output {
FnOutputPlan::Unfold(plan) if plan.iterable_fold => 2,
FnOutputPlan::Unfold(_) => 1,
FnOutputPlan::Value(_) => 0,
};
slots += 1; if registry.error_plans().contains_key(&f.name) {
slots += 1;
}
slots
}
}
fn kotlin_jvm_slots(ty: &str) -> usize {
if !ty.ends_with('?') && matches!(ty, "Long" | "Double") {
2
} else {
1
}
}
fn classify_leaf(
ext: &Declarations,
registry: &Registry<KotlinMeta>,
ident: &syn::Ident,
reading: &TypeRef,
expanded: bool,
source_param: &syn::Ident,
) -> Result<PlanLeaf, PlanError> {
let optional = reading.optional_inner().is_some();
let as_enum_value = ext.is_kotlin_enum_reading(reading);
let kt_name = kt_param_name(&ident.to_string());
if let Some(args) = reading.callback_args() {
let iface = ext.iface_spec(registry, &SpecKey::callback(args));
return Ok(PlanLeaf {
reading: reading.clone(),
kt_name,
kt_public: None,
kt_meta: registry
.input_entry(reading)
.and_then(|e| e.metadata.kotlin_name.clone()),
optional,
as_enum_value,
kind: InputKind::Callback { iface },
});
}
let Some(entry) = registry.input_entry(reading) else {
return Err(if expanded {
PlanError::UnresolvedLeaf {
ty: Box::new(reading.clone()),
param: source_param.clone(),
}
} else {
PlanError::Unresolved {
ty: Box::new(reading.clone()),
}
});
};
let flat_plan = build_flat_input_plan(ext, registry, ident, reading)
.map_err(PlanError::UnflattenableDataClass)?;
let kind = if let Some(v) = (!expanded)
.then(|| vec_build_elem(ext, registry, reading))
.flatten()
{
InputKind::VecBuild {
elem: v.elem,
by_ref: v.by_ref,
elem_wrappers: v.elem_wrappers,
}
} else if let Some(sp) = build_option_scalar_input_plan(ext, registry, ident, reading) {
InputKind::OptionScalar(sp)
} else if let Some(plan) = flat_plan {
InputKind::FlattenStruct(plan)
} else {
match entry.metadata.projection.as_ref().map(|p| p.kind.clone()) {
Some(ProjectionKind::Handle) => InputKind::Handle {
direct: entry.metadata.is_direct_handle(),
},
Some(ProjectionKind::Unsigned64) => InputKind::Unsigned64 {
niche: entry.metadata.projection.as_ref().and_then(|p| {
reading
.optional_inner()
.is_some()
.then(|| p.niche_sentinels.first().cloned())
.flatten()
}),
},
None => InputKind::Plain,
}
};
let kt_meta = entry.metadata.kotlin_name.clone();
let kt_public = match entry.metadata.projection.as_ref() {
Some(p) => projection_leaf_kt(ext, p),
None => kt_meta.clone(),
};
Ok(PlanLeaf {
reading: reading.clone(),
kt_name,
kt_public,
kt_meta,
optional,
as_enum_value,
kind,
})
}
fn build_output(
ext: &Declarations,
registry: &Registry<KotlinMeta>,
f: &prebindgen_registry::flat::Function,
) -> Result<FnOutputPlan, PlanError> {
use prebindgen_registry::unfold::{Delivery, UnfoldShape};
let ident = &f.name;
let unfold_plan = registry.unfold_plans().get(ident);
if let Some(plan) = unfold_plan.filter(|p| p.delivery == Delivery::Callback) {
let iterable_fold = super::is_iterable_fold(&plan.shape);
let optional = matches!(plan.shape, UnfoldShape::Optional(..));
let fixed_builder = plan.fixed_builder;
let generic = if fixed_builder {
None
} else if iterable_fold {
Some("A")
} else {
Some("R")
};
let iface = if iterable_fold {
folder_iface_for_plan(ext, registry, plan)
} else {
let decon = plan
.decon
.clone()
.expect("record-built plan carries its DeconId");
ext.iface_spec(registry, &SpecKey::Builder(decon))
};
return Ok(FnOutputPlan::Unfold(UnfoldOutputPlan {
iterable_fold,
optional,
fixed_builder,
whole_element: plan.element.is_some(),
generic,
iface,
}));
}
let is_convert = unfold_plan.is_some();
let error_plan = registry.error_plans().get(ident);
let ok_ty = error_plan
.and_then(|_| f.ret.fallible_parts())
.map(|(ok, _)| ok);
let target_ty: &prebindgen_registry::flat::TypeRef = match unfold_plan {
Some(p) => p
.convert_out_ty
.as_ref()
.expect("Return delivery carries convert_out_ty"),
None => ok_ty.unwrap_or(&f.ret),
};
let Some(target) = registry.reading(&target_ty.key()) else {
return Err(PlanError::UnknownOutputType {
ty: target_ty.key(),
});
};
let Some(entry) = registry.output_entry(&target) else {
return Err(PlanError::UnresolvedOutput {
ty: Box::new(target),
});
};
let wire_ty = entry.destination.clone();
let ret_decl = if is_convert { target_ty } else { &f.ret };
let (surface, enums) = ReturnSurface::classify(ext, registry, ret_decl);
let EnumSurface {
is_enum,
is_option_enum,
} = enums;
Ok(FnOutputPlan::Value(Box::new(ValueOutputPlan {
is_convert,
target_ty: target_ty.clone(),
wire_ty,
surface,
is_enum,
is_option_enum,
})))
}
#[derive(Clone, Copy)]
pub(crate) struct EnumSurface {
pub is_enum: bool,
pub is_option_enum: bool,
}
impl ReturnSurface {
pub fn classify(
ext: &Declarations,
registry: &impl Conversions<KotlinMeta>,
ret: &prebindgen_registry::flat::TypeRef,
) -> (Self, EnumSurface) {
let outer_meta = registry.output_entry(ret).map(|e| e.metadata.clone());
let stored = outer_meta.as_ref().and_then(|m| m.value_rust_type.as_ref());
let is_unit = match stored {
Some(t) => crate::util::is_unit(t),
None => matches!(ret.kind(), prebindgen_registry::flat::TypeKind::Unit),
};
let enum_probe = |t: &syn::Type| ext.is_kotlin_enum(t);
let (is_enum, is_option_enum) = match stored {
Some(t) => (
enum_probe(t),
prebindgen_registry::types_util::option_inner_type(t)
.map(|inner| enum_probe(&inner))
.unwrap_or(false),
),
None => (
ext.is_kotlin_enum_key(&ret.key()),
ret.optional_inner()
.map(|inner| ext.is_kotlin_enum_key(&inner.key()))
.unwrap_or(false),
),
};
let canonical = EnumSurface {
is_enum,
is_option_enum,
};
if is_unit {
return (Self::Unit, canonical);
}
if let Some(h) = outer_meta.as_ref().and_then(|m| m.projection.clone()) {
let leaf_fqn = projection_leaf_kt(ext, &h).map(|t| t.to_string());
return (
Self::Projected {
projection: h,
leaf_fqn,
},
canonical,
);
}
match outer_meta.and_then(|m| m.kotlin_name) {
Some(kt) => (Self::Plain { kt }, canonical),
None => (Self::Skip, canonical),
}
}
}