use crate::backends::rustler::template_env;
use crate::codegen::shared::binding_fields;
use crate::core::config::ResolvedCrateConfig;
use crate::core::hash::{self, CommentStyle};
use crate::core::ir::TypeRef;
use ahash::AHashSet;
use heck::ToSnakeCase;
use super::json_values::elixir_safe_param_name;
pub(in crate::backends::rustler::gen_bindings) fn gen_native_ex(
api: &crate::core::ir::ApiSurface,
app_name: &str,
app_module: &str,
_crate_name: &str,
config: &ResolvedCrateConfig,
exclude_functions: &AHashSet<String>,
exclude_types: &AHashSet<&str>,
) -> String {
let mut out = String::with_capacity(1024);
let repo_url = config.github_repo();
let build_env_var = format!("{}_BUILD", app_name.to_uppercase());
let default_nif_targets: &[&str] = &[
"aarch64-apple-darwin",
"aarch64-unknown-linux-gnu",
"x86_64-unknown-linux-gnu",
"x86_64-pc-windows-gnu",
];
let nif_targets = match config.elixir.as_ref() {
Some(elixir) if !elixir.nif_targets.is_empty() => elixir.nif_targets.join(" "),
_ => default_nif_targets.join(" "),
};
out.push_str(&hash::header(CommentStyle::Hash));
let nif_targets_list: Vec<&str> = nif_targets.split_whitespace().collect();
let last_idx = nif_targets_list.len().saturating_sub(1);
let nif_targets_block = nif_targets_list
.iter()
.enumerate()
.map(|(idx, target)| {
if idx == last_idx {
format!(" \"{target}\"")
} else {
format!(" \"{target}\",")
}
})
.collect::<Vec<_>>()
.join("\n");
let ctx = minijinja::context! {
app_module => app_module,
app_name => app_name,
repo_url => repo_url,
build_env_var => build_env_var,
nif_targets_block => nif_targets_block,
};
out.push_str(&template_env::render("native_module_header.jinja", ctx));
let mut last_was_multiline = true;
let mut emitted_nif_stubs: AHashSet<String> = AHashSet::new();
for func in api
.functions
.iter()
.filter(|f| !exclude_functions.contains(f.name.as_str()))
{
let fn_name = if func.is_async {
let n = func.name.as_str();
if n.ends_with("_async") {
n.to_string()
} else {
format!("{n}_async")
}
} else {
func.name.clone()
};
let underscored_params: Vec<String> = func
.params
.iter()
.map(|p| format!("_{}", p.name.to_snake_case()))
.collect();
if write_nif_doc(&mut out, &func.doc, last_was_multiline) {
last_was_multiline = true;
}
last_was_multiline = write_nif_stub(&mut out, &fn_name, &underscored_params, last_was_multiline);
emitted_nif_stubs.insert(fn_name.clone());
let has_visitor_bridge = config.trait_bridges.iter().any(|b| {
b.bind_via != crate::core::config::BridgeBinding::OptionsField
&& func.params.iter().any(|p| {
b.param_name.as_deref() == Some(p.name.as_str()) || {
let named = match &p.ty {
TypeRef::Named(n) => Some(n.as_str()),
TypeRef::Optional(inner) => {
if let TypeRef::Named(n) = inner.as_ref() {
Some(n.as_str())
} else {
None
}
}
_ => None,
};
named.map(|n| b.type_alias.as_deref() == Some(n)).unwrap_or(false)
}
})
});
if has_visitor_bridge {
let with_visitor_params: Vec<String> = func
.params
.iter()
.map(|p| format!("_{}", p.name.to_snake_case()))
.collect();
last_was_multiline = write_nif_stub(
&mut out,
&format!("{fn_name}_with_visitor"),
&with_visitor_params,
last_was_multiline,
);
emitted_nif_stubs.insert(format!("{fn_name}_with_visitor"));
}
let has_options_field_bridge = config.trait_bridges.iter().any(|b| {
b.bind_via == crate::core::config::BridgeBinding::OptionsField
&& func.params.iter().any(|p| {
let type_name = match &p.ty {
TypeRef::Named(n) => Some(n.as_str()),
TypeRef::Optional(inner) => {
if let TypeRef::Named(n) = inner.as_ref() {
Some(n.as_str())
} else {
None
}
}
_ => None,
};
type_name.is_some_and(|n| b.options_type.as_deref() == Some(n))
})
});
if has_options_field_bridge {
let mut with_visitor_params: Vec<String> = func
.params
.iter()
.map(|p| format!("_{}", p.name.to_snake_case()))
.collect();
with_visitor_params.push("_visitor".to_string());
last_was_multiline = write_nif_stub(
&mut out,
&format!("{fn_name}_with_visitor"),
&with_visitor_params,
last_was_multiline,
);
emitted_nif_stubs.insert(format!("{fn_name}_with_visitor"));
}
}
if !config.trait_bridges.is_empty() {
last_was_multiline = write_nif_stub(
&mut out,
"visitor_reply",
&["_ref_id".to_string(), "_result".to_string()],
last_was_multiline,
);
last_was_multiline = write_nif_stub(
&mut out,
"complete_trait_call",
&["_reply_id".to_string(), "_result_json".to_string()],
last_was_multiline,
);
last_was_multiline = write_nif_stub(
&mut out,
"fail_trait_call",
&["_reply_id".to_string(), "_error_message".to_string()],
last_was_multiline,
);
}
for bridge in &config.trait_bridges {
if bridge.exclude_languages.contains(&"elixir".to_string()) {
continue;
}
if let Some(register_fn) = &bridge.register_fn {
let params = vec!["_pid".to_string(), "_name".to_string()];
if emitted_nif_stubs.insert(register_fn.clone()) {
last_was_multiline = write_nif_stub(&mut out, register_fn, ¶ms, last_was_multiline);
}
}
if let Some(unregister_fn) = &bridge.unregister_fn {
let params = vec!["_name".to_string()];
if emitted_nif_stubs.insert(unregister_fn.clone()) {
last_was_multiline = write_nif_stub(&mut out, unregister_fn, ¶ms, last_was_multiline);
}
}
if let Some(clear_fn) = &bridge.clear_fn {
let params = vec![];
if emitted_nif_stubs.insert(clear_fn.clone()) {
last_was_multiline = write_nif_stub(&mut out, clear_fn, ¶ms, last_was_multiline);
}
}
}
let streaming_method_keys: AHashSet<String> = config
.adapters
.iter()
.filter(|a| matches!(a.pattern, crate::core::config::AdapterPattern::Streaming))
.filter_map(|a| a.owner_type.as_deref().map(|owner| format!("{owner}.{}", a.name)))
.collect();
for typ in api
.types
.iter()
.filter(|typ| !typ.is_trait && !exclude_types.contains(typ.name.as_str()))
{
for method in typ
.methods
.iter()
.filter(|m| !exclude_functions.contains(m.name.as_str()))
.filter(|m| !streaming_method_keys.contains(&format!("{}.{}", typ.name, m.name)))
{
let nif_fn_name = if method.is_async {
format!("{}_{}_async", typ.name.to_lowercase(), method.name)
} else {
format!("{}_{}", typ.name.to_lowercase(), method.name)
};
let mut underscored_params: Vec<String> = Vec::new();
if method.receiver.is_some() {
underscored_params.push("_obj".to_string());
}
for p in &method.params {
underscored_params.push(format!("_{}", elixir_safe_param_name(&p.name)));
}
if write_nif_doc(&mut out, &method.doc, last_was_multiline) {
last_was_multiline = true;
}
last_was_multiline = write_nif_stub(&mut out, &nif_fn_name, &underscored_params, last_was_multiline);
}
}
for adapter in config
.adapters
.iter()
.filter(|a| matches!(a.pattern, crate::core::config::AdapterPattern::Streaming))
{
let Some(owner) = adapter.owner_type.as_deref() else {
continue;
};
let owner_lc = owner.to_lowercase();
let start_fn = format!("{owner_lc}_{}_start", adapter.name);
let next_fn = format!("{owner_lc}_{}_next", adapter.name);
let mut start_params = vec!["_obj".to_string()];
for p in &adapter.params {
start_params.push(format!("_{}", elixir_safe_param_name(&p.name)));
}
if !out.is_empty() && !out.ends_with("\n\n") {
out.push('\n');
}
out.push_str(" @doc false\n");
let _ = write_nif_stub(&mut out, &start_fn, &start_params, false);
if !out.is_empty() && !out.ends_with("\n\n") {
out.push('\n');
}
out.push_str(" @doc false\n");
let _ = write_nif_stub(&mut out, &next_fn, &["_handle".to_string()], false);
}
let nif_wrapped_types = collect_types_for_nif_derives(api, exclude_types);
for typ in api.types.iter().filter(|t| {
!t.is_trait
&& !t.is_opaque
&& !t.fields.is_empty()
&& t.has_serde
&& !exclude_types.contains(t.name.as_str())
&& nif_wrapped_types.contains(&t.name)
}) {
let from_json_fn_name = format!("{}_from_json", typ.name.to_snake_case());
let params = vec!["_json".to_string()];
if !out.is_empty() && !out.ends_with("\n\n") {
out.push('\n');
}
out.push_str(" @doc false\n");
let _ = write_nif_stub(&mut out, &from_json_fn_name, ¶ms, false);
}
if !api.services.is_empty() {
if !out.is_empty() && !out.ends_with("\n\n") {
out.push('\n');
}
out.push_str(" @doc false\n");
last_was_multiline = write_nif_stub(
&mut out,
"complete_trait_call",
&["_reply_id".to_string(), "_response_json".to_string()],
last_was_multiline,
);
emitted_nif_stubs.insert("complete_trait_call".to_string());
for service in &api.services {
let service_snake = service.name.to_snake_case();
for ep in &service.entrypoints {
let fn_name = format!("{service_snake}_{}", ep.method);
if emitted_nif_stubs.insert(fn_name.clone()) {
let mut params = vec!["_registrations".to_string()];
for p in &ep.params {
params.push(format!("_{}", elixir_safe_param_name(&p.name)));
}
if !out.is_empty() && !out.ends_with("\n\n") {
out.push('\n');
}
out.push_str(" @doc false\n");
last_was_multiline = write_nif_stub(&mut out, &fn_name, ¶ms, last_was_multiline);
}
}
for reg in &service.registrations {
for variant in ®.variants {
let fn_name = format!("{service_snake}_{}", variant.name);
if emitted_nif_stubs.insert(fn_name.clone()) {
let mut params = vec!["_registrations".to_string()];
for p in &variant.signature_params {
params.push(format!("_{}", elixir_safe_param_name(&p.name)));
}
params.push("_handler".to_string());
if !out.is_empty() && !out.ends_with("\n\n") {
out.push('\n');
}
out.push_str(" @doc false\n");
last_was_multiline = write_nif_stub(&mut out, &fn_name, ¶ms, last_was_multiline);
}
}
}
}
}
for error in &api.errors {
for method in error.methods.iter().filter(|m| !m.sanitized) {
let nif_fn_name = format!("{}_{}", error.name.to_lowercase(), method.name);
let params = vec!["_msg".to_string()];
if !out.is_empty() && !out.ends_with("\n\n") {
out.push('\n');
}
out.push_str(" @doc false\n");
let _ = write_nif_stub(&mut out, &nif_fn_name, ¶ms, false);
}
}
out.push_str(&template_env::render(
"native_module_footer.jinja",
minijinja::context! {},
));
out
}
fn write_nif_doc(out: &mut String, doc: &str, _prev_was_multiline: bool) -> bool {
if doc.is_empty() {
return false;
}
if !out.is_empty() && !out.ends_with("\n\n") {
out.push('\n');
}
if !doc.contains('\n') {
let escaped = doc.replace('\\', "\\\\").replace('"', "\\\"");
out.push_str(" @doc \"");
out.push_str(&escaped);
out.push_str("\"\n");
} else {
out.push_str(" @doc \"\"\"\n");
for line in doc.lines() {
let safe = line.replace("\"\"\"", "\"\" \"");
if safe.is_empty() {
out.push('\n');
} else {
out.push_str(" ");
out.push_str(&safe);
out.push('\n');
}
}
out.push_str(" \"\"\"\n");
}
true
}
fn write_nif_stub(out: &mut String, fn_name: &str, params: &[String], prev_was_multiline: bool) -> bool {
let args = params.join(", ");
let sig = if args.is_empty() {
fn_name.to_string()
} else {
format!("{fn_name}({args})")
};
let single_line_len = 6 + sig.len() + 40;
if single_line_len > 120 {
let ctx = minijinja::context! { sig => sig, prev_was_multiline => prev_was_multiline };
out.push_str(&template_env::render("nif_stub_multi_line.jinja", ctx));
true
} else {
let ctx = minijinja::context! { sig => sig };
out.push_str(&template_env::render("nif_stub_single_line.jinja", ctx));
false
}
}
pub(in crate::backends::rustler::gen_bindings) fn collect_types_for_nif_derives(
api: &crate::core::ir::ApiSurface,
exclude_types: &AHashSet<&str>,
) -> AHashSet<String> {
let mut types = AHashSet::new();
for func in &api.functions {
collect_named_types_from_ref(&func.return_type, &mut types);
for param in &func.params {
collect_named_types_from_ref(¶m.ty, &mut types);
}
}
for typ in api.types.iter().filter(|t| !t.is_trait) {
for method in &typ.methods {
collect_named_types_from_ref(&method.return_type, &mut types);
for param in &method.params {
collect_named_types_from_ref(¶m.ty, &mut types);
}
}
}
for enum_def in &api.enums {
for variant in &enum_def.variants {
for field in &variant.fields {
collect_named_types_from_ref(&field.ty, &mut types);
}
}
}
let mut changed = true;
while changed {
changed = false;
let snapshot: Vec<String> = types.iter().cloned().collect();
for type_name in &snapshot {
if let Some(typ) = api.types.iter().find(|t| t.name == *type_name) {
for field in binding_fields(&typ.fields) {
if collect_named_types_from_ref(&field.ty, &mut types) {
changed = true;
}
}
}
}
}
types.retain(|name| {
!exclude_types.contains(name.as_str()) && !api.types.iter().any(|t| t.name == *name && t.is_opaque)
});
types
}
fn collect_named_types_from_ref(ty: &TypeRef, out: &mut AHashSet<String>) -> bool {
match ty {
TypeRef::Named(name) => out.insert(name.clone()),
TypeRef::Optional(inner) => collect_named_types_from_ref(inner, out),
TypeRef::Vec(inner) => collect_named_types_from_ref(inner, out),
TypeRef::Map(k, v) => {
let k_added = collect_named_types_from_ref(k, out);
let v_added = collect_named_types_from_ref(v, out);
k_added || v_added
}
_ => false,
}
}