use std::collections::BTreeSet;
use std::path::PathBuf;
use alef_core::backend::GeneratedFile;
use alef_core::config::workspace::ClientConstructorConfig;
use alef_core::config::{AdapterPattern, ResolvedCrateConfig};
use alef_core::ir::{ApiSurface, TypeRef};
use super::object_wrapper::{format_param_with_imports, kotlin_type_with_string_imports};
use super::shared::{to_lower_camel, to_pascal_case};
pub fn emit_jni_bridge_object(api: &ApiSurface, config: &ResolvedCrateConfig) -> GeneratedFile {
let module_name = to_pascal_case(&config.name);
let bridge_name = format!("{module_name}Bridge");
let exception_class = format!("{bridge_name}Exception");
let lib_name = config.jni_lib_name();
let package = jni_kotlin_package(config);
let exclude_functions: std::collections::HashSet<&str> = config
.kotlin_android
.as_ref()
.map(|c| c.exclude_functions.iter().map(String::as_str).collect())
.unwrap_or_else(|| {
config
.kotlin
.as_ref()
.map(|k| k.exclude_functions.iter().map(String::as_str).collect())
.unwrap_or_default()
});
let visible_functions: Vec<_> = api
.functions
.iter()
.filter(|f| !exclude_functions.contains(f.name.as_str()))
.collect();
let opaque_type_names: std::collections::HashSet<&str> = api
.types
.iter()
.filter(|t| t.is_opaque && !t.is_trait)
.map(|t| t.name.as_str())
.collect();
let mut body = String::new();
body.push_str("@Suppress(\"TooManyFunctions\")\n");
body.push_str(&format!("object {bridge_name} {{\n"));
body.push_str(&format!(" init {{ System.loadLibrary(\"{lib_name}\") }}\n"));
let mut emitted_native_names: std::collections::HashSet<String> = std::collections::HashSet::new();
for f in &visible_functions {
let native_name = format!("native{}", to_pascal_case(&f.name));
emitted_native_names.insert(native_name.clone());
let return_ty = jni_return_type_for_function(&f.return_type, &opaque_type_names);
let jni_params = jni_params_for_function(f, &opaque_type_names);
if matches!(f.return_type, TypeRef::Unit) {
body.push_str(&format!(
"\n @Throws({exception_class}::class)\n external fun {native_name}({jni_params})\n"
));
} else {
body.push_str(&format!(
"\n @Throws({exception_class}::class)\n external fun {native_name}({jni_params}): {return_ty}\n"
));
}
}
emit_method_jni_external_funs(&mut body, api, &exclude_functions, &exception_class);
emit_streaming_jni_external_funs(&mut body, config, &exception_class);
emit_constructor_jni_external_funs(&mut body, api, config, &exception_class);
emit_trait_bridge_jni_external_funs(&mut body, config, &exception_class, &package, &emitted_native_names);
let client_type_names: std::collections::HashSet<&str> = api
.types
.iter()
.filter(|t| t.is_opaque && !t.is_trait && t.methods.iter().any(|m| !m.sanitized && !m.is_static))
.map(|t| t.name.as_str())
.collect();
let top_level_opaque_returns: std::collections::BTreeSet<&str> = visible_functions
.iter()
.filter_map(|f| {
if let TypeRef::Named(n) = &f.return_type {
if opaque_type_names.contains(n.as_str()) && !client_type_names.contains(n.as_str()) {
return Some(n.as_str());
}
}
None
})
.collect();
if !top_level_opaque_returns.is_empty() {
body.push_str("\n // Destructor external funs for opaque handle types.\n");
for type_name in &top_level_opaque_returns {
let free_name = format!("nativeFree{}", to_pascal_case(type_name));
body.push_str(&format!(" external fun {free_name}(handle: Long)\n"));
}
}
body.push_str("}\n");
let mut content = String::new();
content.push_str("// Generated by alef. Do not edit by hand.\n");
content.push_str(
"@file:Suppress(\n \
\"ktlint:standard:max-line-length\",\n \
\"ktlint:standard:trailing-comma-on-declaration-site\",\n \
\"ktlint:standard:trailing-comma-on-call-site\",\n \
\"ktlint:standard:annotation\",\n \
\"MaxLineLength\",\n \
\"TooManyFunctions\",\n \
\"LongParameterList\",\n\
)\n\n",
);
content.push_str(&format!("package {package}\n\n"));
content.push_str(&body);
let path = jni_output_path(config, &format!("{bridge_name}.kt"));
GeneratedFile {
path,
content,
generated_header: false,
}
}
pub fn emit_streaming_jni_external_funs(out: &mut String, config: &ResolvedCrateConfig, exception_class: &str) {
let streaming: Vec<_> = config
.adapters
.iter()
.filter(|a| matches!(a.pattern, AdapterPattern::Streaming) && a.owner_type.is_some())
.collect();
if streaming.is_empty() {
return;
}
out.push_str("\n // JNI streaming external funs — implementations are Rust JNI shims.\n");
for adapter in &streaming {
let Some(owner) = adapter.owner_type.as_deref() else {
continue;
};
let owner_pascal = to_pascal_case(owner);
let adapter_pascal = to_pascal_case(&adapter.name);
let jni_start = format!("native{owner_pascal}{adapter_pascal}Start");
let jni_next = format!("native{owner_pascal}{adapter_pascal}Next");
let jni_free = format!("native{owner_pascal}{adapter_pascal}Free");
out.push('\n');
out.push_str(&format!(
" // Streaming JNI externs for {owner}.{}\n",
to_lower_camel(&adapter.name)
));
out.push_str(&format!(
" @Throws({exception_class}::class)\n external fun {jni_start}(clientHandle: Long, requestJson: String): Long\n"
));
out.push_str(&format!(
" @Throws({exception_class}::class)\n external fun {jni_next}(streamHandle: Long): String?\n"
));
out.push_str(&format!(" external fun {jni_free}(streamHandle: Long)\n"));
}
}
fn emit_method_jni_external_funs(
out: &mut String,
api: &ApiSurface,
exclude_functions: &std::collections::HashSet<&str>,
exception_class: &str,
) {
let client_types: Vec<_> = api
.types
.iter()
.filter(|t| t.is_opaque && !t.is_trait && t.methods.iter().any(|m| !m.sanitized && !m.is_static))
.collect();
if client_types.is_empty() {
return;
}
out.push_str("\n // JNI external funs for client instance methods.\n");
for ty in &client_types {
let owner_pascal = to_pascal_case(&ty.name);
for method in ty.methods.iter().filter(|m| !m.sanitized && !m.is_static) {
if exclude_functions.contains(method.name.as_str()) {
continue;
}
let native_name = format!("native{owner_pascal}{}", to_pascal_case(&method.name));
let return_ty = jni_return_type(&method.return_type);
let params = if method.params.is_empty() {
"handle: Long".to_string()
} else {
"handle: Long, requestJson: String".to_string()
};
if matches!(method.return_type, TypeRef::Unit) {
out.push_str(&format!(
" @Throws({exception_class}::class)\n external fun {native_name}({params})\n"
));
} else {
out.push_str(&format!(
" @Throws({exception_class}::class)\n external fun {native_name}({params}): {return_ty}\n"
));
}
}
let free_name = format!("nativeFree{owner_pascal}");
out.push_str(&format!(" external fun {free_name}(handle: Long)\n"));
}
}
pub fn emit_jni_client_class(
api: &ApiSurface,
config: &ResolvedCrateConfig,
package: Option<&str>,
) -> Option<GeneratedFile> {
let is_client_type = |t: &&alef_core::ir::TypeDef| {
t.is_opaque && !t.is_trait && t.methods.iter().any(|m| !m.sanitized && !m.is_static)
};
let client_types: Vec<_> = api.types.iter().filter(is_client_type).collect();
if client_types.is_empty() {
return None;
}
let exclude_functions: std::collections::HashSet<&str> = config
.kotlin_android
.as_ref()
.map(|c| c.exclude_functions.iter().map(String::as_str).collect())
.or_else(|| {
config
.kotlin
.as_ref()
.map(|k| k.exclude_functions.iter().map(String::as_str).collect())
})
.unwrap_or_default();
let module_name = to_pascal_case(&config.name);
let bridge_name = format!("{module_name}Bridge");
let pkg = package
.map(str::to_string)
.unwrap_or_else(|| jni_kotlin_package(config));
let mut imports: BTreeSet<String> = BTreeSet::new();
let mut body = String::new();
let has_async = client_types
.iter()
.any(|t| t.methods.iter().any(|m| !m.sanitized && m.is_async));
if has_async {
imports.insert("import kotlinx.coroutines.Dispatchers".to_string());
imports.insert("import kotlinx.coroutines.withContext".to_string());
}
let streaming_adapters: Vec<_> = config
.adapters
.iter()
.filter(|a| matches!(a.pattern, AdapterPattern::Streaming))
.filter(|a| !a.skip_languages.iter().any(|l| l == "kotlin"))
.filter(|a| {
a.owner_type
.as_deref()
.map(|owner| client_types.iter().any(|t| t.name == owner))
.unwrap_or(false)
})
.collect();
if !streaming_adapters.is_empty() {
imports.insert("import kotlinx.coroutines.Dispatchers".to_string());
imports.insert("import kotlinx.coroutines.withContext".to_string());
imports.insert("import kotlinx.coroutines.flow.Flow".to_string());
imports.insert("import kotlinx.coroutines.flow.callbackFlow".to_string());
imports.insert("import kotlinx.coroutines.channels.awaitClose".to_string());
}
for ty in &client_types {
let class_name = &ty.name;
for m in ty.methods.iter().filter(|m| !m.sanitized && !m.is_static) {
kotlin_type_with_string_imports(&m.return_type, false, &mut imports);
for p in &m.params {
format_param_with_imports(p, &mut imports);
}
}
for adapter in streaming_adapters
.iter()
.filter(|a| a.owner_type.as_deref() == Some(class_name.as_str()))
{
if let Some(item) = adapter.item_type.as_deref() {
let _ = item;
}
}
body.push_str("@Suppress(\"TooManyFunctions\")\n");
body.push_str(&format!(
"class {class_name} internal constructor(internal val handle: Long) : AutoCloseable {{\n"
));
let has_json_methods = ty
.methods
.iter()
.filter(|m| !m.sanitized && !m.is_static)
.any(|m| !m.params.is_empty() || needs_json_deserialize(&m.return_type));
let ctor_config = config.client_constructors.get(class_name.as_str());
let needs_companion = has_json_methods || ctor_config.is_some();
if needs_companion {
body.push_str(" companion object {\n");
if has_json_methods {
body.push_str(" private val MAPPER = com.fasterxml.jackson.databind.ObjectMapper()\n");
body.push_str(" .registerModule(com.fasterxml.jackson.datatype.jdk8.Jdk8Module())\n");
body.push_str(" .findAndRegisterModules()\n");
body.push_str(
" .setPropertyNamingStrategy(com.fasterxml.jackson.databind.PropertyNamingStrategies.SNAKE_CASE)\n",
);
}
if let Some(ctor) = ctor_config {
emit_jni_client_factory(class_name, &bridge_name, ctor, &mut body);
}
body.push_str(" }\n\n");
}
for method in ty
.methods
.iter()
.filter(|m| !m.sanitized && !m.is_static && !exclude_functions.contains(m.name.as_str()))
{
emit_jni_client_method(method, class_name, &bridge_name, &mut body, &mut imports);
}
for adapter in streaming_adapters
.iter()
.filter(|a| a.owner_type.as_deref() == Some(class_name.as_str()))
{
emit_jni_streaming_client_method(adapter, class_name, &bridge_name, &mut body);
}
let free_name = format!("nativeFree{class_name}");
body.push_str(&format!(
" override fun close() {{ {bridge_name}.{free_name}(handle) }}\n"
));
body.push_str("}\n");
}
let mut content = String::new();
content.push_str("// Generated by alef. Do not edit by hand.\n");
content.push_str(
"@file:Suppress(\n \
\"ktlint:standard:max-line-length\",\n \
\"ktlint:standard:trailing-comma-on-declaration-site\",\n \
\"ktlint:standard:trailing-comma-on-call-site\",\n \
\"ktlint:standard:annotation\",\n \
\"MaxLineLength\",\n \
\"TooManyFunctions\",\n \
\"LongParameterList\",\n \
\"LongMethod\",\n\
)\n\n",
);
content.push_str(&format!("package {pkg}\n\n"));
for import in &imports {
content.push_str(import);
content.push('\n');
}
if !imports.is_empty() {
content.push('\n');
}
content.push_str(&body);
let path = jni_output_path(config, "DefaultClient.kt");
Some(GeneratedFile {
path,
content,
generated_header: false,
})
}
fn emit_jni_client_method(
m: &alef_core::ir::MethodDef,
class_name: &str,
bridge_name: &str,
out: &mut String,
imports: &mut BTreeSet<String>,
) {
if !m.doc.is_empty() {
for line in m.doc.lines() {
out.push_str(&format!(" // {line}\n"));
}
}
let method_name = to_lower_camel(&m.name);
let native_name = format!("native{}{}", to_pascal_case(class_name), to_pascal_case(&m.name));
let async_kw = if m.is_async { "suspend " } else { "" };
let params_with_types: Vec<String> = m.params.iter().map(|p| format_param_with_imports(p, imports)).collect();
let wrapper_return_ty = if is_vec_u8(&m.return_type) {
"ByteArray".to_string()
} else {
kotlin_type_with_string_imports(&m.return_type, false, imports)
};
out.push_str(&format!(
" {async_kw}fun {method_name}({}): {wrapper_return_ty} {{\n",
params_with_types.join(", ")
));
let bridge_call = build_bridge_call(m, bridge_name, &native_name);
emit_method_body(m, out, &bridge_call, imports);
out.push_str(" }\n\n");
}
fn build_bridge_call(m: &alef_core::ir::MethodDef, bridge_name: &str, native_name: &str) -> String {
if m.params.is_empty() {
return format!("{bridge_name}.{native_name}(handle)");
}
let request_json_expr = if m.params.len() == 1 {
let p = &m.params[0];
let param_name = to_lower_camel(&p.name);
if p.optional {
format!("{param_name}?.let {{ MAPPER.writeValueAsString(it) }} ?: \"\"")
} else {
format!("MAPPER.writeValueAsString({param_name})")
}
} else {
let map_entries: Vec<String> = m
.params
.iter()
.map(|p| {
let name = to_lower_camel(&p.name);
format!("\"{name}\" to {name}")
})
.collect();
format!("MAPPER.writeValueAsString(mapOf({}))", map_entries.join(", "))
};
format!("{bridge_name}.{native_name}(handle, {request_json_expr})")
}
fn emit_method_body(m: &alef_core::ir::MethodDef, out: &mut String, bridge_call: &str, imports: &mut BTreeSet<String>) {
let needs_deserialize = needs_json_deserialize(&m.return_type);
let return_kotlin_type = if needs_deserialize {
Some(kotlin_type_with_string_imports(&m.return_type, false, imports))
} else {
None
};
match &m.return_type {
TypeRef::Unit => {
if m.is_async {
out.push_str(&format!(" withContext(Dispatchers.IO) {{ {bridge_call} }}\n"));
} else {
out.push_str(&format!(" {bridge_call}\n"));
}
}
_ if needs_deserialize => {
let kotlin_ty = return_kotlin_type.unwrap();
let base_ty = kotlin_ty.trim_end_matches('?');
let use_type_reference = base_ty.contains('<');
let deserialize_call = if use_type_reference {
imports.insert("import com.fasterxml.jackson.core.type.TypeReference".to_string());
format!("MAPPER.readValue(responseJson, object : TypeReference<{base_ty}>() {{}})")
} else {
format!("MAPPER.readValue(responseJson, {base_ty}::class.java)")
};
if m.is_async {
out.push_str(" return withContext(Dispatchers.IO) {\n");
out.push_str(&format!(" val responseJson = {bridge_call}\n"));
out.push_str(&format!(" {deserialize_call}\n"));
out.push_str(" }\n");
} else {
out.push_str(&format!(" val responseJson = {bridge_call}\n"));
out.push_str(&format!(" return {deserialize_call}\n"));
}
}
_ => {
if m.is_async {
out.push_str(&format!(
" return withContext(Dispatchers.IO) {{ {bridge_call} }}\n"
));
} else {
out.push_str(&format!(" return {bridge_call}\n"));
}
}
}
}
fn is_vec_u8(ty: &TypeRef) -> bool {
matches!(
ty,
TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Primitive(alef_core::ir::PrimitiveType::U8))
)
}
fn needs_json_deserialize(ty: &TypeRef) -> bool {
match ty {
TypeRef::Named(_) => true,
TypeRef::Optional(inner) => matches!(inner.as_ref(), TypeRef::Named(_)),
TypeRef::Map(_, _) => true,
TypeRef::Vec(inner) => {
!matches!(inner.as_ref(), TypeRef::Primitive(alef_core::ir::PrimitiveType::U8))
}
_ => false,
}
}
fn emit_jni_streaming_client_method(
adapter: &alef_core::config::AdapterConfig,
class_name: &str,
bridge_name: &str,
out: &mut String,
) {
let method_name = to_lower_camel(&adapter.name);
let item_type = adapter.item_type.as_deref().unwrap_or("Any");
let owner_pascal = to_pascal_case(class_name);
let adapter_pascal = to_pascal_case(&adapter.name);
let jni_start = format!("native{owner_pascal}{adapter_pascal}Start");
let jni_next = format!("native{owner_pascal}{adapter_pascal}Next");
let jni_free = format!("native{owner_pascal}{adapter_pascal}Free");
let params: Vec<String> = adapter
.params
.iter()
.map(|p| {
let simple_ty = p.ty.rsplit("::").next().unwrap_or(&p.ty);
let param_name = to_lower_camel(&p.name);
format!("{param_name}: {simple_ty}")
})
.collect();
let first_param_name = adapter
.params
.first()
.map(|p| to_lower_camel(&p.name))
.unwrap_or_else(|| "request".to_string());
out.push_str(" @Suppress(\"TooGenericExceptionCaught\")\n");
out.push_str(&format!(
" fun {method_name}({}): kotlinx.coroutines.flow.Flow<{item_type}> = kotlinx.coroutines.flow.callbackFlow {{\n",
params.join(", ")
));
out.push_str(" val mapper = com.fasterxml.jackson.databind.ObjectMapper()\n");
out.push_str(" .registerModule(com.fasterxml.jackson.datatype.jdk8.Jdk8Module())\n");
out.push_str(" .findAndRegisterModules()\n");
out.push_str(
" .setPropertyNamingStrategy(com.fasterxml.jackson.databind.PropertyNamingStrategies.SNAKE_CASE)\n",
);
out.push_str(" val streamHandle: Long = withContext(Dispatchers.IO) {\n");
out.push_str(&format!(
" {bridge_name}.{jni_start}(handle, mapper.writeValueAsString({first_param_name}))\n"
));
out.push_str(" }\n");
out.push_str(" try {\n");
out.push_str(" while (true) {\n");
out.push_str(" val chunkJson: String? = withContext(Dispatchers.IO) {\n");
out.push_str(&format!(" {bridge_name}.{jni_next}(streamHandle)\n"));
out.push_str(" }\n");
out.push_str(" if (chunkJson == null) break\n");
out.push_str(&format!(
" val chunk = mapper.readValue(chunkJson, {item_type}::class.java)\n"
));
out.push_str(" send(chunk)\n");
out.push_str(" }\n");
out.push_str(" close()\n");
out.push_str(" } catch (e: Throwable) {\n");
out.push_str(" close(e)\n");
out.push_str(" }\n");
out.push_str(" awaitClose {\n");
out.push_str(&format!(" {bridge_name}.{jni_free}(streamHandle)\n"));
out.push_str(" }\n");
out.push_str(" }\n\n");
}
fn jni_return_type(ty: &TypeRef) -> &'static str {
match ty {
TypeRef::Unit => "Unit",
TypeRef::Primitive(p) => {
use alef_core::ir::PrimitiveType;
match p {
PrimitiveType::Bool => "Boolean",
PrimitiveType::I8 => "Byte",
PrimitiveType::I16 => "Short",
PrimitiveType::I32 => "Int",
PrimitiveType::I64 => "Long",
PrimitiveType::U8 => "Byte",
PrimitiveType::U16 => "Short",
PrimitiveType::U32 => "Int",
PrimitiveType::U64 => "Long",
PrimitiveType::F32 => "Float",
PrimitiveType::F64 => "Double",
PrimitiveType::Usize | PrimitiveType::Isize => "Long",
}
}
TypeRef::String => "String",
TypeRef::Optional(_) => "String?",
TypeRef::Named(_) => "String",
TypeRef::Vec(inner) => {
if matches!(inner.as_ref(), TypeRef::Primitive(alef_core::ir::PrimitiveType::U8)) {
"ByteArray"
} else {
"String"
}
}
TypeRef::Map(_, _) => "String",
TypeRef::Bytes => "ByteArray",
_ => "Long",
}
}
fn jni_return_type_for_function(ty: &TypeRef, opaque_type_names: &std::collections::HashSet<&str>) -> &'static str {
if let TypeRef::Named(n) = ty {
if opaque_type_names.contains(n.as_str()) {
return "Long";
}
}
jni_return_type(ty)
}
fn jni_params_for_function(
f: &alef_core::ir::FunctionDef,
opaque_type_names: &std::collections::HashSet<&str>,
) -> String {
f.params
.iter()
.map(|p| {
let jni_ty = jni_param_type_for_function(&p.ty, opaque_type_names);
let name = to_lower_camel(&p.name);
format!("{name}: {jni_ty}")
})
.collect::<Vec<_>>()
.join(", ")
}
fn jni_param_type_for_function(ty: &TypeRef, opaque_type_names: &std::collections::HashSet<&str>) -> &'static str {
let base = match ty {
TypeRef::Optional(inner) => inner.as_ref(),
other => other,
};
if let TypeRef::Named(n) = base {
if opaque_type_names.contains(n.as_str()) {
return "Long";
}
}
jni_param_type(ty)
}
fn jni_param_type(ty: &TypeRef) -> &'static str {
match ty {
TypeRef::Primitive(p) => {
use alef_core::ir::PrimitiveType;
match p {
PrimitiveType::Bool => "Boolean",
PrimitiveType::I8 => "Byte",
PrimitiveType::I16 => "Short",
PrimitiveType::I32 => "Int",
PrimitiveType::I64 => "Long",
PrimitiveType::U8 => "Byte",
PrimitiveType::U16 => "Short",
PrimitiveType::U32 => "Int",
PrimitiveType::U64 => "Long",
PrimitiveType::F32 => "Float",
PrimitiveType::F64 => "Double",
PrimitiveType::Usize | PrimitiveType::Isize => "Long",
}
}
TypeRef::String => "String",
_ => "String",
}
}
fn emit_constructor_jni_external_funs(
out: &mut String,
api: &ApiSurface,
config: &ResolvedCrateConfig,
exception_class: &str,
) {
let opaque_names: std::collections::HashSet<&str> = api
.types
.iter()
.filter(|t| t.is_opaque && !t.is_trait)
.map(|t| t.name.as_str())
.collect();
let mut sorted: Vec<(&str, &ClientConstructorConfig)> = config
.client_constructors
.iter()
.filter(|(name, _)| opaque_names.contains(name.as_str()))
.map(|(name, ctor)| (name.as_str(), ctor))
.collect();
sorted.sort_by_key(|(name, _)| *name);
if sorted.is_empty() {
return;
}
out.push_str("\n // JNI constructor external funs — implementations are Rust JNI shims.\n");
for (type_name, ctor) in sorted {
let native_name = format!("nativeNew{}", to_pascal_case(type_name));
let params: Vec<String> = ctor
.params
.iter()
.map(|p| {
let kt_ty = if p.ty.contains("c_char") { "String" } else { "Long" };
let param_name = to_lower_camel(&p.name);
format!("{param_name}: {kt_ty}")
})
.collect();
let params_str = params.join(", ");
out.push_str(&format!(
" @Throws({exception_class}::class)\n external fun {native_name}({params_str}): Long\n"
));
}
}
fn emit_jni_client_factory(class_name: &str, bridge_name: &str, ctor: &ClientConstructorConfig, out: &mut String) {
let native_name = format!("nativeNew{}", to_pascal_case(class_name));
let params: Vec<String> = ctor
.params
.iter()
.map(|p| {
let kt_ty = if p.ty.contains("c_char") { "String" } else { "Long" };
let param_name = to_lower_camel(&p.name);
format!("{param_name}: {kt_ty}")
})
.collect();
let param_names: Vec<String> = ctor.params.iter().map(|p| to_lower_camel(&p.name)).collect();
let params_str = params.join(", ");
let call_args = param_names.join(", ");
out.push_str(&format!(
" fun create({params_str}): {class_name} = {class_name}({bridge_name}.{native_name}({call_args}))\n"
));
}
fn emit_trait_bridge_jni_external_funs(
out: &mut String,
config: &ResolvedCrateConfig,
exception_class: &str,
kotlin_package: &str,
emitted_native_names: &std::collections::HashSet<String>,
) {
let bridges: Vec<_> = config
.trait_bridges
.iter()
.filter(|b| !b.exclude_languages.iter().any(|l| l == "kotlin_android"))
.collect();
if bridges.is_empty() {
return;
}
out.push_str("\n // JNI trait-bridge external funs — implementations are Rust JNI shims.\n");
for bridge in &bridges {
let trait_pascal = to_pascal_case(&bridge.trait_name);
let iface_fqn = format!("{kotlin_package}.I{trait_pascal}");
if bridge.register_fn.is_some() {
let native_name = format!("nativeRegister{trait_pascal}");
if !emitted_native_names.contains(&native_name) {
out.push_str(&format!(
"\n @Throws({exception_class}::class)\n external fun {native_name}(impl: {iface_fqn})\n"
));
}
}
if bridge.unregister_fn.is_some() {
let native_name = format!("nativeUnregister{trait_pascal}");
if !emitted_native_names.contains(&native_name) {
out.push_str(&format!(
" @Throws({exception_class}::class)\n external fun {native_name}(name: String)\n"
));
}
}
if bridge.clear_fn.is_some() {
let native_name = format!("nativeClear{trait_pascal}s");
if !emitted_native_names.contains(&native_name) {
out.push_str(&format!(
" @Throws({exception_class}::class)\n external fun {native_name}()\n"
));
}
}
}
}
fn jni_kotlin_package(config: &ResolvedCrateConfig) -> String {
config
.kotlin_android
.as_ref()
.and_then(|a| a.package.clone())
.or_else(|| config.kotlin.as_ref().and_then(|k| k.package.clone()))
.unwrap_or_else(|| config.kotlin_package())
}
fn jni_output_path(config: &ResolvedCrateConfig, filename: &str) -> PathBuf {
if let Some(android_out) = config.output_for("kotlin_android") {
return android_out.join(filename);
}
let kotlin_root = config
.output_for("kotlin")
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_else(|| "packages/kotlin".to_string());
let package = jni_kotlin_package(config);
let package_path = package.replace('.', "/");
if config.explicit_output.kotlin.is_some() {
PathBuf::from(&kotlin_root).join(filename)
} else {
PathBuf::from(&kotlin_root)
.join("src/main/kotlin")
.join(&package_path)
.join(filename)
}
}