use alef::backends::jni::JniBackend;
use alef::backends::kotlin_android::KotlinAndroidBackend;
use alef::core::backend::{Backend, GeneratedFile};
use alef::core::config::{JniConfig, KotlinAndroidConfig, ResolvedCrateConfig};
use alef::core::ir::{ApiSurface, FunctionDef, MethodDef, ParamDef, ReceiverKind, TypeDef, TypeRef};
use std::collections::BTreeSet;
const CRATE_NAME: &str = "sample_llm";
const CLIENT_TYPE: &str = "Client";
const RESULT_TYPE: &str = "SingleflightResult";
fn base_surface() -> ApiSurface {
let client = TypeDef {
name: CLIENT_TYPE.to_owned(),
rust_path: format!("{CRATE_NAME}::{CLIENT_TYPE}"),
is_opaque: true,
methods: vec![MethodDef {
name: "get_or_fetch".to_owned(),
params: vec![ParamDef {
name: "key".to_owned(),
ty: TypeRef::String,
..ParamDef::default()
}],
return_type: TypeRef::Named(RESULT_TYPE.to_owned()),
receiver: Some(ReceiverKind::Ref),
..MethodDef::default()
}],
..TypeDef::default()
};
let result = TypeDef {
name: RESULT_TYPE.to_owned(),
rust_path: format!("{CRATE_NAME}::{RESULT_TYPE}"),
is_opaque: true,
..TypeDef::default()
};
ApiSurface {
crate_name: CRATE_NAME.to_owned(),
version: "0.1.0".to_owned(),
types: vec![client, result],
..ApiSurface::default()
}
}
fn base_config() -> ResolvedCrateConfig {
ResolvedCrateConfig {
name: CRATE_NAME.to_owned(),
kotlin_android: Some(KotlinAndroidConfig {
package: Some("dev.sample_llm".to_owned()),
namespace: Some("dev.sample_llm".to_owned()),
..KotlinAndroidConfig::default()
}),
..ResolvedCrateConfig::default()
}
}
fn generate(api: &ApiSurface, config: &ResolvedCrateConfig) -> (Vec<GeneratedFile>, String) {
let kotlin_files = KotlinAndroidBackend
.generate_bindings(api, config)
.expect("kotlin_android generation should succeed");
let jni_files = JniBackend
.generate_bindings(api, config)
.expect("jni generation should succeed");
let jni_source = jni_files
.into_iter()
.find(|file| file.path.file_name().is_some_and(|name| name == "lib.rs"))
.expect("jni backend must emit lib.rs")
.content;
(kotlin_files, jni_source)
}
fn kt_files(files: &[GeneratedFile]) -> Vec<(&str, &str)> {
files
.iter()
.filter_map(|generated| {
let name = generated.path.file_name()?.to_str()?;
name.ends_with(".kt").then_some((name, generated.content.as_str()))
})
.collect()
}
fn leading_identifier(text: &str) -> Option<&str> {
let end = text
.find(|c: char| !c.is_alphanumeric() && c != '_')
.unwrap_or(text.len());
(end > 0).then(|| &text[..end])
}
fn declared_native_funs(files: &[(&str, &str)]) -> BTreeSet<String> {
let mut declared = BTreeSet::new();
for (_, content) in files {
for line in content.lines() {
let Some(rest) = line.trim_start().strip_prefix("external fun ") else {
continue;
};
if let Some(name) = leading_identifier(rest)
&& name.starts_with("native")
{
declared.insert(name.to_owned());
}
}
}
declared
}
fn called_native_funs<'a>(files: &[(&'a str, &'a str)]) -> Vec<(&'a str, String)> {
let mut calls = Vec::new();
for (name, content) in files {
for line in content.lines() {
for (index, _) in line.match_indices('.') {
let after = &line[index + 1..];
let Some(ident) = leading_identifier(after) else {
continue;
};
if ident.starts_with("native") && after[ident.len()..].starts_with('(') {
calls.push((*name, ident.to_owned()));
}
}
}
}
calls
}
fn implemented_native_symbols(jni_source: &str) -> BTreeSet<String> {
let mut implemented = BTreeSet::new();
for line in jni_source.lines() {
let Some(rest) = line.trim_start().strip_prefix("pub unsafe extern \"system\" fn Java_") else {
continue;
};
let Some(open_paren) = rest.find('(') else {
continue;
};
let full_name = &rest[..open_paren];
if let Some(idx) = full_name.rfind("_native") {
implemented.insert(full_name[idx + 1..].to_owned());
}
}
implemented
}
#[test]
fn every_native_method_kotlin_calls_is_declared_and_implemented() {
let api = base_surface();
let config = base_config();
let (kotlin_files, jni_source) = generate(&api, &config);
let files = kt_files(&kotlin_files);
let declared = declared_native_funs(&files);
let calls = called_native_funs(&files);
let implemented = implemented_native_symbols(&jni_source);
assert!(
declared.len() >= 2,
"parsed {} `external fun native*` declarations — the declaration scanner stopped \
recognising the emitted shape: {declared:?}",
declared.len()
);
assert!(
calls.len() >= 2,
"found {} `.native*(` call sites — the call scanner stopped recognising the emitted \
shape: {calls:?}",
calls.len()
);
for required in ["nativeClientGetOrFetch", "nativeFreeSingleflightResult"] {
assert!(
calls.iter().any(|(_, symbol)| symbol == required),
"the fixture must reach the handle-wrapper close(), which calls `{required}`; \
without it this cross-check never examines the branch it exists for: {calls:?}"
);
}
let undeclared: Vec<&(&str, String)> = calls.iter().filter(|(_, symbol)| !declared.contains(symbol)).collect();
assert!(
undeclared.is_empty(),
"generated Kotlin calls native methods the Bridge object never declares as `external \
fun` — Kotlin fails to compile with Unresolved reference:\n{undeclared:#?}\ndeclared: \
{declared:?}"
);
let unimplemented: BTreeSet<&String> = declared
.iter()
.filter(|symbol| !implemented.contains(*symbol))
.collect();
assert!(
unimplemented.is_empty(),
"the Bridge object declares native methods the jni backend never implements — the AAR \
links but throws UnsatisfiedLinkError the first time Kotlin calls one:\n{unimplemented:#?}\n\
implemented: {implemented:?}"
);
}
#[test]
fn jni_only_exclude_functions_does_not_starve_the_destructor_it_still_needs() {
let mut api = base_surface();
api.types
.iter_mut()
.find(|type_def| type_def.name == CLIENT_TYPE)
.expect("fixture must declare Client")
.methods
.clear();
api.functions.push(FunctionDef {
name: "get_backup_result".to_owned(),
rust_path: format!("{CRATE_NAME}::get_backup_result"),
return_type: TypeRef::Named(RESULT_TYPE.to_owned()),
..FunctionDef::default()
});
let mut config = base_config();
config.jni = Some(JniConfig {
exclude_functions: vec!["get_backup_result".to_owned()],
..JniConfig::default()
});
let (kotlin_files, jni_source) = generate(&api, &config);
let files = kt_files(&kotlin_files);
let declared = declared_native_funs(&files);
let calls = called_native_funs(&files);
let implemented = implemented_native_symbols(&jni_source);
assert!(
calls.iter().any(|(_, symbol)| symbol == "nativeGetBackupResult"),
"the fixture must reach the top-level function call; without it this test examines \
nothing: {calls:?}"
);
let destructor = "nativeFreeSingleflightResult";
assert!(
declared.contains(destructor),
"the Bridge object must still declare `{destructor}`: {declared:?}"
);
assert!(
calls.iter().any(|(_, symbol)| symbol == destructor),
"the handle wrapper's close() must still call `{destructor}`: {calls:?}"
);
assert!(
implemented.contains(destructor),
"`[crates.jni].exclude_functions` narrowing which function gets its OWN native shim must \
not also drop the destructor for what that function returns — `{destructor}` is missing \
from the jni crate's implemented symbols: {implemented:?}\n{jni_source}"
);
assert!(
!implemented.contains("nativeGetBackupResult"),
"`[crates.jni].exclude_functions` should still exclude the function's own native shim; \
a consumer relying on hand-writing it would now get a duplicate `#[no_mangle]` symbol: \
{implemented:?}"
);
}