mod adapter_wrappers;
mod call_args;
mod conversion_bindings;
mod return_wrapping;
pub(super) use adapter_wrappers::{gen_adapter_wrapper, gen_tokio_runtime};
pub(super) use call_args::{
core_prim_str, napi_apply_primitive_casts_to_call_args, napi_gen_call_args, needs_napi_cast,
};
use call_args::{is_bytes_param, needs_vec_f32_conversion};
use conversion_bindings::{gen_napi_buffer_conversion_bindings, gen_vec_f32_conversion_bindings};
pub(super) use return_wrapping::{napi_wrap_return, napi_wrap_return_fn};
use crate::codegen::generators::{self, RustBindingConfig};
use crate::codegen::naming::to_node_name;
use crate::codegen::shared::function_params;
use crate::codegen::type_mapper::TypeMapper;
use crate::core::ir::{FunctionDef, TypeRef};
use ahash::AHashSet;
use crate::backends::napi::type_map::NapiMapper;
#[allow(clippy::too_many_arguments)]
pub(super) fn gen_function(
func: &FunctionDef,
mapper: &NapiMapper,
cfg: &RustBindingConfig,
opaque_types: &AHashSet<String>,
default_types: &AHashSet<String>,
prefix: &str,
capsule_types: &std::collections::HashMap<String, crate::core::config::NodeCapsuleTypeConfig>,
mutex_types: &AHashSet<String>,
) -> String {
let augmented_params: Vec<crate::core::ir::ParamDef> = func
.params
.iter()
.map(|p| {
let mut p2 = p.clone();
if !p2.optional
&& let TypeRef::Named(n) = &p2.ty
&& default_types.contains(n.as_str())
&& !opaque_types.contains(n.as_str())
{
p2.optional = true;
}
p2
})
.collect();
let params = function_params(&augmented_params, &|ty| {
if let TypeRef::Named(n) = ty {
if capsule_types.contains_key(n.as_str())
&& let Some(capsule_cfg) = capsule_types.get(n.as_str())
{
return capsule_cfg.from_module.clone();
}
if opaque_types.contains(n.as_str()) {
return format!("&{prefix}{n}");
}
}
mapper.map_type(ty)
});
let default_coerce_prefix: String = augmented_params
.iter()
.zip(func.params.iter())
.enumerate()
.filter_map(|(idx, (aug, orig))| {
if aug.optional && !orig.optional && !crate::codegen::shared::is_promoted_optional(&func.params, idx) {
let is_named_non_opaque = matches!(&orig.ty,
TypeRef::Named(n) if !opaque_types.contains(n.as_str())
);
if is_named_non_opaque {
return None;
}
let mut_kw = if orig.is_mut { "mut " } else { "" };
Some(format!(
" let {}{} = {}.unwrap_or_default();\n",
mut_kw, orig.name, orig.name
))
} else {
None
}
})
.collect();
let writeback_param = crate::codegen::mut_writeback::writeback_param(&func.params, &func.return_type, opaque_types);
let writeback_var = writeback_param.map(|p| format!("{}_core", p.name));
let return_type = match writeback_param {
Some(p) => mapper.map_type(&p.ty),
None => mapper.map_type(&func.return_type),
};
let return_annotation = mapper.wrap_return(&return_type, func.error_type.is_some());
let js_name = to_node_name(&func.name);
let js_name_attr = if js_name != func.name {
format!("(js_name = \"{}\")", js_name)
} else {
String::new()
};
let core_import = cfg.core_import;
let core_fn_path = {
let path = func.rust_path.replace('-', "_");
if path.starts_with(core_import) {
path
} else {
format!("{core_import}::{}", func.name)
}
};
let use_let_bindings = generators::has_named_params(&func.params, opaque_types)
|| func.params.iter().any(|p| needs_vec_f32_conversion(&p.ty))
|| func.params.iter().any(|p| is_bytes_param(&p.ty));
let call_args = if use_let_bindings {
let base_args = generators::gen_call_args_with_let_bindings_mutex(&func.params, opaque_types, mutex_types);
napi_apply_primitive_casts_to_call_args(&base_args, &func.params)
} else {
napi_gen_call_args(&func.params, opaque_types)
};
let can_delegate_fn = generators::can_auto_delegate_function_with_named_let_bindings(func, opaque_types);
let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
let async_kw = if func.is_async { "async " } else { "" };
let body = if !can_delegate_fn {
if cfg.has_serde && use_let_bindings && func.error_type.is_some() {
let serde_bindings =
generators::gen_serde_let_bindings(&func.params, opaque_types, core_import, err_conv, " ");
let vec_str_bindings: String = func.params.iter().filter(|p| {
p.is_ref && p.vec_inner_is_ref && matches!(&p.ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::String | TypeRef::Char))
}).map(|p| {
format!("let {}_refs: Vec<&str> = {}.iter().map(|s| s.as_str()).collect();\n ", p.name, p.name)
}).collect();
let core_call = format!("{core_fn_path}({call_args})");
let await_kw = if func.is_async { ".await" } else { "" };
if matches!(func.return_type, TypeRef::Unit) {
format!("{vec_str_bindings}{serde_bindings}{core_call}{await_kw}{err_conv}?;\n Ok(())")
} else {
let wrapped = napi_wrap_return_fn(
"val",
&func.return_type,
opaque_types,
func.returns_ref,
prefix,
Some(capsule_types),
mutex_types,
);
if wrapped == "val" {
format!("{vec_str_bindings}{serde_bindings}{core_call}{await_kw}{err_conv}")
} else {
format!("{vec_str_bindings}{serde_bindings}{core_call}{await_kw}.map(|val| {wrapped}){err_conv}")
}
}
} else {
generators::gen_unimplemented_body(
&func.return_type,
&func.name,
func.error_type.is_some(),
cfg,
&func.params,
opaque_types,
)
}
} else if func.is_async {
let mut let_bindings = if use_let_bindings {
generators::gen_named_let_bindings_with_augmented(
&augmented_params,
&func.params,
opaque_types,
core_import,
)
} else {
String::new()
};
let_bindings.push_str(&gen_vec_f32_conversion_bindings(&func.params));
let_bindings.push_str(&gen_napi_buffer_conversion_bindings(&func.params));
let core_call = format!("{core_fn_path}({call_args})");
if let Some(var) = &writeback_var {
if func.error_type.is_some() {
format!("{let_bindings}{core_call}.await.map(|_| {var}.into()){err_conv}")
} else {
format!("{let_bindings}{core_call}.await;\n {var}.into()")
}
} else {
let return_wrap = napi_wrap_return_fn(
"result",
&func.return_type,
opaque_types,
func.returns_ref,
prefix,
Some(capsule_types),
mutex_types,
);
let return_type = mapper.map_type(&func.return_type);
generators::gen_async_body(
&core_call,
cfg,
func.error_type.is_some(),
&return_wrap,
false,
&let_bindings,
matches!(func.return_type, TypeRef::Unit),
Some(&return_type),
)
}
} else {
let core_call = format!("{core_fn_path}({call_args})");
let mut let_bindings = if use_let_bindings {
generators::gen_named_let_bindings_with_augmented(
&augmented_params,
&func.params,
opaque_types,
core_import,
)
} else {
String::new()
};
let_bindings.push_str(&gen_vec_f32_conversion_bindings(&func.params));
let_bindings.push_str(&gen_napi_buffer_conversion_bindings(&func.params));
if let Some(var) = &writeback_var {
if func.error_type.is_some() {
format!("{let_bindings}{core_call}.map(|_| {var}.into()){err_conv}")
} else {
format!("{let_bindings}{core_call};\n {var}.into()")
}
} else if func.error_type.is_some() {
let wrapped = napi_wrap_return_fn(
"val",
&func.return_type,
opaque_types,
func.returns_ref,
prefix,
Some(capsule_types),
mutex_types,
);
if wrapped == "val" {
format!("{let_bindings}{core_call}{err_conv}")
} else {
format!("{let_bindings}{core_call}.map(|val| {wrapped}){err_conv}")
}
} else {
format!(
"{let_bindings}{}",
napi_wrap_return_fn(
&core_call,
&func.return_type,
opaque_types,
func.returns_ref,
prefix,
Some(capsule_types),
mutex_types
)
)
}
};
let mut attrs = String::new();
let sanitized_doc =
crate::codegen::doc_emission::sanitize_rust_idioms(&func.doc, crate::codegen::doc_emission::DocTarget::TsDoc);
crate::codegen::doc_emission::emit_rustdoc(&mut attrs, &sanitized_doc, "");
if func.params.len() > 7 {
attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
}
if func.error_type.is_some() {
attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
}
let body = if default_coerce_prefix.is_empty() {
body
} else {
format!("{}{}", default_coerce_prefix, body)
};
crate::backends::napi::template_env::render(
"function_wrapper.jinja",
minijinja::context! {
attrs => attrs,
js_name_attr => js_name_attr,
async_kw => async_kw,
func_name => &func.name,
params => params,
return_annotation => return_annotation,
body => body,
},
)
}
#[cfg(test)]
mod tests {
use super::gen_tokio_runtime;
#[test]
fn gen_tokio_runtime_contains_runtime() {
let result = gen_tokio_runtime();
assert!(result.contains("TOKIO_RUNTIME") || result.contains("Runtime") || result.contains("tokio"));
assert!(
result.contains("thread_stack_size"),
"worker pool must enlarge the stack:\n{result}"
);
}
fn record_param(is_ref: bool, is_mut: bool) -> crate::core::ir::ParamDef {
crate::core::ir::ParamDef {
name: "record".to_owned(),
ty: crate::core::ir::TypeRef::Named("Record".to_owned()),
is_ref,
is_mut,
..crate::core::ir::ParamDef::default()
}
}
fn gen_probe_function(func: &crate::core::ir::FunctionDef) -> String {
use crate::backends::napi::gen_bindings::NapiBackend;
use crate::backends::napi::type_map::NapiMapper;
let mapper = NapiMapper::new("Js".to_owned());
let cfg = NapiBackend::binding_config("sample_core", "Js", true);
let opaque_types = ahash::AHashSet::new();
let default_types = ahash::AHashSet::new();
let capsule_types = std::collections::HashMap::new();
let mutex_types = ahash::AHashSet::new();
super::gen_function(
func,
&mapper,
&cfg,
&opaque_types,
&default_types,
"Js",
&capsule_types,
&mutex_types,
)
}
#[test]
fn napi_mut_dto_param_returns_the_updated_value() {
use crate::core::ir::{FunctionDef, TypeRef};
let func = FunctionDef {
name: "tag_record".to_owned(),
rust_path: "sample_core::tag_record".to_owned(),
params: vec![record_param(true, true)],
return_type: TypeRef::Unit,
is_async: false,
error_type: None,
..FunctionDef::default()
};
let output = gen_probe_function(&func);
assert!(
output.contains("-> JsRecord"),
"expected the binding to return the mutated DTO type instead of `()`:\n{output}"
);
assert!(
!output.contains("-> ()"),
"must not still advertise a unit return:\n{output}"
);
assert!(
output.contains("sample_core::tag_record(&mut record_core)"),
"expected the core call to still pass `&mut record_core`:\n{output}"
);
assert!(
output.contains("record_core.into()"),
"expected the mutated intermediate to be returned:\n{output}"
);
}
#[test]
fn napi_immutable_dto_param_keeps_unit_return() {
use crate::core::ir::{FunctionDef, TypeRef};
let func = FunctionDef {
name: "read_record".to_owned(),
rust_path: "sample_core::read_record".to_owned(),
params: vec![record_param(true, false)],
return_type: TypeRef::Unit,
is_async: false,
error_type: None,
..FunctionDef::default()
};
let output = gen_probe_function(&func);
assert!(
output.contains("-> ()"),
"immutable borrow must keep unit return:\n{output}"
);
assert!(
!output.contains("record_core.into()"),
"immutable borrow must not gain a write-back tail:\n{output}"
);
}
#[test]
fn napi_owned_dto_param_unaffected_by_writeback() {
use crate::core::ir::{FunctionDef, TypeRef};
let func = FunctionDef {
name: "consume_record".to_owned(),
rust_path: "sample_core::consume_record".to_owned(),
params: vec![record_param(false, false)],
return_type: TypeRef::Unit,
is_async: false,
error_type: None,
..FunctionDef::default()
};
let output = gen_probe_function(&func);
assert!(output.contains("-> ()"), "owned param must keep unit return:\n{output}");
assert!(
!output.contains("record_core.into()"),
"owned param must not gain a write-back tail:\n{output}"
);
}
#[test]
fn napi_async_mut_dto_param_returns_the_updated_value() {
use crate::core::ir::{FunctionDef, TypeRef};
let func = FunctionDef {
name: "tag_record_async".to_owned(),
rust_path: "sample_core::tag_record_async".to_owned(),
params: vec![record_param(true, true)],
return_type: TypeRef::Unit,
is_async: true,
error_type: None,
..FunctionDef::default()
};
let output = gen_probe_function(&func);
assert!(
output.contains("-> JsRecord"),
"expected the binding to return the mutated DTO type instead of `()`:\n{output}"
);
assert!(
!output.contains("-> ()"),
"must not still advertise a unit return:\n{output}"
);
assert!(
output.contains("sample_core::tag_record_async(&mut record_core)"),
"expected the core call to still pass `&mut record_core`:\n{output}"
);
assert!(
output.contains(".await"),
"expected the core call to still be awaited:\n{output}"
);
assert!(
output.contains("record_core.into()"),
"expected the mutated intermediate to be returned:\n{output}"
);
assert!(
!output.contains("Ok(())"),
"must not resolve the future to unit and drop the mutated value:\n{output}"
);
}
#[test]
fn napi_async_mut_dto_param_with_error_returns_the_updated_value() {
use crate::core::ir::{FunctionDef, TypeRef};
let func = FunctionDef {
name: "tag_record_async".to_owned(),
rust_path: "sample_core::tag_record_async".to_owned(),
params: vec![record_param(true, true)],
return_type: TypeRef::Unit,
is_async: true,
error_type: Some("ProbeError".to_owned()),
..FunctionDef::default()
};
let output = gen_probe_function(&func);
assert!(
output.contains("Result<JsRecord>"),
"expected a fallible write-back to return Result<JsRecord>:\n{output}"
);
assert!(
output.contains("sample_core::tag_record_async(&mut record_core)"),
"expected the core call to still pass `&mut record_core`:\n{output}"
);
assert!(
output.contains(".map(|_| record_core.into())"),
"expected the Result<(), E> to be mapped into the mutated intermediate:\n{output}"
);
}
#[test]
fn napi_async_immutable_dto_param_keeps_unit_return() {
use crate::core::ir::{FunctionDef, TypeRef};
let func = FunctionDef {
name: "read_record_async".to_owned(),
rust_path: "sample_core::read_record_async".to_owned(),
params: vec![record_param(true, false)],
return_type: TypeRef::Unit,
is_async: true,
error_type: None,
..FunctionDef::default()
};
let output = gen_probe_function(&func);
assert!(
!output.contains("record_core.into()"),
"immutable borrow must not gain a write-back tail:\n{output}"
);
}
#[test]
fn napi_async_owned_dto_param_unaffected_by_writeback() {
use crate::core::ir::{FunctionDef, TypeRef};
let func = FunctionDef {
name: "consume_record_async".to_owned(),
rust_path: "sample_core::consume_record_async".to_owned(),
params: vec![record_param(false, false)],
return_type: TypeRef::Unit,
is_async: true,
error_type: None,
..FunctionDef::default()
};
let output = gen_probe_function(&func);
assert!(
!output.contains("record_core.into()"),
"owned param must not gain a write-back tail:\n{output}"
);
}
}