use std::collections::BTreeSet;
use std::path::PathBuf;
use crate::core::backend::GeneratedFile;
use crate::core::config::workspace::ClientConstructorConfig;
use crate::core::config::{AdapterPattern, ResolvedCrateConfig};
use crate::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};
use crate::backends::kotlin::template_env;
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(&template_env::render(
"jni_bridge_object_header.jinja",
minijinja::context! {
bridge_name => bridge_name,
lib_name => lib_name,
},
));
let mut emitted_native_names: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut emitted_destructor_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);
body.push('\n');
push_jni_external_fun(
&mut body,
&native_name,
&jni_params,
non_unit_return_type(&f.return_type, return_ty),
Some(&exception_class),
);
}
let methods_emitted_before = body.matches("// JNI external funs for client instance methods").count();
emit_method_jni_external_funs(
&mut body,
api,
&exclude_functions,
&exception_class,
&mut emitted_destructor_names,
);
let methods_emitted_after = body.matches("// JNI external funs for client instance methods").count();
if methods_emitted_before == methods_emitted_after {
let opaque_with_methods: Vec<_> = api
.types
.iter()
.filter(|t| {
t.is_opaque
&& !t.is_trait
&& !t.methods.is_empty()
&& !exclude_functions
.iter()
.all(|&excluded| t.methods.iter().all(|m| excluded == m.name.as_str()))
})
.collect();
if !opaque_with_methods.is_empty() {
body.push_str("\n // JNI external funs for client instance methods (fallback).\n");
for ty in &opaque_with_methods {
let owner_pascal = to_pascal_case(&ty.name);
for method in &ty.methods {
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 if method.params.len() == 1 && is_binary_param_type(&method.params[0].ty) {
format!("handle: Long, {}: ByteArray", to_lower_camel(&method.params[0].name))
} else {
"handle: Long, requestJson: String".to_string()
};
push_jni_external_fun(
&mut body,
&native_name,
¶ms,
non_unit_return_type(&method.return_type, return_ty),
Some(&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 handle_only_opaque_returns: std::collections::BTreeSet<&str> = api
.types
.iter()
.filter(|t| t.is_opaque && !t.is_trait && !client_type_names.contains(t.name.as_str()))
.map(|t| t.name.as_str())
.collect();
if !handle_only_opaque_returns.is_empty() {
body.push_str("\n // Destructor external funs for handle-only opaque types.\n");
for type_name in &handle_only_opaque_returns {
let free_name = format!("nativeFree{}", to_pascal_case(type_name));
if !emitted_destructor_names.contains(&free_name) {
push_jni_external_fun(&mut body, &free_name, "handle: Long", None, None);
}
}
}
body.push_str("}\n");
let content = template_env::render(
"jni_bridge_file.jinja",
minijinja::context! {
package => package,
body => body,
},
);
let path = jni_output_path(config, &format!("{bridge_name}.kt"));
GeneratedFile {
path,
content,
generated_header: false,
}
}
fn non_unit_return_type(return_type: &TypeRef, rendered_return_type: &str) -> Option<String> {
if matches!(return_type, TypeRef::Unit) {
None
} else {
Some(rendered_return_type.to_string())
}
}
fn push_jni_external_fun(
out: &mut String,
native_name: &str,
params: &str,
return_type: Option<String>,
throws_class: Option<&str>,
) {
out.push_str(&template_env::render(
"jni_external_fun.jinja",
minijinja::context! {
native_name => native_name,
params => params,
return_type => return_type,
throws_class => throws_class,
},
));
out.push('\n');
}
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(&template_env::render(
"jni_streaming_extern_comment.jinja",
minijinja::context! {
owner => owner,
adapter_name => to_lower_camel(&adapter.name),
},
));
push_jni_external_fun(
out,
&jni_start,
"clientHandle: Long, requestJson: String",
Some("Long".to_string()),
Some(exception_class),
);
push_jni_external_fun(
out,
&jni_next,
"streamHandle: Long",
Some("String?".to_string()),
Some(exception_class),
);
push_jni_external_fun(out, &jni_free, "streamHandle: Long", None, None);
}
}
fn emit_method_jni_external_funs(
out: &mut String,
api: &ApiSurface,
exclude_functions: &std::collections::HashSet<&str>,
exception_class: &str,
emitted_destructor_names: &mut std::collections::HashSet<String>,
) {
let client_types: Vec<_> = api
.types
.iter()
.filter(|t| t.is_opaque && !t.is_trait && t.methods.iter().any(|m| !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.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 if method.params.len() == 1 && is_binary_param_type(&method.params[0].ty) {
format!("handle: Long, {}: ByteArray", to_lower_camel(&method.params[0].name))
} else {
"handle: Long, requestJson: String".to_string()
};
push_jni_external_fun(
out,
&native_name,
¶ms,
non_unit_return_type(&method.return_type, return_ty),
Some(exception_class),
);
}
let free_name = format!("nativeFree{owner_pascal}");
push_jni_external_fun(out, &free_name, "handle: Long", None, None);
emitted_destructor_names.insert(free_name);
}
}
pub fn emit_jni_client_class(
api: &ApiSurface,
config: &ResolvedCrateConfig,
package: Option<&str>,
) -> Option<GeneratedFile> {
let is_client_type = |t: &&crate::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(&template_env::render(
"jni_client_class_header.jinja",
minijinja::context! {
class_name => class_name,
},
));
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, api, &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(&template_env::render(
"jni_client_close_method.jinja",
minijinja::context! {
bridge_name => bridge_name,
free_name => free_name,
},
));
body.push_str("}\n");
}
let imports = imports.iter().cloned().collect::<Vec<_>>();
let content = template_env::render(
"jni_client_file.jinja",
minijinja::context! {
package => pkg,
imports => imports,
body => body,
},
);
let path = jni_output_path(config, "DefaultClient.kt");
Some(GeneratedFile {
path,
content,
generated_header: false,
})
}
fn is_enum_param(ty: &str, enum_names: &std::collections::HashSet<&str>) -> bool {
enum_names.contains(ty)
}
fn extract_named_type(ty: &str) -> Option<&str> {
if !ty.is_empty() && ty.chars().next().unwrap().is_uppercase() {
Some(ty)
} else {
None
}
}
fn emit_jni_client_method(
m: &crate::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(&template_env::render(
"line_comment.jinja",
minijinja::context! {
indent => " ",
line => line,
},
));
}
}
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_binary_return_type(&m.return_type) {
"ByteArray".to_string()
} else if is_optional_binary_return_type(&m.return_type) {
"ByteArray?".to_string()
} else {
kotlin_type_with_string_imports(&m.return_type, false, imports)
};
out.push_str(&template_env::render(
"jni_client_method_header.jinja",
minijinja::context! {
async_kw => async_kw,
method_name => method_name,
params => params_with_types.join(", "),
return_type => wrapper_return_ty,
},
));
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: &crate::core::ir::MethodDef, bridge_name: &str, native_name: &str) -> String {
if m.params.is_empty() {
return format!("{bridge_name}.{native_name}(handle)");
}
if m.params.len() == 1 && is_binary_param_type(&m.params[0].ty) {
let p = &m.params[0];
let param_name = to_lower_camel(&p.name);
let arg = if p.optional {
format!("{param_name} ?: ByteArray(0)")
} else {
param_name
};
return format!("{bridge_name}.{native_name}(handle, {arg})");
}
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: &crate::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 => {
out.push_str(&template_env::render(
"jni_unit_body.jinja",
minijinja::context! {
is_async => m.is_async,
bridge_call => bridge_call,
},
));
}
_ 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)")
};
out.push_str(&template_env::render(
"jni_deserialize_body.jinja",
minijinja::context! {
is_async => m.is_async,
bridge_call => bridge_call,
deserialize_call => deserialize_call,
},
));
}
_ => {
out.push_str(&template_env::render(
"jni_passthrough_body.jinja",
minijinja::context! {
is_async => m.is_async,
bridge_call => bridge_call,
},
));
}
}
}
fn is_vec_u8(ty: &TypeRef) -> bool {
matches!(
ty,
TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Primitive(crate::core::ir::PrimitiveType::U8))
)
}
fn is_binary_return_type(ty: &TypeRef) -> bool {
matches!(ty, TypeRef::Bytes) || is_vec_u8(ty)
}
fn is_optional_binary_return_type(ty: &TypeRef) -> bool {
matches!(ty, TypeRef::Optional(inner) if is_binary_return_type(inner))
}
fn is_binary_param_type(ty: &TypeRef) -> bool {
match ty {
TypeRef::Bytes => true,
TypeRef::Vec(inner) => matches!(inner.as_ref(), TypeRef::Primitive(crate::core::ir::PrimitiveType::U8)),
TypeRef::Optional(inner) => is_binary_param_type(inner),
_ => false,
}
}
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(crate::core::ir::PrimitiveType::U8))
}
_ => false,
}
}
fn emit_jni_streaming_client_method(
adapter: &crate::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(&template_env::render(
"jni_streaming_client_method.jinja",
minijinja::context! {
method_name => method_name,
params => params.join(", "),
item_type => item_type,
bridge_name => bridge_name,
jni_start => jni_start,
jni_next => jni_next,
jni_free => jni_free,
first_param_name => first_param_name,
},
));
}
fn jni_return_type(ty: &TypeRef) -> &'static str {
match ty {
TypeRef::Unit => "Unit",
TypeRef::Primitive(p) => {
use crate::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::Bytes => "ByteArray",
TypeRef::Optional(inner) if is_binary_return_type(inner) => "ByteArray?",
TypeRef::Optional(_) => "String?",
TypeRef::Named(_) => "String",
TypeRef::Vec(inner) => {
if matches!(inner.as_ref(), TypeRef::Primitive(crate::core::ir::PrimitiveType::U8)) {
"ByteArray"
} else {
"String"
}
}
TypeRef::Map(_, _) => "String",
_ => "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: &crate::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 {
if is_binary_param_type(ty) {
return "ByteArray";
}
match ty {
TypeRef::Primitive(p) => {
use crate::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 enum_names: std::collections::HashSet<&str> = api.enums.iter().map(|e| e.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".to_string()
} else if is_enum_param(&p.ty, &enum_names) {
"Int".to_string()
} else {
"Long".to_string()
};
let param_name = to_lower_camel(&p.name);
format!("{param_name}: {kt_ty}")
})
.collect();
let params_str = params.join(", ");
push_jni_external_fun(
out,
&native_name,
¶ms_str,
Some("Long".to_string()),
Some(exception_class),
);
}
}
fn emit_jni_client_factory(
class_name: &str,
bridge_name: &str,
ctor: &ClientConstructorConfig,
api: &ApiSurface,
out: &mut String,
) {
let native_name = format!("nativeNew{}", to_pascal_case(class_name));
let enum_names: std::collections::HashSet<&str> = api.enums.iter().map(|e| e.name.as_str()).collect();
let params: Vec<String> = ctor
.params
.iter()
.map(|p| {
let kt_ty = if p.ty.contains("c_char") {
"String".to_string()
} else if is_enum_param(&p.ty, &enum_names) {
let enum_name = extract_named_type(&p.ty).unwrap_or("Any");
enum_name.to_string()
} else {
"Long".to_string()
};
let param_name = to_lower_camel(&p.name);
format!("{param_name}: {kt_ty}")
})
.collect();
let call_args: Vec<String> = ctor
.params
.iter()
.map(|p| {
let param_name = to_lower_camel(&p.name);
if is_enum_param(&p.ty, &enum_names) {
format!("{param_name}.ordinal")
} else {
param_name
}
})
.collect();
let params_str = params.join(", ");
let call_args_str = call_args.join(", ");
out.push_str(&template_env::render(
"jni_client_constructor.jinja",
minijinja::context! {
params => params_str,
class_name => class_name,
bridge_name => bridge_name,
native_name => native_name,
call_args => call_args_str,
},
));
}
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('\n');
push_jni_external_fun(
out,
&native_name,
&format!("impl: {iface_fqn}"),
None,
Some(exception_class),
);
}
}
if bridge.unregister_fn.is_some() {
let native_name = format!("nativeUnregister{trait_pascal}");
if !emitted_native_names.contains(&native_name) {
push_jni_external_fun(out, &native_name, "name: String", None, Some(exception_class));
}
}
if bridge.clear_fn.is_some() {
let native_name = format!("nativeClear{trait_pascal}s");
if !emitted_native_names.contains(&native_name) {
push_jni_external_fun(out, &native_name, "", None, Some(exception_class));
}
}
}
}
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)
}
}