use std::path::PathBuf;
use crate::core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile};
use crate::core::config::workspace::ClientConstructorConfig;
use crate::core::config::{AdapterPattern, Language, ResolvedCrateConfig};
use crate::core::ir::{ApiSurface, ParamDef, PrimitiveType, TypeDef, TypeRef};
use crate::core::jni::{
bridge_class_name, bridge_method_name, destructor_method_name, jni_symbol, streaming_method_names,
};
#[derive(Debug, Default, Clone, Copy)]
pub struct JniBackend;
impl Backend for JniBackend {
fn name(&self) -> &str {
"jni"
}
fn language(&self) -> Language {
Language::Jni
}
fn capabilities(&self) -> Capabilities {
Capabilities {
supports_async: true,
supports_classes: true,
supports_enums: false,
supports_option: true,
supports_result: true,
supports_callbacks: false,
supports_streaming: true,
supports_service_api: true,
}
}
fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
if config.kotlin_android.is_none() {
anyhow::bail!(
"kotlin-android config required for JNI shim generation: \
add [crates.kotlin_android] with package = \"...\" to alef.toml"
);
}
let output_path = jni_output_path(config);
let content = emit_lib_rs(api, config);
Ok(vec![GeneratedFile {
path: output_path,
content,
generated_header: true,
}])
}
fn generate_service_api(
&self,
api: &ApiSurface,
config: &ResolvedCrateConfig,
) -> anyhow::Result<Vec<GeneratedFile>> {
super::service_api::generate(api, config)
}
fn build_config(&self) -> Option<BuildConfig> {
Some(BuildConfig {
tool: "cargo",
crate_suffix: "-jni",
build_dep: BuildDependency::Ffi,
post_build: vec![],
})
}
}
fn jni_output_path(config: &ResolvedCrateConfig) -> PathBuf {
let jni_crate = format!("{}-jni", config.jni_crate_base());
PathBuf::from(format!("crates/{jni_crate}/src/lib.rs"))
}
pub(crate) fn emit_lib_rs(api: &ApiSurface, config: &ResolvedCrateConfig) -> String {
let package = jni_kotlin_package(config);
let bridge = bridge_class_name(&config.name);
let core_crate = core_use_path(config);
let error_class = resolve_error_class(config, &package);
let mut out = String::new();
out.push_str("// Generated by alef. Do not edit by hand.\n");
out.push_str("//\n");
out.push_str("// JNI shim for the Android AAR. Every `pub unsafe extern \"system\" fn`\n");
out.push_str("// here matches one `external fun native*` in the paired Kotlin Bridge object.\n");
out.push('\n');
out.push_str("#![allow(non_snake_case)]\n");
out.push_str("#![allow(clippy::too_many_arguments)]\n");
out.push_str("#![allow(clippy::missing_safety_doc)]\n");
out.push_str("#![allow(unused_imports)]\n");
out.push_str("#![allow(unused_variables)]\n");
out.push_str("#![allow(unused_mut)]\n");
out.push_str("#![allow(dead_code)]\n");
out.push_str("#![allow(clippy::let_unit_value)]\n");
out.push_str("#![allow(clippy::unused_unit)]\n");
out.push_str("#![allow(clippy::let_and_return)]\n");
out.push('\n');
out.push_str("use std::sync::OnceLock;\n");
out.push_str("use std::sync::Mutex;\n");
out.push_str("use futures_util::stream::BoxStream;\n");
out.push_str("use futures_util::StreamExt;\n");
out.push_str("use jni::{AttachGuard, Env, EnvUnowned};\n");
out.push_str("use jni::objects::{JClass, JObject, JString};\n");
out.push_str("use jni::sys::{jboolean, jbyteArray, jlong, jstring};\n");
out.push_str("use tokio::runtime::Runtime;\n");
out.push('\n');
out.push_str(&format!("use {core_crate} as core_crate;\n"));
out.push_str("use core_crate::*; // bring trait methods into scope\n");
out.push('\n');
out.push_str(&format!("const ERROR_CLASS: &str = \"{error_class}\";\n"));
out.push('\n');
emit_runtime_helpers(&mut out);
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_default();
let visible_functions: Vec<_> = api
.functions
.iter()
.filter(|f| !f.sanitized && !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();
for f in &visible_functions {
let method_name = bridge_method_name("", &f.name);
let symbol = jni_symbol(&package, &bridge, &method_name);
emit_function_shim(
&mut out,
&symbol,
&f.name,
&f.params,
&f.return_type,
f.is_async,
f.error_type.is_some(),
&opaque_type_names,
);
}
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();
let client_type_names: std::collections::HashSet<&str> = client_types.iter().map(|t| t.name.as_str()).collect();
for ty in &client_types {
emit_client_shims(
&mut out,
ty,
api,
config,
&package,
&bridge,
&exclude_functions,
&opaque_type_names,
);
}
let top_level_opaque_returns: std::collections::HashSet<&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();
for type_name in &top_level_opaque_returns {
let free_name = destructor_method_name(type_name);
let free_symbol = jni_symbol(&package, &bridge, &free_name);
emit_destructor_shim(&mut out, &free_symbol, type_name);
}
emit_trait_bridge_shims(&mut out, config, api, &package, &bridge);
out
}
fn emit_trait_bridge_shims(
out: &mut String,
config: &ResolvedCrateConfig,
api: &ApiSurface,
package: &str,
bridge: &str,
) {
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// ---------------------------------------------------------------------------\n");
out.push_str("// Trait-bridge shims\n");
out.push_str("// ---------------------------------------------------------------------------\n\n");
for bridge_cfg in &bridges {
use heck::ToUpperCamelCase;
let trait_pascal = bridge_cfg.trait_name.to_upper_camel_case();
let trait_def = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name);
if let Some(register_fn) = bridge_cfg.register_fn.as_deref() {
let native_name = format!("nativeRegister{trait_pascal}");
let symbol = jni_symbol(package, bridge, &native_name);
let has_super_trait = bridge_cfg.super_trait.is_some();
emit_trait_register_shim(out, &symbol, &trait_pascal, register_fn, trait_def, has_super_trait);
}
if let Some(unregister_fn) = bridge_cfg.unregister_fn.as_deref() {
let native_name = format!("nativeUnregister{trait_pascal}");
let symbol = jni_symbol(package, bridge, &native_name);
emit_trait_unregister_shim(out, &symbol, unregister_fn);
}
if let Some(clear_fn) = bridge_cfg.clear_fn.as_deref() {
let native_name = format!("nativeClear{trait_pascal}s");
let symbol = jni_symbol(package, bridge, &native_name);
emit_trait_clear_shim(out, &symbol, clear_fn);
}
}
}
fn emit_trait_register_shim(
out: &mut String,
symbol: &str,
_trait_pascal: &str,
register_fn: &str,
_trait_def: Option<&TypeDef>,
has_super_trait: bool,
) {
if has_super_trait {
out.push_str(&format!(
"#[unsafe(no_mangle)]\npub unsafe extern \"system\" fn {symbol}(\n mut env: EnvUnowned,\n _class: JClass,\n impl_obj: JObject,\n) {{\n // SAFETY: env is a valid EnvUnowned passed by the JVM for this native call frame.\n let mut __jni_attach_guard = unsafe {{ jni::AttachGuard::from_unowned(env.as_raw()) }};\n let env = __jni_attach_guard.borrow_env_mut();\n"
));
out.push_str(
" let name = match jni_call_string_method(env, impl_obj, \"name\", \"()Ljava/lang/String;\") {\n",
);
out.push_str(" Ok(n) => n,\n");
out.push_str(
" Err(e) => { throw_jni_error(env, &format!(\"Failed to get implementation name: {e}\")); return; }\n",
);
out.push_str(" };\n\n");
} else {
out.push_str(&format!(
"#[unsafe(no_mangle)]\npub unsafe extern \"system\" fn {symbol}(\n mut env: EnvUnowned,\n _class: JClass,\n impl_obj: JObject,\n name: JString,\n) {{\n // SAFETY: env is a valid EnvUnowned passed by the JVM for this native call frame.\n let mut __jni_attach_guard = unsafe {{ jni::AttachGuard::from_unowned(env.as_raw()) }};\n let env = __jni_attach_guard.borrow_env_mut();\n"
));
out.push_str(" let name = match jstring_to_string(env, name) {\n");
out.push_str(" Ok(s) => s,\n");
out.push_str(
" Err(e) => { throw_jni_error(env, &format!(\"Failed to decode name parameter: {e}\")); return; }\n",
);
out.push_str(" };\n\n");
}
out.push_str(" let global_impl = match env.new_global_ref(impl_obj) {\n");
out.push_str(" Ok(g) => g,\n");
out.push_str(
" Err(e) => { throw_jni_error(env, &format!(\"Failed to create global reference: {e}\")); return; }\n",
);
out.push_str(" };\n\n");
out.push_str(" let bridge_handle = std::sync::Arc::new(global_impl.clone());\n\n");
out.push_str(&format!(
" let Some(result) = run_or_throw(env, std::panic::AssertUnwindSafe(|| core_crate::{register_fn}(&name, bridge_handle))) else {{\n"
));
out.push_str(" return;\n");
out.push_str(" };\n");
out.push_str(" if let Err(e) = result {\n");
out.push_str(" // On registration failure, the global ref is cleaned up when bridge_handle is dropped\n");
out.push_str(" throw_jni_error(env, &format!(\"{e}\"));\n");
out.push_str(" }\n");
out.push_str("}\n\n");
}
fn emit_trait_unregister_shim(out: &mut String, symbol: &str, unregister_fn: &str) {
out.push_str(&format!(
"#[unsafe(no_mangle)]\npub unsafe extern \"system\" fn {symbol}(\n mut env: EnvUnowned,\n _class: JClass,\n name: JString,\n) {{\n // SAFETY: env is a valid EnvUnowned passed by the JVM for this native call frame.\n let mut __jni_attach_guard = unsafe {{ jni::AttachGuard::from_unowned(env.as_raw()) }};\n let env = __jni_attach_guard.borrow_env_mut();\n"
));
out.push_str(" let name = match jstring_to_string(env, name) {\n");
out.push_str(" Ok(s) => s,\n");
out.push_str(" Err(e) => { throw_jni_error(env, &format!(\"{e}\")); return; }\n");
out.push_str(" };\n");
out.push_str(&format!(
" let Some(result) = run_or_throw(env, std::panic::AssertUnwindSafe(|| core_crate::{unregister_fn}(&name))) else {{\n"
));
out.push_str(" return;\n");
out.push_str(" };\n");
out.push_str(" if let Err(e) = result {\n");
out.push_str(" throw_jni_error(env, &format!(\"{e}\"));\n");
out.push_str(" }\n");
out.push_str("}\n\n");
}
fn emit_trait_clear_shim(out: &mut String, symbol: &str, clear_fn: &str) {
out.push_str(&format!(
"#[unsafe(no_mangle)]\npub unsafe extern \"system\" fn {symbol}(\n mut env: EnvUnowned,\n _class: JClass,\n) {{\n // SAFETY: env is a valid EnvUnowned passed by the JVM for this native call frame.\n let mut __jni_attach_guard = unsafe {{ jni::AttachGuard::from_unowned(env.as_raw()) }};\n let env = __jni_attach_guard.borrow_env_mut();\n"
));
out.push_str(&format!(
" let Some(result) = run_or_throw(env, std::panic::AssertUnwindSafe(core_crate::{clear_fn})) else {{\n"
));
out.push_str(" return;\n");
out.push_str(" };\n");
out.push_str(" if let Err(e) = result {\n");
out.push_str(" throw_jni_error(env, &format!(\"{e}\"));\n");
out.push_str(" }\n");
out.push_str("}\n\n");
}
fn emit_runtime_helpers(out: &mut String) {
out.push_str("fn runtime() -> &'static Runtime {\n");
out.push_str(" static RT: OnceLock<Runtime> = OnceLock::new();\n");
out.push_str(" RT.get_or_init(|| Runtime::new().expect(\"create tokio runtime\"))\n");
out.push_str("}\n");
out.push('\n');
out.push_str(
"fn jstring_to_string(env: &mut Env<'_>, s: JString) -> std::result::Result<String, jni::errors::Error> {\n",
);
out.push_str(" s.try_to_string(env)\n");
out.push_str("}\n");
out.push('\n');
out.push_str("fn string_to_jstring(env: &mut Env<'_>, s: &str) -> jstring {\n");
out.push_str(" match env.new_string(s) {\n");
out.push_str(" Ok(o) => o.into_raw(),\n");
out.push_str(" Err(_) => std::ptr::null_mut(),\n");
out.push_str(" }\n");
out.push_str("}\n");
out.push('\n');
out.push_str("fn jni_call_string_method(env: &mut Env<'_>, obj: JObject, method_name: &str, method_sig: &str) -> std::result::Result<String, jni::errors::Error> {\n");
out.push_str(" use std::str::FromStr;\n");
out.push_str(" let class = env.get_object_class(&obj)?;\n");
out.push_str(" let name_jni = jni::strings::JNIString::from(method_name);\n");
out.push_str(" let sig_runtime = jni::signature::RuntimeMethodSignature::from_str(method_sig)?;\n");
out.push_str(" let sig = sig_runtime.method_signature();\n");
out.push_str(" let method_id = env.get_method_id(&class, name_jni, sig)?;\n");
out.push_str(" // SAFETY: method_id is valid from the preceding get_method_id call, and the method exists on the class.\n");
out.push_str(
" let result = unsafe { env.call_method_unchecked(&obj, method_id, jni::signature::ReturnType::Object, &[])? }\n",
);
out.push_str(" .l()?;\n");
out.push_str(" // SAFETY: JNI return type guaranteed a String, so the raw jstring pointer is valid.\n");
out.push_str(" let jstring = unsafe { JString::from_raw(env, result.into_raw()) };\n");
out.push_str(" jstring_to_string(env, jstring)\n");
out.push_str("}\n");
out.push('\n');
out.push_str("fn throw_jni_error(env: &mut Env<'_>, msg: &str) {\n");
out.push_str(" // If the error class cannot be found (misconfigured AAR), fall back to a\n");
out.push_str(" // generic RuntimeException so the caller always gets *some* exception rather\n");
out.push_str(" // than a silent null/zero return that looks like a valid result.\n");
out.push_str(" let class_jni = jni::strings::JNIString::from(ERROR_CLASS);\n");
out.push_str(" let msg_jni = jni::strings::JNIString::from(msg);\n");
out.push_str(" if env.throw_new(&class_jni, &msg_jni).is_err() {\n");
out.push_str(" let fallback = jni::strings::JNIString::from(\"java/lang/RuntimeException\");\n");
out.push_str(" let _ = env.throw_new(&fallback, &msg_jni);\n");
out.push_str(" }\n");
out.push_str("}\n");
out.push('\n');
out.push_str("fn run_or_throw<T, F>(env: &mut Env<'_>, f: F) -> Option<T>\n");
out.push_str("where\n");
out.push_str(" F: FnOnce() -> T + std::panic::UnwindSafe,\n");
out.push_str("{\n");
out.push_str(" match std::panic::catch_unwind(f) {\n");
out.push_str(" Ok(v) => Some(v),\n");
out.push_str(" Err(payload) => {\n");
out.push_str(" let msg = payload.downcast_ref::<String>().cloned()\n");
out.push_str(" .or_else(|| payload.downcast_ref::<&str>().map(|s| (*s).to_string()))\n");
out.push_str(" .unwrap_or_else(|| \"panic in native code\".to_string());\n");
out.push_str(" throw_jni_error(env, &format!(\"native panic: {msg}\"));\n");
out.push_str(" None\n");
out.push_str(" }\n");
out.push_str(" }\n");
out.push_str("}\n");
out.push('\n');
}
#[allow(clippy::too_many_arguments)]
fn emit_client_shims(
out: &mut String,
ty: &TypeDef,
api: &ApiSurface,
config: &ResolvedCrateConfig,
package: &str,
bridge: &str,
exclude_functions: &std::collections::HashSet<&str>,
opaque_type_names: &std::collections::HashSet<&str>,
) {
for method in ty.methods.iter().filter(|m| !m.sanitized && !m.is_static) {
if exclude_functions.contains(method.name.as_str()) {
continue;
}
let method_name = bridge_method_name(&ty.name, &method.name);
let symbol = jni_symbol(package, bridge, &method_name);
let receiver_is_mut = matches!(method.receiver.as_ref(), Some(crate::core::ir::ReceiverKind::RefMut));
let receiver_owned = matches!(method.receiver.as_ref(), Some(crate::core::ir::ReceiverKind::Owned));
emit_method_shim(
out,
&symbol,
&ty.name,
&method.name,
&method.params,
&method.return_type,
method.is_async,
method.error_type.is_some(),
receiver_is_mut,
receiver_owned,
opaque_type_names,
);
}
let free_name = destructor_method_name(&ty.name);
let free_symbol = jni_symbol(package, bridge, &free_name);
emit_destructor_shim(out, &free_symbol, &ty.name);
if let Some(ctor) = config.client_constructors.get(&ty.name) {
let ctor_method_name = format!("nativeNew{}", &ty.name);
let ctor_symbol = jni_symbol(package, bridge, &ctor_method_name);
emit_constructor_shim(out, &ctor_symbol, ty, config, ctor);
}
let streaming: Vec<_> = config
.adapters
.iter()
.filter(|a| matches!(a.pattern, AdapterPattern::Streaming) && a.owner_type.as_deref() == Some(ty.name.as_str()))
.collect();
for adapter in &streaming {
let (start_name, next_name, free_adapter_name) = streaming_method_names(&ty.name, &adapter.name);
let start_sym = jni_symbol(package, bridge, &start_name);
let next_sym = jni_symbol(package, bridge, &next_name);
let free_sym = jni_symbol(package, bridge, &free_adapter_name);
emit_streaming_shims(out, &start_sym, &next_sym, &free_sym, ty, adapter, api);
}
let _ = api; }
#[allow(clippy::too_many_arguments)]
fn emit_function_shim(
out: &mut String,
symbol: &str,
rust_fn_name: &str,
params: &[ParamDef],
return_type: &TypeRef,
is_async: bool,
has_error: bool,
opaque_type_names: &std::collections::HashSet<&str>,
) {
let core_fn = format!("core_crate::{}", rust_fn_name.replace('-', "_"));
let is_opaque_return = matches!(return_type, TypeRef::Named(n) if opaque_type_names.contains(n.as_str()));
let ret_decl = if is_opaque_return { " -> jlong" } else { " -> jstring" };
let err_null = if is_opaque_return { "0" } else { "std::ptr::null_mut()" };
let mut param_sigs = String::new();
let mut unmarshal = String::new();
let mut call_args = String::new();
for p in params {
let rust_name = p.name.replace('-', "_");
let base_ty = match &p.ty {
TypeRef::Optional(inner) => inner.as_ref(),
other => other,
};
match base_ty {
TypeRef::String => {
param_sigs.push_str(&format!(" {rust_name}: JString,\n"));
unmarshal.push_str(&format!(
" let {rust_name} = match jstring_to_string(env, {rust_name}) {{\n"
));
unmarshal.push_str(" Ok(s) => s,\n");
unmarshal.push_str(&format!(
" Err(e) => {{ throw_jni_error(env, &format!(\"{{e}}\")); return {err_null}; }}\n"
));
unmarshal.push_str(" };\n");
if p.optional {
call_args.push_str(&format!(
"if {rust_name}.is_empty() {{ None }} else {{ Some({rust_name}) }}"
));
} else if p.is_ref {
call_args.push_str(&format!("&{rust_name}"));
} else {
call_args.push_str(&rust_name);
}
}
TypeRef::Primitive(prim) => {
let jni_ty = jni_primitive_type(prim);
param_sigs.push_str(&format!(" {rust_name}: {jni_ty},\n"));
let cast = primitive_cast(prim);
let cast_expr = if cast.is_empty() {
rust_name.clone()
} else {
format!("{rust_name} as {cast}")
};
if p.optional {
let zero_lit = primitive_zero_literal(prim);
if let Some(zero) = zero_lit {
call_args.push_str(&format!(
"if {rust_name} != {zero} {{ Some({cast_expr}) }} else {{ None }}"
));
} else {
call_args.push_str(&format!("Some({cast_expr})"));
}
} else {
call_args.push_str(&cast_expr);
}
}
TypeRef::Named(type_name) if opaque_type_names.contains(type_name.as_str()) => {
param_sigs.push_str(&format!(" {rust_name}: jlong,\n"));
let type_path = format!("core_crate::{type_name}");
unmarshal.push_str(&format!(
" // SAFETY: {rust_name} was allocated by the matching constructor shim and\n"
));
unmarshal.push_str(" // remains valid until the destructor shim is called.\n");
unmarshal.push_str(&format!(
" let {rust_name}: &{type_path} = unsafe {{ &*({rust_name} as *const {type_path}) }};\n"
));
call_args.push_str(&rust_name);
}
_ => {
param_sigs.push_str(&format!(" {rust_name}: JString,\n"));
unmarshal.push_str(&format!(
" let {rust_name}_str = match jstring_to_string(env, {rust_name}) {{\n"
));
unmarshal.push_str(" Ok(s) => s,\n");
unmarshal.push_str(&format!(
" Err(e) => {{ throw_jni_error(env, &format!(\"{{e}}\")); return {err_null}; }}\n"
));
unmarshal.push_str(" };\n");
let type_path = type_ref_to_core_path(base_ty, "core_crate");
if p.optional {
unmarshal.push_str(&format!(
" let {rust_name}: Option<{type_path}> = if {rust_name}_str.is_empty() {{\n"
));
unmarshal.push_str(" None\n");
unmarshal.push_str(" } else {\n");
unmarshal.push_str(&format!(
" match serde_json::from_str::<{type_path}>(&{rust_name}_str) {{\n"
));
unmarshal.push_str(" Ok(v) => Some(v),\n");
unmarshal.push_str(&format!(
" Err(e) => {{ throw_jni_error(env, &format!(\"deserialize: {{e}}\")); return {err_null}; }}\n"
));
unmarshal.push_str(" }\n");
unmarshal.push_str(" };\n");
call_args.push_str(&rust_name);
} else {
unmarshal.push_str(&format!(
" let {rust_name}: {type_path} = match serde_json::from_str(&{rust_name}_str) {{\n"
));
unmarshal.push_str(" Ok(v) => v,\n");
unmarshal.push_str(&format!(
" Err(e) => {{ throw_jni_error(env, &format!(\"deserialize: {{e}}\")); return {err_null}; }}\n"
));
unmarshal.push_str(" };\n");
if p.is_ref {
call_args.push_str(&format!("&{rust_name}"));
} else {
call_args.push_str(&rust_name);
}
}
}
}
call_args.push_str(", ");
}
if call_args.ends_with(", ") {
call_args.truncate(call_args.len() - 2);
}
out.push_str(&format!(
"#[unsafe(no_mangle)]\npub unsafe extern \"system\" fn {symbol}(\n mut env: EnvUnowned,\n _class: JClass,\n{param_sigs}){ret_decl} {{\n // SAFETY: env is a valid EnvUnowned passed by the JVM for this native call frame.\n let mut __jni_attach_guard = unsafe {{ jni::AttachGuard::from_unowned(env.as_raw()) }};\n let env = __jni_attach_guard.borrow_env_mut();\n"
));
out.push_str(&unmarshal);
let raw_call = if call_args.is_empty() {
format!("{core_fn}()")
} else {
format!("{core_fn}({call_args})")
};
if has_error {
if is_async {
out.push_str(&format!(
" let Some(result) = run_or_throw(env, std::panic::AssertUnwindSafe(|| runtime().block_on({raw_call}))) else {{\n"
));
out.push_str(&format!(" return {err_null};\n"));
out.push_str(" };\n");
} else {
out.push_str(&format!(" let result = {raw_call};\n"));
}
out.push_str(" match result {\n");
out.push_str(" Err(e) => {\n");
out.push_str(" throw_jni_error(env, &format!(\"{e}\"));\n");
out.push_str(&format!(" {err_null}\n"));
out.push_str(" }\n");
out.push_str(" Ok(v) => {\n");
if is_opaque_return {
out.push_str(" Box::into_raw(Box::new(v)) as jlong\n");
} else if matches!(return_type, TypeRef::Unit) {
out.push_str(" string_to_jstring(env, \"null\")\n");
} else {
out.push_str(" let s = match serde_json::to_string(&v) {\n");
out.push_str(" Ok(s) => s,\n");
out.push_str(&format!(
" Err(e) => {{ throw_jni_error(env, &format!(\"serialize: {{e}}\")); return {err_null}; }}\n"
));
out.push_str(" };\n");
out.push_str(" string_to_jstring(env, &s)\n");
}
out.push_str(" }\n");
out.push_str(" }\n");
} else {
if is_async {
out.push_str(&format!(
" let Some(v) = run_or_throw(env, std::panic::AssertUnwindSafe(|| runtime().block_on({raw_call}))) else {{\n"
));
out.push_str(&format!(" return {err_null};\n"));
out.push_str(" };\n");
} else {
out.push_str(&format!(" let v = {raw_call};\n"));
}
if is_opaque_return {
out.push_str(" Box::into_raw(Box::new(v)) as jlong\n");
} else if matches!(return_type, TypeRef::Unit) {
out.push_str(" string_to_jstring(env, \"null\")\n");
} else {
out.push_str(" let s = match serde_json::to_string(&v) {\n");
out.push_str(" Ok(s) => s,\n");
out.push_str(&format!(
" Err(e) => {{ throw_jni_error(env, &format!(\"serialize: {{e}}\")); return {err_null}; }}\n"
));
out.push_str(" };\n");
out.push_str(" string_to_jstring(env, &s)\n");
}
}
out.push_str("}\n\n");
}
#[allow(clippy::too_many_arguments)]
fn emit_method_shim(
out: &mut String,
symbol: &str,
type_name: &str,
method_name: &str,
params: &[ParamDef],
return_type: &TypeRef,
is_async: bool,
has_error: bool,
receiver_is_mut: bool,
receiver_owned: bool,
opaque_type_names: &std::collections::HashSet<&str>,
) {
let rust_method = method_name.replace('-', "_");
let has_params = !params.is_empty();
let is_opaque_return = matches!(return_type, TypeRef::Named(n) if opaque_type_names.contains(n.as_str()));
let is_optional_opaque_return = matches!(
return_type,
TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Named(n) if opaque_type_names.contains(n.as_str()))
);
let ret_decl = if is_opaque_return || is_optional_opaque_return {
" -> jlong".to_string()
} else {
method_return_type_decl(return_type)
};
let ret_null = if is_opaque_return || is_optional_opaque_return {
"0"
} else {
method_return_null(return_type)
};
let request_param = if !has_params {
String::new()
} else if params.len() == 1 {
let p = ¶ms[0];
let rust_name = p.name.replace('-', "_");
let base_ty = match &p.ty {
TypeRef::Optional(inner) => inner.as_ref(),
other => other,
};
match base_ty {
TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::U8)) => {
format!(" {rust_name}: jbyteArray,\n")
}
TypeRef::Bytes => format!(" {rust_name}: jbyteArray,\n"),
_ => " request_json: JString,\n".to_string(),
}
} else {
" request_json: JString,\n".to_string()
};
out.push_str(&format!(
"#[unsafe(no_mangle)]\npub unsafe extern \"system\" fn {symbol}(\n mut env: EnvUnowned,\n _class: JClass,\n handle: jlong,\n{request_param}){ret_decl} {{\n // SAFETY: env is a valid EnvUnowned passed by the JVM for this native call frame.\n let mut __jni_attach_guard = unsafe {{ jni::AttachGuard::from_unowned(env.as_raw()) }};\n let env = __jni_attach_guard.borrow_env_mut();\n"
));
out.push_str(" // SAFETY: handle was allocated by the matching constructor shim and remains\n");
out.push_str(" // valid until nativeFree is called. The Kotlin AutoCloseable.close() guarantee\n");
out.push_str(" // ensures the handle outlives this call.\n");
if receiver_owned {
out.push_str(&format!(
" let client: core_crate::{type_name} = unsafe {{ (*(handle as *const core_crate::{type_name})).clone() }};\n"
));
} else if receiver_is_mut {
out.push_str(&format!(
" let client: &mut core_crate::{type_name} = unsafe {{ &mut *(handle as *mut core_crate::{type_name}) }};\n"
));
} else {
out.push_str(&format!(
" let client: &core_crate::{type_name} = unsafe {{ &*(handle as *const core_crate::{type_name}) }};\n"
));
}
let call_args: String = if !has_params {
String::new()
} else if params.len() == 1 {
let p = ¶ms[0];
let rust_name = p.name.replace('-', "_");
let base_ty = match &p.ty {
TypeRef::Optional(inner) => inner.as_ref(),
other => other,
};
let unmarshal_produces_option = p.optional
&& !matches!(
base_ty,
TypeRef::Vec(_) | TypeRef::Bytes | TypeRef::Path | TypeRef::String
);
emit_single_param_unmarshal(out, &rust_name, base_ty, ret_null, unmarshal_produces_option);
let is_vec_string_ref =
p.is_ref && matches!(base_ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::String));
if is_vec_string_ref {
let refs_name = format!("{rust_name}_refs");
out.push_str(&format!(
" let {refs_name}: Vec<&str> = {rust_name}_vec.iter().map(String::as_str).collect();\n"
));
format!("&{refs_name}")
} else if unmarshal_produces_option {
rust_name
} else if p.optional {
format!("Some({rust_name})")
} else if p.is_ref {
format!("&{rust_name}")
} else {
rust_name
}
} else {
out.push_str(" let req_str = match jstring_to_string(env, request_json) {\n");
out.push_str(" Ok(s) => s,\n");
out.push_str(&format!(" Err(e) => {{ throw_jni_error(env, &format!(\"invalid request_json: {{e}}\")); return {ret_null}; }}\n"));
out.push_str(" };\n");
out.push_str(
" let req_map: serde_json::Map<String, serde_json::Value> = match serde_json::from_str(&req_str) {\n",
);
out.push_str(" Ok(m) => m,\n");
out.push_str(&format!(
" Err(e) => {{ throw_jni_error(env, &format!(\"param deserialize: {{e}}\")); return {ret_null}; }}\n"
));
out.push_str(" };\n");
let mut args = Vec::new();
for p in params {
let rust_name = p.name.replace('-', "_");
let base_ty = match &p.ty {
TypeRef::Optional(inner) => inner.as_ref(),
other => other,
};
let type_path = type_ref_to_core_path(base_ty, "core_crate");
out.push_str(&format!(
" let {rust_name}: {type_path} = match req_map.get(\"{rust_name}\").and_then(|v| serde_json::from_value(v.clone()).ok()) {{\n"
));
out.push_str(" Some(v) => v,\n");
out.push_str(&format!(
" None => {{ throw_jni_error(env, \"missing param: {rust_name}\"); return {ret_null}; }}\n"
));
out.push_str(" };\n");
let call_arg = if p.optional {
format!("Some({rust_name})")
} else if p.is_ref {
format!("&{rust_name}")
} else {
rust_name
};
args.push(call_arg);
}
args.join(", ")
};
let call_expr = if call_args.is_empty() {
format!("client.{rust_method}()")
} else {
format!("client.{rust_method}({call_args})")
};
if has_error {
if is_async {
out.push_str(&format!(
" let Some(result) = run_or_throw(env, std::panic::AssertUnwindSafe(|| runtime().block_on({call_expr}))) else {{\n"
));
out.push_str(&format!(" return {ret_null};\n"));
out.push_str(" };\n");
} else {
out.push_str(&format!(" let result = {call_expr};\n"));
}
out.push_str(" match result {\n");
out.push_str(" Err(e) => {\n");
out.push_str(" throw_jni_error(env, &format!(\"{e}\"));\n");
out.push_str(&format!(" {ret_null}\n"));
out.push_str(" }\n");
out.push_str(" Ok(v) => {\n");
if is_opaque_return {
out.push_str(" Box::into_raw(Box::new(v)) as jlong\n");
} else if is_optional_opaque_return {
out.push_str(" match v {\n");
out.push_str(" None => 0i64,\n");
out.push_str(" Some(inner) => Box::into_raw(Box::new(inner)) as jlong,\n");
out.push_str(" }\n");
} else {
emit_return_marshal(out, return_type, ret_null);
}
out.push_str(" }\n");
out.push_str(" }\n");
} else {
if is_async {
out.push_str(&format!(
" let Some(v) = run_or_throw(env, std::panic::AssertUnwindSafe(|| runtime().block_on({call_expr}))) else {{\n"
));
out.push_str(&format!(" return {ret_null};\n"));
out.push_str(" };\n");
} else {
out.push_str(&format!(" let v = {call_expr};\n"));
}
if is_opaque_return {
out.push_str(" Box::into_raw(Box::new(v)) as jlong\n");
} else if is_optional_opaque_return {
out.push_str(" match v {\n");
out.push_str(" None => 0i64,\n");
out.push_str(" Some(inner) => Box::into_raw(Box::new(inner)) as jlong,\n");
out.push_str(" }\n");
} else {
emit_return_marshal_with_indent(out, return_type, " ", ret_null);
}
}
out.push_str("}\n\n");
}
fn emit_single_param_unmarshal(out: &mut String, rust_name: &str, ty: &TypeRef, ret_null: &str, is_optional: bool) {
match ty {
TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::U8)) => {
out.push_str(&format!(
" let {rust_name}_jarr = unsafe {{ jni::objects::JByteArray::from_raw(env, {rust_name}) }};\n"
));
out.push_str(&format!(
" let {rust_name}: Vec<u8> = match env.convert_byte_array(&{rust_name}_jarr) {{\n"
));
out.push_str(" Ok(v) => v,\n");
out.push_str(&format!(
" Err(e) => {{ throw_jni_error(env, &format!(\"{{e}}\")); return {ret_null}; }}\n"
));
out.push_str(" };\n");
}
TypeRef::Bytes => {
out.push_str(&format!(
" let {rust_name}_jarr = unsafe {{ jni::objects::JByteArray::from_raw(env, {rust_name}) }};\n"
));
out.push_str(&format!(
" let {rust_name}: Vec<u8> = match env.convert_byte_array(&{rust_name}_jarr) {{\n"
));
out.push_str(" Ok(v) => v,\n");
out.push_str(&format!(
" Err(e) => {{ throw_jni_error(env, &format!(\"{{e}}\")); return {ret_null}; }}\n"
));
out.push_str(" };\n");
}
TypeRef::Path => {
out.push_str(" let req_str = match jstring_to_string(env, request_json) {\n");
out.push_str(" Ok(s) => s,\n");
out.push_str(&format!(
" Err(e) => {{ throw_jni_error(env, &format!(\"{{e}}\")); return {ret_null}; }}\n"
));
out.push_str(" };\n");
out.push_str(&format!(" let {rust_name} = std::path::PathBuf::from(req_str);\n"));
}
TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::String) => {
out.push_str(" let req_str = match jstring_to_string(env, request_json) {\n");
out.push_str(" Ok(s) => s,\n");
out.push_str(&format!(
" Err(e) => {{ throw_jni_error(env, &format!(\"{{e}}\")); return {ret_null}; }}\n"
));
out.push_str(" };\n");
out.push_str(&format!(
" let {rust_name}_vec: Vec<String> = match serde_json::from_str(&req_str) {{\n"
));
out.push_str(" Ok(v) => v,\n");
out.push_str(&format!(" Err(e) => {{ throw_jni_error(env, &format!(\"request deserialize: {{e}}\")); return {ret_null}; }}\n"));
out.push_str(" };\n");
out.push_str(&format!(" let {rust_name} = {rust_name}_vec.clone();\n"));
}
TypeRef::String => {
out.push_str(" let req_str = match jstring_to_string(env, request_json) {\n");
out.push_str(" Ok(s) => s,\n");
out.push_str(&format!(
" Err(e) => {{ throw_jni_error(env, &format!(\"{{e}}\")); return {ret_null}; }}\n"
));
out.push_str(" };\n");
out.push_str(&format!(
" let {rust_name}: String = match serde_json::from_str(&req_str) {{\n"
));
out.push_str(" Ok(s) => s,\n");
out.push_str(" Err(_) => req_str,\n");
out.push_str(" };\n");
}
_ => {
out.push_str(" let req_str = match jstring_to_string(env, request_json) {\n");
out.push_str(" Ok(s) => s,\n");
out.push_str(&format!(
" Err(e) => {{ throw_jni_error(env, &format!(\"{{e}}\")); return {ret_null}; }}\n"
));
out.push_str(" };\n");
let type_path = type_ref_to_core_path(ty, "core_crate");
if is_optional {
out.push_str(&format!(
" let {rust_name}: Option<{type_path}> = if req_str.is_empty() {{ None }} else {{\n"
));
out.push_str(" match serde_json::from_str(&req_str) {\n");
out.push_str(" Ok(v) => Some(v),\n");
out.push_str(&format!(" Err(e) => {{ throw_jni_error(env, &format!(\"request deserialize: {{e}}\")); return {ret_null}; }}\n"));
out.push_str(" }\n");
out.push_str(" };\n");
} else {
out.push_str(&format!(
" let {rust_name}: {type_path} = match serde_json::from_str(&req_str) {{\n"
));
out.push_str(" Ok(v) => v,\n");
out.push_str(&format!(" Err(e) => {{ throw_jni_error(env, &format!(\"request deserialize: {{e}}\")); return {ret_null}; }}\n"));
out.push_str(" };\n");
}
}
}
}
fn emit_return_marshal(out: &mut String, return_type: &TypeRef, ret_null: &str) {
emit_return_marshal_with_indent(out, return_type, " ", ret_null);
}
fn emit_return_marshal_with_indent(out: &mut String, return_type: &TypeRef, indent: &str, ret_null: &str) {
match return_type {
TypeRef::Unit => {
}
TypeRef::Primitive(PrimitiveType::Bool) => {
out.push_str(&format!("{indent}v\n"));
}
TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::U8)) => {
out.push_str(&format!("{indent}match env.byte_array_from_slice(&v) {{\n"));
out.push_str(&format!(
"{indent} Ok(arr) => {{ use jni::objects::JObject; JObject::from(arr).into_raw() as jbyteArray }}\n"
));
out.push_str(&format!("{indent} Err(_) => std::ptr::null_mut(),\n"));
out.push_str(&format!("{indent}}}\n"));
}
TypeRef::Bytes => {
out.push_str(&format!("{indent}match env.byte_array_from_slice(v.as_ref()) {{\n"));
out.push_str(&format!(
"{indent} Ok(arr) => {{ use jni::objects::JObject; JObject::from(arr).into_raw() as jbyteArray }}\n"
));
out.push_str(&format!("{indent} Err(_) => std::ptr::null_mut(),\n"));
out.push_str(&format!("{indent}}}\n"));
}
TypeRef::Primitive(p) => {
let jni_ty = jni_primitive_type(p);
out.push_str(&format!("{indent}v as {jni_ty}\n"));
}
_ => {
out.push_str(&format!("{indent}let s = match serde_json::to_string(&v) {{\n"));
out.push_str(&format!("{indent} Ok(s) => s,\n"));
out.push_str(&format!(
"{indent} Err(e) => {{ throw_jni_error(env, &format!(\"serialize: {{e}}\")); return {ret_null}; }}\n"
));
out.push_str(&format!("{indent}}};\n"));
out.push_str(&format!("{indent}string_to_jstring(env, &s)\n"));
}
}
}
fn emit_constructor_shim(
out: &mut String,
symbol: &str,
ty: &TypeDef,
config: &ResolvedCrateConfig,
ctor: &ClientConstructorConfig,
) {
let type_name = &ty.name;
let core_prefix = core_use_path(config);
let mut param_sigs = String::new();
let mut unmarshal = String::new();
let mut call_args = Vec::new();
for param in &ctor.params {
let rust_name = param.name.replace('-', "_");
if param.ty.contains("c_char") {
param_sigs.push_str(&format!(" {rust_name}: JString,\n"));
unmarshal.push_str(&format!(
" let {rust_name} = match jstring_to_string(env, {rust_name}) {{\n"
));
unmarshal.push_str(" Ok(s) => s,\n");
unmarshal.push_str(" Err(e) => { throw_jni_error(env, &format!(\"{e}\")); return 0; }\n");
unmarshal.push_str(" };\n");
call_args.push(rust_name.clone());
} else {
param_sigs.push_str(&format!(" {rust_name}: jlong,\n"));
call_args.push(rust_name.clone());
}
}
let body_expr = ctor
.body
.replace("{type_name}", type_name)
.replace("{source_path}", &format!("{core_prefix}::{type_name}"));
let call_expr = if call_args.is_empty() || body_expr.contains('(') {
body_expr.clone()
} else {
format!("{}({})", body_expr, call_args.join(", "))
};
out.push_str(&format!(
"#[unsafe(no_mangle)]\npub unsafe extern \"system\" fn {symbol}(\n mut env: EnvUnowned,\n _class: JClass,\n{param_sigs}) -> jlong {{\n"
));
out.push_str(" // SAFETY: env is a valid EnvUnowned passed by the JVM for this native call frame.\n");
out.push_str(" let mut __jni_attach_guard = unsafe { jni::AttachGuard::from_unowned(env.as_raw()) };\n");
out.push_str(" let env = __jni_attach_guard.borrow_env_mut();\n");
out.push_str(&unmarshal);
let has_error = ctor.error_type.is_some() || ctor.body.contains("->") || {
call_expr.contains("?") || call_expr.ends_with(")")
};
out.push_str(&format!(
" let Some(result) = run_or_throw(env, std::panic::AssertUnwindSafe(|| {call_expr})) else {{\n"
));
out.push_str(" return 0;\n");
out.push_str(" };\n");
out.push_str(" match result {\n");
out.push_str(" Err(e) => {\n");
out.push_str(" throw_jni_error(env, &format!(\"{e}\"));\n");
out.push_str(" 0\n");
out.push_str(" }\n");
out.push_str(" Ok(v) => Box::into_raw(Box::new(v)) as jlong,\n");
out.push_str(" }\n");
out.push_str("}\n\n");
let _ = has_error; }
fn emit_destructor_shim(out: &mut String, symbol: &str, type_name: &str) {
out.push_str(&format!(
"#[unsafe(no_mangle)]\npub unsafe extern \"system\" fn {symbol}(\n _env: EnvUnowned,\n _class: JClass,\n handle: jlong,\n) {{\n"
));
out.push_str(" if handle == 0 { return; }\n");
out.push_str(" // SAFETY: `handle` was allocated by the matching constructor shim and\n");
out.push_str(" // ownership is transferred back here for drop via Box::from_raw.\n");
out.push_str(&format!(
" unsafe {{ let _ = Box::from_raw(handle as *mut core_crate::{type_name}); }}\n"
));
out.push_str("}\n\n");
}
#[allow(clippy::too_many_arguments)]
fn emit_streaming_shims(
out: &mut String,
start_sym: &str,
next_sym: &str,
free_sym: &str,
ty: &TypeDef,
adapter: &crate::core::config::AdapterConfig,
_api: &ApiSurface,
) {
let type_name = &ty.name;
let adapter_pascal = {
use heck::ToUpperCamelCase;
adapter.name.to_upper_camel_case()
};
let stream_handle_type = format!("{type_name}{adapter_pascal}StreamHandle");
let adapter_method = adapter.name.replace('-', "_");
let item_type = adapter
.item_type
.as_deref()
.map(|t| format!("core_crate::{t}"))
.unwrap_or_else(|| "serde_json::Value".to_string());
let stream_item_alias = format!("{stream_handle_type}Item");
let stream_box_alias = format!("{stream_handle_type}Stream");
out.push_str(&format!(
"type {stream_item_alias} = std::result::Result<{item_type}, Box<dyn std::error::Error + Send + Sync + 'static>>;\n"
));
out.push_str(&format!(
"type {stream_box_alias} = BoxStream<'static, {stream_item_alias}>;\n\n"
));
out.push_str(&format!("struct {stream_handle_type} {{\n"));
out.push_str(" rt: &'static Runtime,\n");
out.push_str(&format!(" stream: Mutex<Option<{stream_box_alias}>>,\n"));
out.push_str("}\n\n");
out.push_str(&format!(
"#[unsafe(no_mangle)]\npub unsafe extern \"system\" fn {start_sym}(\n mut env: EnvUnowned,\n _class: JClass,\n client_handle: jlong,\n request_json: JString,\n) -> jlong {{\n // SAFETY: env is a valid EnvUnowned passed by the JVM for this native call frame.\n let mut __jni_attach_guard = unsafe {{ jni::AttachGuard::from_unowned(env.as_raw()) }};\n let env = __jni_attach_guard.borrow_env_mut();\n"
));
out.push_str(" // SAFETY: client_handle was produced by the matching constructor shim.\n");
out.push_str(&format!(
" let client: &core_crate::{type_name} = unsafe {{ &*(client_handle as *const core_crate::{type_name}) }};\n"
));
out.push_str(" let req_str = match jstring_to_string(env, request_json) {\n");
out.push_str(" Ok(s) => s,\n");
out.push_str(" Err(e) => { throw_jni_error(env, &format!(\"{e}\")); return 0; }\n");
out.push_str(" };\n");
if let Some(first_param) = adapter.params.first() {
let param_type = first_param.ty.rsplit("::").next().unwrap_or(&first_param.ty);
out.push_str(&format!(
" let request: core_crate::{param_type} = match serde_json::from_str(&req_str) {{\n"
));
out.push_str(" Ok(v) => v,\n");
out.push_str(" Err(e) => { throw_jni_error(env, &format!(\"{e}\")); return 0; }\n");
out.push_str(" };\n");
out.push_str(&format!(
" let Some(stream_result) = run_or_throw(env, std::panic::AssertUnwindSafe(|| runtime().block_on(async {{ client.{adapter_method}(request).await }}))) else {{\n"
));
out.push_str(" return 0;\n");
out.push_str(" };\n");
} else {
out.push_str(&format!(
" let Some(stream_result) = run_or_throw(env, std::panic::AssertUnwindSafe(|| runtime().block_on(async {{ client.{adapter_method}().await }}))) else {{\n"
));
out.push_str(" return 0;\n");
out.push_str(" };\n");
}
out.push_str(" let stream = match stream_result {\n");
out.push_str(" Ok(s) => s,\n");
out.push_str(" Err(e) => { throw_jni_error(env, &format!(\"{e}\")); return 0; }\n");
out.push_str(" };\n");
out.push_str(" // Map the concrete error type to Box<dyn Error> so the handle type is\n");
out.push_str(" // independent of the stream's error associated type.\n");
out.push_str(" let mapped = {\n");
out.push_str(" use futures_util::StreamExt;\n");
out.push_str(" Box::pin(stream.map(|r| r.map_err(|e| -> Box<dyn std::error::Error + Send + Sync + 'static> { Box::new(e) })))\n");
out.push_str(" };\n");
out.push_str(&format!(" let handle = Box::new({stream_handle_type} {{\n"));
out.push_str(" rt: runtime(),\n");
out.push_str(" stream: Mutex::new(Some(mapped)),\n");
out.push_str(" });\n");
out.push_str(" Box::into_raw(handle) as jlong\n");
out.push_str("}\n\n");
out.push_str(&format!(
"#[unsafe(no_mangle)]\npub unsafe extern \"system\" fn {next_sym}(\n mut env: EnvUnowned,\n _class: JClass,\n stream_handle: jlong,\n) -> jstring {{\n // SAFETY: env is a valid EnvUnowned passed by the JVM for this native call frame.\n let mut __jni_attach_guard = unsafe {{ jni::AttachGuard::from_unowned(env.as_raw()) }};\n let env = __jni_attach_guard.borrow_env_mut();\n"
));
out.push_str(" if stream_handle == 0 { return std::ptr::null_mut(); }\n");
out.push_str(" // SAFETY: stream_handle was produced by the matching Start shim.\n");
out.push_str(&format!(
" let h = unsafe {{ &*(stream_handle as *const {stream_handle_type}) }};\n"
));
out.push_str(" let mut guard = match h.stream.lock() {\n");
out.push_str(" Ok(g) => g,\n");
out.push_str(" Err(_) => return std::ptr::null_mut(),\n");
out.push_str(" };\n");
out.push_str(" let Some(stream) = guard.as_mut() else { return std::ptr::null_mut(); };\n");
out.push_str(" let Some(next) = run_or_throw(env, std::panic::AssertUnwindSafe(|| h.rt.block_on(stream.next()))) else {\n");
out.push_str(" return std::ptr::null_mut();\n");
out.push_str(" };\n");
out.push_str(" match next {\n");
out.push_str(" None => std::ptr::null_mut(),\n");
out.push_str(" Some(Err(e)) => {\n");
out.push_str(" throw_jni_error(env, &format!(\"{e}\"));\n");
out.push_str(" std::ptr::null_mut()\n");
out.push_str(" }\n");
out.push_str(" Some(Ok(chunk)) => {\n");
out.push_str(" let s = match serde_json::to_string(&chunk) {\n");
out.push_str(" Ok(s) => s,\n");
out.push_str(" Err(e) => {\n");
out.push_str(
" throw_jni_error(env, &format!(\"serialize: {e}\")); return std::ptr::null_mut();\n",
);
out.push_str(" }\n");
out.push_str(" };\n");
out.push_str(" string_to_jstring(env, &s)\n");
out.push_str(" }\n");
out.push_str(" }\n");
out.push_str("}\n\n");
out.push_str(&format!(
"#[unsafe(no_mangle)]\npub unsafe extern \"system\" fn {free_sym}(\n _env: EnvUnowned,\n _class: JClass,\n stream_handle: jlong,\n) {{\n"
));
out.push_str(" if stream_handle == 0 { return; }\n");
out.push_str(" // SAFETY: stream_handle was produced by the matching Start shim.\n");
out.push_str(&format!(
" unsafe {{ let _ = Box::from_raw(stream_handle as *mut {stream_handle_type}); }}\n"
));
out.push_str("}\n\n");
}
fn method_return_type_decl(return_type: &TypeRef) -> String {
match return_type {
TypeRef::Unit => String::new(),
TypeRef::Primitive(PrimitiveType::Bool) => " -> jboolean".to_string(),
TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::U8)) => {
" -> jbyteArray".to_string()
}
TypeRef::Bytes => " -> jbyteArray".to_string(),
TypeRef::Primitive(_) => {
let jni_ty = jni_return_type(return_type);
format!(" -> {jni_ty}")
}
_ => " -> jstring".to_string(),
}
}
fn method_return_null(return_type: &TypeRef) -> &'static str {
match return_type {
TypeRef::Unit => "()",
TypeRef::Primitive(PrimitiveType::Bool) => "false",
TypeRef::Primitive(PrimitiveType::F32) => "0.0f32",
TypeRef::Primitive(PrimitiveType::F64) => "0.0f64",
TypeRef::Primitive(_) => "0",
TypeRef::Bytes => "std::ptr::null_mut()",
TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::U8)) => {
"std::ptr::null_mut()"
}
_ => "std::ptr::null_mut()",
}
}
fn jni_return_type(ty: &TypeRef) -> &'static str {
match ty {
TypeRef::Unit => "()",
TypeRef::Primitive(p) => jni_primitive_type(p),
TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::U8)) => "jbyteArray",
TypeRef::String | TypeRef::Named(_) | TypeRef::Optional(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) => "jstring",
_ => "jlong",
}
}
fn jni_primitive_type(p: &PrimitiveType) -> &'static str {
match p {
PrimitiveType::Bool => "jboolean",
PrimitiveType::I8 | PrimitiveType::U8 => "jni::sys::jbyte",
PrimitiveType::I16 | PrimitiveType::U16 => "jni::sys::jshort",
PrimitiveType::I32 | PrimitiveType::U32 => "jni::sys::jint",
PrimitiveType::I64 | PrimitiveType::U64 | PrimitiveType::Usize | PrimitiveType::Isize => "jlong",
PrimitiveType::F32 => "jni::sys::jfloat",
PrimitiveType::F64 => "jni::sys::jdouble",
}
}
fn primitive_zero_literal(p: &PrimitiveType) -> Option<&'static str> {
match p {
PrimitiveType::Bool => None,
PrimitiveType::I8
| PrimitiveType::U8
| PrimitiveType::I16
| PrimitiveType::U16
| PrimitiveType::I32
| PrimitiveType::U32
| PrimitiveType::I64
| PrimitiveType::U64
| PrimitiveType::Usize
| PrimitiveType::Isize => Some("0"),
PrimitiveType::F32 | PrimitiveType::F64 => Some("0.0"),
}
}
fn primitive_cast(p: &PrimitiveType) -> &'static str {
match p {
PrimitiveType::Bool => "bool",
PrimitiveType::I8 => "i8",
PrimitiveType::U8 => "u8",
PrimitiveType::I16 => "i16",
PrimitiveType::U16 => "u16",
PrimitiveType::I32 => "i32",
PrimitiveType::U32 => "u32",
PrimitiveType::I64 => "i64",
PrimitiveType::U64 => "u64",
PrimitiveType::F32 => "f32",
PrimitiveType::F64 => "f64",
PrimitiveType::Usize => "usize",
PrimitiveType::Isize => "isize",
}
}
fn type_ref_to_core_path(ty: &TypeRef, core_prefix: &str) -> String {
match ty {
TypeRef::String => "String".to_string(),
TypeRef::Primitive(p) => primitive_rust_type(p).to_string(),
TypeRef::Named(n) => format!("{core_prefix}::{n}"),
TypeRef::Optional(inner) => format!("Option<{}>", type_ref_to_core_path(inner, core_prefix)),
TypeRef::Vec(inner) => format!("Vec<{}>", type_ref_to_core_path(inner, core_prefix)),
TypeRef::Map(k, v) => format!(
"std::collections::HashMap<{}, {}>",
type_ref_to_core_path(k, core_prefix),
type_ref_to_core_path(v, core_prefix)
),
_ => "serde_json::Value".to_string(),
}
}
fn primitive_rust_type(p: &PrimitiveType) -> &'static str {
match p {
PrimitiveType::Bool => "bool",
PrimitiveType::I8 => "i8",
PrimitiveType::U8 => "u8",
PrimitiveType::I16 => "i16",
PrimitiveType::U16 => "u16",
PrimitiveType::I32 => "i32",
PrimitiveType::U32 => "u32",
PrimitiveType::I64 => "i64",
PrimitiveType::U64 => "u64",
PrimitiveType::F32 => "f32",
PrimitiveType::F64 => "f64",
PrimitiveType::Usize => "usize",
PrimitiveType::Isize => "isize",
}
}
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 resolve_error_class(config: &ResolvedCrateConfig, package: &str) -> String {
let package_slashed = package.replace('.', "/");
let bridge = bridge_class_name(&config.name);
format!("{package_slashed}/{bridge}Exception")
}
fn core_use_path(config: &ResolvedCrateConfig) -> String {
config.name.replace('-', "_")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn jni_return_type_unit() {
assert_eq!(jni_return_type(&TypeRef::Unit), "()");
}
#[test]
fn jni_return_type_i64() {
assert_eq!(jni_return_type(&TypeRef::Primitive(PrimitiveType::I64)), "jlong");
}
#[test]
fn jni_return_type_string() {
assert_eq!(jni_return_type(&TypeRef::String), "jstring");
}
#[test]
fn jni_return_type_vec_u8() {
assert_eq!(
jni_return_type(&TypeRef::Vec(Box::new(TypeRef::Primitive(PrimitiveType::U8)))),
"jbyteArray"
);
}
#[test]
fn throw_jni_error_has_runtime_exception_fallback() {
use crate::core::config::NewAlefConfig;
let raw: NewAlefConfig = toml::from_str(
r#"
[workspace]
languages = ["kotlin_android", "jni"]
[[crates]]
name = "demo"
sources = ["src/lib.rs"]
[crates.kotlin_android]
package = "dev.sample_crate"
namespace = "dev.sample_crate"
"#,
)
.unwrap();
let config = raw.resolve().unwrap().remove(0);
let api = crate::core::ir::ApiSurface {
crate_name: "demo".into(),
version: "0.1.0".into(),
types: vec![],
functions: vec![],
enums: vec![],
errors: vec![],
excluded_type_paths: Default::default(),
excluded_trait_names: ::std::collections::HashSet::new(),
services: vec![],
handler_contracts: vec![],
};
let content = emit_lib_rs(&api, &config);
assert!(
!content.contains("let _ = env.throw_new(ERROR_CLASS"),
"throw_jni_error must not discard the throw_new result: {content}"
);
assert!(
content.contains("if env.throw_new(&class_jni, &msg_jni).is_err()"),
"throw_jni_error must check throw_new result: {content}"
);
assert!(
content.contains("jni::strings::JNIString::from(ERROR_CLASS)"),
"throw_jni_error must wrap ERROR_CLASS in JNIString::from: {content}"
);
assert!(
content.contains("java/lang/RuntimeException"),
"throw_jni_error must fall back to RuntimeException: {content}"
);
}
}