use crate::codegen::naming::to_node_name;
use crate::core::config::NodeCapsuleTypeConfig;
use crate::core::ir::{FunctionDef, TypeRef};
use std::collections::HashMap;
pub(super) fn function_involves_capsule(
func: &FunctionDef,
capsule_types: &HashMap<String, NodeCapsuleTypeConfig>,
) -> bool {
return_type_name(func, capsule_types).is_some()
}
pub(super) fn return_type_name<'a>(
func: &'a FunctionDef,
capsule_types: &'a HashMap<String, NodeCapsuleTypeConfig>,
) -> Option<&'a str> {
fn named_from_ref(ty: &TypeRef) -> Option<&str> {
match ty {
TypeRef::Named(n) => Some(n.as_str()),
TypeRef::Optional(inner) => named_from_ref(inner),
_ => None,
}
}
let name = named_from_ref(&func.return_type)?;
if capsule_types.contains_key(name) {
Some(name)
} else {
None
}
}
pub(super) fn gen_capsule_function(
func: &FunctionDef,
capsule_types: &HashMap<String, NodeCapsuleTypeConfig>,
core_import: &str,
) -> String {
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 mut sig_params: Vec<String> = vec!["env: &napi::Env".to_string()];
for param in &func.params {
let ts = match ¶m.ty {
TypeRef::String | TypeRef::Char => "String".to_string(),
TypeRef::Optional(inner) => match inner.as_ref() {
TypeRef::String | TypeRef::Char => "Option<String>".to_string(),
TypeRef::Primitive(p) => format!("Option<{}>", prim_rust_str(p)),
_ => "Option<String>".to_string(),
},
TypeRef::Primitive(p) => prim_rust_str(p).to_string(),
_ => "String".to_string(),
};
sig_params.push(format!("{}: {ts}", param.name));
}
let call_args: Vec<String> = func
.params
.iter()
.map(|p| {
if p.is_ref && matches!(p.ty, TypeRef::String | TypeRef::Char) {
format!("&{}", p.name)
} else {
p.name.clone()
}
})
.collect();
let core_fn_path = {
let path = func.rust_path.replace('-', "_");
if path.starts_with(core_import) {
path
} else {
format!("{core_import}::{}", func.name)
}
};
let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?";
let capsule_name = return_type_name(func, capsule_types).unwrap_or("");
let cfg = capsule_types.get(capsule_name);
let property_name = cfg
.map(|c| c.property_name.clone())
.unwrap_or_else(|| "__parser".to_string());
let type_tag_const = cfg
.and_then(|c| c.type_tag.as_ref())
.map(|_| format!("__ALEF_CAPSULE_TAG_{}", capsule_name.to_ascii_uppercase()));
let tag_block = if let Some(const_name) = &type_tag_const {
format!(
r#" let status = unsafe {{
napi_type_tag_object(env_raw, external_value, &{const_name})
}};
if status != napi::sys::Status::napi_ok {{
return Err(napi::Error::new(
napi::Status::GenericFailure,
format!("napi_type_tag_object failed: status={{status}}"),
));
}}
"#
)
} else {
String::new()
};
let body = format!(
r#" let value = {core_fn_path}({args}){err_conv};
// SAFETY: `into_raw()` transfers ownership of the raw pointer. The downstream JS
// runtime keeps the External alive as long as the returned object is reachable;
// dropping the pointer prematurely would be a use-after-free.
let ptr = value.into_raw() as *mut std::ffi::c_void;
let mut obj = napi::bindgen_prelude::Object::new(env)?;
let env_raw = env.raw();
let mut external_value: napi::sys::napi_value = std::ptr::null_mut();
let create_status = unsafe {{
napi_create_external(env_raw, ptr, None, std::ptr::null_mut(), &mut external_value)
}};
if create_status != napi::sys::Status::napi_ok {{
return Err(napi::Error::new(
napi::Status::GenericFailure,
format!("napi_create_external failed: status={{create_status}}"),
));
}}
{tag_block} // SAFETY: external_value was just created by napi_create_external on env_raw.
let unknown = unsafe {{
napi::bindgen_prelude::Unknown::from_raw_unchecked(env_raw, external_value)
}};
obj.set_named_property("{property_name}", unknown)?;
Ok(obj)"#,
core_fn_path = core_fn_path,
args = call_args.join(", "),
err_conv = err_conv,
tag_block = tag_block,
property_name = property_name,
);
format!(
"#[napi{js_name_attr}]\npub fn {fn_name}({params}) -> napi::Result<napi::bindgen_prelude::Object<'_>> {{\n{body}\n}}\n\n",
js_name_attr = js_name_attr,
fn_name = func.name,
params = sig_params.join(", "),
body = body,
)
}
pub(super) fn gen_ffi_declarations() -> String {
r#"#[repr(C)]
struct NapiTypeTag {
lower: u64,
upper: u64,
}
#[cfg_attr(target_os = "windows", link(name = "node", kind = "raw-dylib"))]
unsafe extern "C" {
fn napi_create_external(
env: napi::sys::napi_env,
data: *mut std::ffi::c_void,
finalize_cb: Option<
unsafe extern "C" fn(
env: napi::sys::napi_env,
data: *mut std::ffi::c_void,
hint: *mut std::ffi::c_void,
),
>,
finalize_hint: *mut std::ffi::c_void,
result: *mut napi::sys::napi_value,
) -> napi::sys::napi_status;
fn napi_type_tag_object(
env: napi::sys::napi_env,
value: napi::sys::napi_value,
type_tag: *const NapiTypeTag,
) -> napi::sys::napi_status;
}
"#
.to_string()
}
pub(super) fn gen_type_tag_constants(capsule_types: &HashMap<String, NodeCapsuleTypeConfig>) -> String {
let mut entries: Vec<(&String, &NodeCapsuleTypeConfig)> = capsule_types.iter().collect();
entries.sort_by(|a, b| a.0.cmp(b.0));
let mut out = String::new();
for (name, cfg) in entries {
if let Some(tag) = &cfg.type_tag {
let lower = tag.lower.trim_start_matches("0x");
let upper = tag.upper.trim_start_matches("0x");
out.push_str(&crate::backends::napi::template_env::render(
"capsule_type_tag_constant.jinja",
minijinja::context! {
name => name.to_ascii_uppercase(),
lower => lower,
upper => upper,
},
));
}
}
out
}
fn prim_rust_str(p: &crate::core::ir::PrimitiveType) -> &'static str {
use crate::core::ir::PrimitiveType;
match p {
PrimitiveType::Bool => "bool",
PrimitiveType::U8 => "u8",
PrimitiveType::U16 => "u16",
PrimitiveType::U32 => "u32",
PrimitiveType::U64 => "i64", PrimitiveType::I8 => "i8",
PrimitiveType::I16 => "i16",
PrimitiveType::I32 => "i32",
PrimitiveType::I64 => "i64",
PrimitiveType::F32 => "f64", PrimitiveType::F64 => "f64",
PrimitiveType::Usize => "i64",
PrimitiveType::Isize => "i64",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::config::NodeCapsuleTypeConfig;
use crate::core::ir::{FunctionDef, ParamDef, TypeRef};
use std::collections::HashMap;
fn make_capsule_config(type_name: &str, from_module: &str) -> NodeCapsuleTypeConfig {
NodeCapsuleTypeConfig {
type_name: type_name.to_string(),
from_module: from_module.to_string(),
construct: "external_pointer".to_string(),
property_name: "__parser".to_string(),
type_tag: None,
}
}
fn capsule_map(entries: &[(&str, NodeCapsuleTypeConfig)]) -> HashMap<String, NodeCapsuleTypeConfig> {
entries.iter().map(|(k, v)| (k.to_string(), v.clone())).collect()
}
fn make_get_language_fn() -> FunctionDef {
FunctionDef {
name: "get_language".to_string(),
rust_path: "sample_pack::get_language".to_string(),
original_rust_path: String::new(),
params: vec![ParamDef {
name: "name".to_string(),
ty: TypeRef::String,
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: true,
is_mut: false,
newtype_wrapper: None,
original_type: None,
map_is_ahash: false,
map_key_is_cow: false,
vec_inner_is_ref: false,
map_is_btree: false,
core_wrapper: crate::core::ir::CoreWrapper::None,
}],
return_type: TypeRef::Named("Language".to_string()),
is_async: false,
error_type: Some("sample_pack::Error".to_string()),
doc: String::new(),
cfg: None,
sanitized: false,
return_sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
}
}
#[test]
fn function_involves_capsule_detects_capsule_return() {
let func = make_get_language_fn();
let capsules = capsule_map(&[("Language", make_capsule_config("Language", "sample_language"))]);
assert!(function_involves_capsule(&func, &capsules));
}
#[test]
fn function_involves_capsule_returns_false_for_non_capsule() {
let func = FunctionDef {
name: "get_name".to_string(),
rust_path: "sample_pack::get_name".to_string(),
original_rust_path: String::new(),
params: vec![],
return_type: TypeRef::String,
is_async: false,
error_type: None,
doc: String::new(),
cfg: None,
sanitized: false,
return_sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
};
let capsules = capsule_map(&[("Language", make_capsule_config("Language", "sample_language"))]);
assert!(!function_involves_capsule(&func, &capsules));
}
#[test]
fn return_type_name_detects_capsule_return() {
let func = make_get_language_fn();
let capsules = capsule_map(&[("Language", make_capsule_config("Language", "sample_language"))]);
assert_eq!(return_type_name(&func, &capsules), Some("Language"));
}
#[test]
fn gen_capsule_function_emits_external_and_parser_property() {
let func = make_get_language_fn();
let capsules = capsule_map(&[("Language", make_capsule_config("Language", "sample_language"))]);
let out = gen_capsule_function(&func, &capsules, "sample_pack");
assert!(out.contains("#[napi"), "must have #[napi] attr: {out}");
assert!(out.contains("napi::Env"), "must accept env: {out}");
assert!(
out.contains("bindgen_prelude::Object"),
"must return bindgen_prelude::Object: {out}"
);
assert!(out.contains("into_raw"), "must call into_raw(): {out}");
assert!(
out.contains("napi_create_external"),
"must call raw napi_create_external (not bindgen_prelude::External::new): {out}"
);
assert!(
!out.contains("bindgen_prelude::External::new"),
"must NOT use bindgen_prelude::External::new (rejected by node-sample_language): {out}"
);
assert!(
out.contains("__parser"),
"must default property name to __parser: {out}"
);
assert!(
!out.contains("napi_type_tag_object"),
"must NOT emit type-tag call when type_tag is unset: {out}"
);
}
#[test]
fn gen_capsule_function_emits_type_tag_when_configured() {
let func = make_get_language_fn();
let mut cfg = make_capsule_config("Language", "sample_language");
cfg.property_name = "language".to_string();
cfg.type_tag = Some(crate::core::config::NapiTypeTagConfig {
lower: "0x8AF2E5212AD58ABF".to_string(),
upper: "0xD5006CAD83ABBA16".to_string(),
});
let capsules = capsule_map(&[("Language", cfg)]);
let out = gen_capsule_function(&func, &capsules, "sample_pack");
assert!(
out.contains("napi_type_tag_object"),
"must call napi_type_tag_object when type_tag set: {out}"
);
assert!(
out.contains("__ALEF_CAPSULE_TAG_LANGUAGE"),
"must reference the per-capsule tag constant: {out}"
);
assert!(
out.contains(r#"set_named_property("language""#),
"must honour configured property_name: {out}"
);
}
#[test]
fn gen_type_tag_constants_emits_only_tagged_entries() {
let mut tagged = make_capsule_config("Language", "sample_language");
tagged.type_tag = Some(crate::core::config::NapiTypeTagConfig {
lower: "0x8AF2E5212AD58ABF".to_string(),
upper: "0xD5006CAD83ABBA16".to_string(),
});
let untagged = make_capsule_config("Parser", "sample_language");
let capsules = capsule_map(&[("Language", tagged), ("Parser", untagged)]);
let out = gen_type_tag_constants(&capsules);
assert!(
out.contains("__ALEF_CAPSULE_TAG_LANGUAGE"),
"must emit constant for tagged entry: {out}"
);
assert!(out.contains("0x8AF2E5212AD58ABF"), "must inline lower hex: {out}");
assert!(out.contains("0xD5006CAD83ABBA16"), "must inline upper hex: {out}");
assert!(
!out.contains("__ALEF_CAPSULE_TAG_PARSER"),
"must skip untagged entries: {out}"
);
}
#[test]
fn gen_ffi_declarations_exposes_required_n_api_entry_points() {
let out = gen_ffi_declarations();
assert!(out.contains("fn napi_create_external"));
assert!(out.contains("fn napi_type_tag_object"));
assert!(out.contains("struct NapiTypeTag"));
}
#[test]
fn gen_ffi_declarations_emits_windows_raw_dylib_link() {
let out = gen_ffi_declarations();
assert!(
out.contains(r#"#[cfg_attr(target_os = "windows", link(name = "node", kind = "raw-dylib"))]"#),
"must gate the raw napi extern block with a Windows raw-dylib link attribute so MSVC can \
link against napi_create_external + napi_type_tag_object (symbols missing from napi-sys's \
generate! allowlist). Got: {out}"
);
}
}