use minijinja::context;
use crate::backends::jni::template_env;
use crate::codegen::naming::{pascal_to_snake, to_class_name};
use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{ApiSurface, EntrypointKind, HandlerContractDef, RegistrationDef, ServiceDef, TypeRef};
use crate::core::jni::{bridge_method_name, jni_package, jni_symbol, service_bridge_class_name};
use std::path::PathBuf;
fn find_contract<'a>(api: &'a ApiSurface, trait_name: &str) -> Option<&'a HandlerContractDef> {
api.handler_contracts.iter().find(|c| c.trait_name == trait_name)
}
fn internal_symbol_component(name: &str) -> String {
pascal_to_snake(name)
}
fn internal_class_component(name: &str) -> String {
to_class_name(name)
}
fn render_service_param_decl(name: &str, type_name: &str) -> String {
template_env::render(
"service_param_decl.rs.jinja",
context! {
name => name,
type_name => type_name,
},
)
}
fn typeref_to_jni_type(ty: &TypeRef, _core_import: &str) -> String {
match ty {
TypeRef::String => "jni::objects::JString",
TypeRef::Char => "c_char",
TypeRef::Primitive(p) => {
use crate::core::ir::PrimitiveType;
match p {
PrimitiveType::Bool => "jboolean",
PrimitiveType::U8 => "jbyte",
PrimitiveType::U16 => "jchar",
PrimitiveType::U32 => "jint",
PrimitiveType::U64 => "jlong",
PrimitiveType::I8 => "jbyte",
PrimitiveType::I16 => "jshort",
PrimitiveType::I32 => "jint",
PrimitiveType::I64 => "jlong",
PrimitiveType::F32 => "jfloat",
PrimitiveType::F64 => "jdouble",
PrimitiveType::Usize => "jlong",
PrimitiveType::Isize => "jlong",
}
}
TypeRef::Bytes => "*const u8",
TypeRef::Unit => "()",
_ => "jni::objects::JObject",
}
.to_owned()
}
pub(super) fn gen_service_rs(api: &ApiSurface, config: &ResolvedCrateConfig) -> String {
let core_import = config.core_import_name();
let package = jni_package(config);
let mut out = String::new();
out.push_str(&template_env::render("service_header.rs.jinja", context! {}));
for service in &api.services {
let service_bridge_class = service_bridge_class_name(&service.name);
gen_service_opaque(&mut out, service, &core_import, &package, &service_bridge_class);
}
let referenced_contracts: Vec<&HandlerContractDef> = {
let mut names: Vec<&str> = api
.services
.iter()
.flat_map(|s| s.registrations.iter())
.map(|r| r.callback_contract.as_str())
.collect();
names.sort_unstable();
names.dedup();
names.iter().filter_map(|n| find_contract(api, n)).collect()
};
for contract in &referenced_contracts {
gen_handler_bridge(&mut out, contract, &core_import);
}
for service in &api.services {
let service_bridge_class = service_bridge_class_name(&service.name);
for reg in &service.registrations {
gen_register_jni_function(
&mut out,
service,
reg,
api,
&core_import,
&package,
&service_bridge_class,
);
}
for ep in &service.entrypoints {
gen_entrypoint_jni_function(&mut out, service, ep, &core_import, &package, &service_bridge_class);
}
}
out
}
fn gen_service_opaque(
out: &mut String,
service: &ServiceDef,
_core_import: &str,
package: &str,
service_bridge_class: &str,
) {
let opaque_name = format!("{}Opaque", service.name);
let service_snake = internal_symbol_component(&service.name);
let owner_path = &service.rust_path;
let ctor_method = bridge_method_name(&service.name, "new");
let ctor_symbol = jni_symbol(package, service_bridge_class, &ctor_method);
let dtor_method = bridge_method_name(&service.name, "free");
let dtor_symbol = jni_symbol(package, service_bridge_class, &dtor_method);
out.push_str(&template_env::render(
"service_opaque.rs.jinja",
context! {
service_name => service.name,
service_snake => service_snake,
opaque_name => opaque_name,
owner_path => owner_path,
constructor_name => service.constructor.name,
ctor_symbol => ctor_symbol,
dtor_symbol => dtor_symbol,
},
));
}
fn gen_handler_bridge(out: &mut String, contract: &HandlerContractDef, core_import: &str) {
let trait_name = &contract.trait_name;
let bridge_name = format!("Jni{}Bridge", internal_class_component(trait_name));
let dispatch_name = &contract.dispatch.name;
let req_type = contract.wire_request_type.as_deref().unwrap_or("serde_json::Value");
let resp_type = contract.wire_response_type.as_deref().unwrap_or("serde_json::Value");
out.push_str(&template_env::render(
"handler_bridge_struct.rs.jinja",
context! {
trait_name => trait_name,
bridge_name => bridge_name,
},
));
let extra_param: String = contract
.dispatch_extra_params
.iter()
.map(|p| format!(", {p}"))
.collect();
let wire_name = contract.wire_param_name.as_deref().unwrap_or("request");
let req_path = if req_type == "Value" {
"serde_json::Value".to_string()
} else if req_type.contains("::") {
req_type.split("::").last().unwrap_or(req_type).to_string()
} else {
format!("{core_import}::{req_type}")
};
let resp_path = if resp_type == "Value" {
"serde_json::Value".to_string()
} else if resp_type.contains("::") {
resp_type.split("::").last().unwrap_or(resp_type).to_string()
} else {
format!("{core_import}::{resp_type}")
};
let box_err = "Box<dyn std::error::Error + Send + Sync>";
let wire_output = format!("Result<{resp_path}, {box_err}>");
let output_type = contract
.dispatch_return_type
.clone()
.unwrap_or_else(|| wire_output.clone());
let tail = match &contract.response_adapter {
Some(adapter) => format!("{adapter}(outcome)"),
None => "outcome".to_string(),
};
out.push_str(&template_env::render(
"handler_bridge_impl.rs.jinja",
context! {
core_import => core_import,
trait_name => trait_name,
bridge_name => bridge_name,
dispatch_name => dispatch_name,
extra_param => extra_param,
wire_name => wire_name,
req_path => req_path,
output_type => output_type,
wire_output => wire_output,
resp_path => resp_path,
tail => tail,
},
));
}
fn gen_register_jni_function(
out: &mut String,
service: &ServiceDef,
reg: &RegistrationDef,
api: &ApiSurface,
core_import: &str,
package: &str,
service_bridge_class: &str,
) {
let service_pascal = internal_class_component(&service.name);
let method_pascal = internal_class_component(®.method);
let contract_name = ®.callback_contract;
if let Some(contract) = find_contract(api, contract_name) {
let bridge_name = format!("Jni{}Bridge", internal_class_component(contract_name));
let opaque_name = format!("{}Opaque", service.name);
let register_method = bridge_method_name(&service.name, &format!("register_{}", reg.method));
let symbol = jni_symbol(package, service_bridge_class, ®ister_method);
let mut metadata_params_decl = String::new();
for meta_param in ®.metadata_params {
let rust_type = typeref_to_jni_type(&meta_param.ty, core_import);
metadata_params_decl.push_str(&render_service_param_decl(&meta_param.name, &rust_type));
}
let dispatch_method_name = &contract.dispatch.name;
let mut register_args: Vec<String> = reg.metadata_params.iter().map(|p| p.name.clone()).collect();
register_args.push("handler_arc".to_string());
out.push_str(&template_env::render(
"registration_function.rs.jinja",
context! {
service_pascal => service_pascal,
method_pascal => method_pascal,
symbol => symbol,
metadata_params_decl => metadata_params_decl,
dispatch_method_name => dispatch_method_name,
bridge_name => bridge_name,
core_import => core_import,
contract_name => contract_name,
opaque_name => opaque_name,
register_method => reg.method,
register_args => register_args.join(", "),
setup_block => "",
},
));
for variant in ®.variants {
gen_register_variant_jni_function(
out,
service,
reg,
variant,
api,
core_import,
package,
service_bridge_class,
);
}
}
}
#[allow(clippy::too_many_arguments)]
fn gen_register_variant_jni_function(
out: &mut String,
service: &ServiceDef,
reg: &RegistrationDef,
variant: &crate::core::ir::RegistrationVariant,
api: &ApiSurface,
core_import: &str,
package: &str,
service_bridge_class: &str,
) {
let service_pascal = internal_class_component(&service.name);
let variant_name = &variant.name;
let contract_name = ®.callback_contract;
if let Some(contract) = find_contract(api, contract_name) {
let bridge_name = format!("Jni{}Bridge", internal_class_component(contract_name));
let opaque_name = format!("{}Opaque", service.name);
let register_method = bridge_method_name(&service.name, &format!("register_{}_{}", reg.method, variant_name));
let symbol = jni_symbol(package, service_bridge_class, ®ister_method);
let dispatch_method_name = &contract.dispatch.name;
let mut free_params_decl = String::new();
for param in &variant.signature_params {
let rust_type = typeref_to_jni_type(¶m.ty, core_import);
free_params_decl.push_str(&render_service_param_decl(¶m.name, &rust_type));
}
let mut wrapper_block = String::new();
if let Some(wc) = &variant.wrapper_call {
let mut constructor_args = Vec::new();
for arg in &wc.args {
match arg {
crate::core::ir::WrapperConstructorArg::Fixed {
param_name: _,
value_expr,
} => {
constructor_args.push(value_expr.clone());
}
crate::core::ir::WrapperConstructorArg::Free { param } => {
constructor_args.push(param.name.clone());
}
}
}
wrapper_block.push_str(&template_env::render(
"wrapper_setup.rs.jinja",
context! {
name => wc.metadata_param,
wrapper_type_path => wc.wrapper_type_path,
constructor_method => wc.constructor_method,
constructor_args => constructor_args.join(", "),
},
));
}
let mut base_call_args = Vec::new();
if let Some(wc) = &variant.wrapper_call {
base_call_args.push(wc.metadata_param.clone());
}
for override_ in &variant.overrides {
base_call_args.push(override_.value_expr.clone());
}
base_call_args.push("handler_arc".to_string());
out.push_str(&template_env::render(
"registration_function.rs.jinja",
context! {
service_pascal => service_pascal,
method_pascal => variant_name,
symbol => symbol,
metadata_params_decl => free_params_decl,
dispatch_method_name => dispatch_method_name,
bridge_name => bridge_name,
core_import => core_import,
contract_name => contract_name,
opaque_name => opaque_name,
register_method => reg.method,
register_args => base_call_args.join(", "),
setup_block => wrapper_block,
},
));
}
}
fn gen_entrypoint_jni_function(
out: &mut String,
service: &ServiceDef,
ep: &crate::core::ir::EntrypointDef,
core_import: &str,
package: &str,
service_bridge_class: &str,
) {
let service_pascal = internal_class_component(&service.name);
let ep_pascal = internal_class_component(&ep.method);
let opaque_name = format!("{}Opaque", service.name);
let ep_method = bridge_method_name(&service.name, &ep.method);
let symbol = jni_symbol(package, service_bridge_class, &ep_method);
let mut params_decl = String::new();
for ep_param in &ep.params {
let jni_type = typeref_to_jni_type(&ep_param.ty, core_import);
params_decl.push_str(&render_service_param_decl(&ep_param.name, &jni_type));
}
let call_args = ep
.params
.iter()
.map(|param| param.name.as_str())
.collect::<Vec<_>>()
.join(", ");
match ep.kind {
EntrypointKind::Run => {
out.push_str(&template_env::render(
"entrypoint_run.rs.jinja",
context! {
service_pascal => service_pascal,
ep_pascal => ep_pascal,
symbol => symbol,
params_decl => params_decl,
opaque_name => opaque_name,
ep_method => ep.method,
call_args => call_args,
},
));
}
EntrypointKind::Finalize => {
out.push_str(&template_env::render(
"entrypoint_finalize.rs.jinja",
context! {
service_pascal => service_pascal,
ep_pascal => ep_pascal,
symbol => symbol,
params_decl => params_decl,
opaque_name => opaque_name,
ep_method => ep.method,
call_args => call_args,
},
));
}
}
}
pub fn generate(api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
if api.services.is_empty() {
return Ok(vec![]);
}
let jni_crate = format!("{}-jni", config.jni_crate_base());
let output_dir = PathBuf::from(format!("crates/{jni_crate}/src/service.rs"));
let service_rs = gen_service_rs(api, config);
Ok(vec![GeneratedFile {
path: output_dir,
content: service_rs,
generated_header: true,
}])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ir::{
EntrypointDef, EntrypointKind, HandlerContractDef, MethodDef, ParamDef, RegistrationDef, ServiceDef, TypeRef,
};
fn make_fixture_surface() -> ApiSurface {
let constructor = MethodDef {
name: "new".to_owned(),
params: vec![],
return_type: TypeRef::Unit,
is_async: false,
is_static: true,
error_type: None,
doc: "Create a new service owner.".to_owned(),
receiver: None,
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
};
let registration = RegistrationDef {
method: "add_handler".to_owned(),
callback_param: "handler".to_owned(),
callback_contract: "RequestHandler".to_owned(),
metadata_params: vec![ParamDef {
name: "path".to_owned(),
ty: TypeRef::String,
optional: false,
default: None,
..ParamDef::default()
}],
receiver: Some(crate::core::ir::ReceiverKind::RefMut),
return_type: TypeRef::Unit,
error_type: None,
doc: "Register a request handler.".to_owned(),
variants: vec![],
..Default::default()
};
let run_ep = EntrypointDef {
method: "run".to_owned(),
kind: EntrypointKind::Run,
is_async: true,
params: vec![ParamDef {
name: "addr".to_owned(),
ty: TypeRef::String,
optional: false,
default: None,
..ParamDef::default()
}],
return_type: TypeRef::Unit,
error_type: Some("ServiceError".to_owned()),
doc: "Run the service.".to_owned(),
};
let service = ServiceDef {
name: "TestService".to_owned(),
rust_path: "my_crate::TestService".to_owned(),
constructor,
configurators: vec![],
registrations: vec![registration],
entrypoints: vec![run_ep],
doc: "A test service owner.".to_owned(),
cfg: None,
};
let dispatch_method = MethodDef {
name: "handle".to_owned(),
params: vec![ParamDef {
name: "request".to_owned(),
ty: TypeRef::Named("RequestData".to_owned()),
optional: false,
default: None,
..ParamDef::default()
}],
return_type: TypeRef::Named("ResponseData".to_owned()),
is_async: true,
is_static: false,
error_type: Some("HandlerError".to_owned()),
doc: "Dispatch a request.".to_owned(),
receiver: Some(crate::core::ir::ReceiverKind::Ref),
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
};
let contract = HandlerContractDef {
trait_name: "RequestHandler".to_owned(),
rust_path: "my_crate::RequestHandler".to_owned(),
dispatch: dispatch_method,
optional_methods: vec![],
wire_request_type: Some("RequestData".to_owned()),
wire_response_type: Some("ResponseData".to_owned()),
dispatch_extra_params: vec![],
wire_param_name: None,
dispatch_return_type: None,
response_adapter: None,
doc: "Async trait for handling requests.".to_owned(),
};
ApiSurface {
crate_name: "my_crate".to_owned(),
version: "0.1.0".to_owned(),
services: vec![service],
handler_contracts: vec![contract],
..ApiSurface::default()
}
}
fn make_fixture_surface_with_variants() -> ApiSurface {
let mut surface = make_fixture_surface();
if let Some(service) = surface.services.first_mut() {
if let Some(reg) = service.registrations.first_mut() {
reg.variants.push(crate::core::ir::RegistrationVariant {
name: "get".to_owned(),
overrides: vec![crate::core::ir::RegistrationVariantOverride {
param_name: "path".to_owned(),
value_expr: "\"/api\"".to_owned(),
}],
wrapper_call: None,
signature_params: vec![],
doc: Some("Register a GET handler.".to_owned()),
style: Default::default(),
..Default::default()
});
}
}
surface
}
#[test]
fn rust_output_contains_handler_bridge_struct() {
let surface = make_fixture_surface();
let config = make_test_config();
let output = gen_service_rs(&surface, &config);
assert!(
output.contains("pub struct JniRequestHandlerBridge"),
"expected `JniRequestHandlerBridge` struct in output:\n{output}"
);
}
#[test]
fn rust_output_contains_handler_bridge_impl() {
let surface = make_fixture_surface();
let config = make_test_config();
let output = gen_service_rs(&surface, &config);
assert!(
output.contains("impl my_crate::RequestHandler for JniRequestHandlerBridge"),
"expected trait impl:\n{output}"
);
assert!(
output.contains("fn handle(") && output.contains("Pin<Box<dyn std::future::Future<Output"),
"expected boxed-future dispatch method:\n{output}"
);
}
#[test]
fn rust_output_contains_jni_thread_attach() {
let surface = make_fixture_surface();
let config = make_test_config();
let output = gen_service_rs(&surface, &config);
assert!(
output.contains("attach_current_thread"),
"expected JVM thread attachment:\n{output}"
);
}
#[test]
fn rust_output_contains_json_serialization() {
let surface = make_fixture_surface();
let config = make_test_config();
let output = gen_service_rs(&surface, &config);
assert!(
output.contains("serde_json::to_string(&request)"),
"expected request JSON serialization:\n{output}"
);
assert!(
output.contains("serde_json::from_str(&result_json)"),
"expected response JSON deserialization:\n{output}"
);
}
#[test]
fn rust_output_contains_jni_method_call() {
let surface = make_fixture_surface();
let config = make_test_config();
let output = gen_service_rs(&surface, &config);
assert!(
output.contains("call_method_unchecked"),
"expected JNI method call:\n{output}"
);
}
#[test]
fn rust_output_register_calls_owner_method() {
let surface = make_fixture_surface();
let config = make_test_config();
let output = gen_service_rs(&surface, &config);
assert!(
output.contains("#[no_mangle]"),
"expected #[no_mangle] attribute:\n{output}"
);
assert!(
output.contains("extern \"system\""),
"expected extern system ABI:\n{output}"
);
assert!(
output.contains("nativeTestServiceRegisterAddHandler"),
"expected register function for TestService.add_handler:\n{output}"
);
assert!(
output.contains(".inner.add_handler("),
"register function must call owner.add_handler():\n{output}"
);
assert!(
output.contains("JniRequestHandlerBridge"),
"register function must create the bridge:\n{output}"
);
assert!(
output.contains("new_global_ref"),
"register function must create global reference to handler:\n{output}"
);
assert!(
output.contains("get_method_id"),
"register function must cache method ID:\n{output}"
);
}
#[test]
fn rust_output_run_calls_owner_entrypoint() {
let surface = make_fixture_surface();
let config = make_test_config();
let output = gen_service_rs(&surface, &config);
assert!(
output.contains("nativeTestServiceRun"),
"expected run entrypoint function:\n{output}"
);
assert!(
output.contains("tokio::runtime::Runtime::new"),
"run function must create tokio runtime:\n{output}"
);
assert!(
output.contains("owner_ref.run("),
"run function must call owner.run():\n{output}"
);
assert!(
output.contains("block_on"),
"run function must block_on the async entrypoint:\n{output}"
);
}
#[test]
fn rust_output_contains_service_opaque_and_constructor() {
let surface = make_fixture_surface();
let config = make_test_config();
let output = gen_service_rs(&surface, &config);
assert!(
output.contains("pub struct TestServiceOpaque"),
"expected TestServiceOpaque struct:\n{output}"
);
assert!(
output.contains("nativeTestServiceNew"),
"expected nativeTestServiceNew entry point:\n{output}"
);
assert!(
output.contains("my_crate::TestService::new()"),
"constructor must call the Rust service constructor:\n{output}"
);
assert!(
output.contains("Box::into_raw"),
"constructor must return raw pointer as jlong:\n{output}"
);
}
#[test]
fn rust_output_contains_service_destructor() {
let surface = make_fixture_surface();
let config = make_test_config();
let output = gen_service_rs(&surface, &config);
assert!(
output.contains("nativeTestServiceFree"),
"expected nativeTestServiceFree entry point:\n{output}"
);
assert!(
output.contains("Box::from_raw"),
"destructor must reconstruct from raw pointer:\n{output}"
);
assert!(
output.contains("if handle != 0"),
"destructor must check for null pointer:\n{output}"
);
}
#[test]
fn rust_output_contains_safety_comments() {
let surface = make_fixture_surface();
let config = make_test_config();
let output = gen_service_rs(&surface, &config);
assert!(
output.contains("// SAFETY:"),
"expected SAFETY comments on unsafe:\n{output}"
);
}
#[test]
fn generate_returns_one_file_for_non_empty_services() {
let surface = make_fixture_surface();
let config = make_test_config();
let files = generate(&surface, &config).expect("generate should not fail");
assert_eq!(files.len(), 1, "expected 1 generated file, got {}", files.len());
let path = files[0].path.file_name().unwrap().to_str().unwrap();
assert_eq!(path, "service.rs", "expected service.rs, got {path}");
}
#[test]
fn generate_returns_empty_for_no_services() {
let surface = ApiSurface::default();
let config = make_test_config();
let files = generate(&surface, &config).expect("generate should not fail");
assert!(files.is_empty(), "expected no files for surface without services");
}
#[test]
fn rust_output_contains_registration_variants() {
let surface = make_fixture_surface_with_variants();
let config = make_test_config();
let output = gen_service_rs(&surface, &config);
assert!(
output.contains("nativeTestServiceRegisterAddHandlerGet"),
"expected variant registration function nativeTestServiceRegisterAddHandlerGet:\n{output}"
);
assert!(
output.contains("inner.add_handler("),
"variant function must call the base registration method:\n{output}"
);
}
fn make_test_config() -> ResolvedCrateConfig {
use crate::core::config::resolved::ResolvedCrateConfig;
ResolvedCrateConfig {
name: "my-crate".to_owned(),
..ResolvedCrateConfig::default()
}
}
}