use crate::codegen::conversions::core_type_path;
use crate::codegen::doc_emission::emit_c_doxygen;
use crate::codegen::naming::{pascal_to_snake, to_class_name};
use crate::core::ir::{CoreWrapper, FunctionDef, MethodDef, ParamDef, ReceiverKind, TypeDef, TypeRef};
use ahash::{AHashMap, AHashSet};
use minijinja::context;
pub(super) fn should_skip_method_wrapper(
method: &MethodDef,
typ: &TypeDef,
path_map: &AHashMap<String, String>,
) -> bool {
for param in &method.params {
if let TypeRef::Named(name) = ¶m.ty {
if !path_map.contains_key(name.as_str()) {
return true;
}
}
}
if method.returns_ref {
if let TypeRef::Named(name) = &method.return_type {
if name == &typ.name {
return true;
}
}
}
if typ.is_opaque && method.is_static {
if let TypeRef::Named(name) = &method.return_type {
if name == &typ.name {
return true;
}
}
}
false
}
fn ffi_doxygen_block(doc: &str) -> String {
let mut full_doc = String::with_capacity(doc.len() + 128);
if !doc.is_empty() {
full_doc.push_str(doc);
if !doc.contains("# Safety") {
full_doc.push_str(
"\n\n# Safety\n\nCaller must ensure all pointer arguments are valid or null. \
Returned pointers must be freed with the appropriate free function.",
);
}
} else {
full_doc.push_str(
"# Safety\n\nCaller must ensure all pointer arguments are valid or null. \
Returned pointers must be freed with the appropriate free function.",
);
}
let mut out = String::new();
emit_c_doxygen(&mut out, &full_doc, "");
out
}
fn sanitized_recoverable(func: &FunctionDef) -> bool {
let params_ok = func.params.iter().all(|p| {
if !p.sanitized {
return true;
}
p.original_type.is_some() && matches!(&p.ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::String))
});
if !params_ok {
return false;
}
let any_param_sanitized = func.params.iter().any(|p| p.sanitized);
!func.sanitized || any_param_sanitized
}
fn method_sanitized_recoverable(method: &MethodDef) -> bool {
let params_ok = method.params.iter().all(|p| {
if !p.sanitized {
return true;
}
p.original_type.is_some() && matches!(&p.ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::String))
});
if !params_ok {
return false;
}
let any_param_sanitized = method.params.iter().any(|p| p.sanitized);
!method.sanitized || any_param_sanitized
}
use crate::backends::ffi::type_map::{c_return_type_with_paths, is_passthrough_return, is_void_return};
use super::helpers::{gen_ffi_unimplemented_body, gen_owned_value_to_c, null_return_value};
fn return_type_needs_non_serde_named(ty: &TypeRef, serde_names: &AHashSet<String>) -> bool {
match ty {
TypeRef::Vec(inner) => {
if let TypeRef::Named(n) = inner.as_ref() {
return !serde_names.contains(n.as_str());
}
false
}
TypeRef::Map(k, v) => {
let k_bad = matches!(k.as_ref(), TypeRef::Named(n) if !serde_names.contains(n.as_str()));
let v_bad = matches!(v.as_ref(), TypeRef::Named(n) if !serde_names.contains(n.as_str()));
k_bad || v_bad
}
TypeRef::Optional(inner) => return_type_needs_non_serde_named(inner, serde_names),
_ => false,
}
}
fn c_symbol_component(name: &str) -> String {
pascal_to_snake(name)
}
fn internal_class_component(name: &str) -> String {
to_class_name(name)
}
pub(super) fn returns_c_char(ty: &TypeRef) -> bool {
match ty {
TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => true,
TypeRef::Vec(_) | TypeRef::Map(_, _) => true,
TypeRef::Optional(inner) => matches!(
inner.as_ref(),
TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json | TypeRef::Vec(_) | TypeRef::Map(_, _)
),
_ => false,
}
}
fn gen_owned_c_char_to_c_with_len(expr: &str, ty: &TypeRef, indent: &str) -> String {
match ty {
TypeRef::String | TypeRef::Char => format!(
"{indent}{{\n\
{indent} let __alef_return = {expr}.to_string();\n\
{indent} match CString::new(__alef_return) {{\n\
{indent} Ok(cs) => {{\n\
{indent} set_last_return_len(cs.as_bytes().len());\n\
{indent} cs.into_raw()\n\
{indent} }}\n\
{indent} Err(_) => {{\n\
{indent} set_last_return_len(0);\n\
{indent} std::ptr::null_mut()\n\
{indent} }}\n\
{indent} }}\n\
{indent}}}"
),
TypeRef::Path => format!(
"{indent}{{\n\
{indent} let __alef_return = {expr}.to_string_lossy().to_string();\n\
{indent} match CString::new(__alef_return) {{\n\
{indent} Ok(cs) => {{\n\
{indent} set_last_return_len(cs.as_bytes().len());\n\
{indent} cs.into_raw()\n\
{indent} }}\n\
{indent} Err(_) => {{\n\
{indent} set_last_return_len(0);\n\
{indent} std::ptr::null_mut()\n\
{indent} }}\n\
{indent} }}\n\
{indent}}}"
),
TypeRef::Json | TypeRef::Vec(_) | TypeRef::Map(_, _) => format!(
"{indent}{{\n\
{indent} match serde_json::to_string(&{expr}) {{\n\
{indent} Ok(__alef_return) => match CString::new(__alef_return) {{\n\
{indent} Ok(cs) => {{\n\
{indent} set_last_return_len(cs.as_bytes().len());\n\
{indent} cs.into_raw()\n\
{indent} }}\n\
{indent} Err(_) => {{\n\
{indent} set_last_return_len(0);\n\
{indent} std::ptr::null_mut()\n\
{indent} }}\n\
{indent} }},\n\
{indent} Err(_) => {{\n\
{indent} set_last_return_len(0);\n\
{indent} std::ptr::null_mut()\n\
{indent} }}\n\
{indent} }}\n\
{indent}}}"
),
TypeRef::Optional(inner) => {
let inner_conversion = gen_owned_c_char_to_c_with_len("val", inner, &format!("{indent} "));
format!(
"{indent}match {expr} {{\n\
{indent} Some(val) => {{\n\
{inner_conversion}\n\
{indent} }}\n\
{indent} None => {{\n\
{indent} set_last_return_len(0);\n\
{indent} std::ptr::null_mut()\n\
{indent} }}\n\
{indent}}}"
)
}
_ => gen_owned_value_to_c(expr, ty, indent, &AHashSet::new()),
}
}
pub(super) fn gen_free_function_len_companion(
func: &FunctionDef,
prefix: &str,
_core_import: &str,
path_map: &AHashMap<String, String>,
enum_names: &AHashSet<String>,
) -> String {
let fn_name_snake = c_symbol_component(&func.name);
let ffi_name = format!("{prefix}_{fn_name_snake}_len");
let ffi_param_count = func.params.len() + func.params.iter().filter(|p| matches!(p.ty, TypeRef::Bytes)).count();
let allow_clippy = if ffi_param_count > 7 {
Some("clippy::too_many_arguments".to_string())
} else {
None
};
let will_be_unimplemented = func.sanitized && !sanitized_recoverable(func);
let mut params = Vec::new();
for p in &func.params {
let param_name = format!("_{}", p.name);
params.push(format!(
" {}: {}",
param_name,
crate::backends::ffi::type_map::c_param_type_with_paths_and_enums(
&p.ty,
_core_import,
path_map,
enum_names,
false )
));
if matches!(p.ty, TypeRef::Bytes) {
params.push(format!(" _{}_len: usize", p.name));
}
}
let synthetic_doc = format!(
"Return the byte length of the C string most recently returned by `{prefix}_{fn_name_snake}` \
on this thread. Returns 0 when the primary call returned null or failed before producing a \
string. Enables safe slice construction in Zig and Java FFM Panama without a NUL-scan.\n\n\
# Safety\n\nPointer arguments are ignored and are present only to keep the companion ABI \
aligned with `{prefix}_{fn_name_snake}`.",
);
let doc_comment = ffi_doxygen_block(&synthetic_doc);
let mut out = String::with_capacity(2048);
out.push_str(&doc_comment);
if let Some(ref clippy) = allow_clippy {
out.push_str(&format!("#[allow({clippy})]\n"));
}
out.push_str("#[unsafe(no_mangle)]\n");
out.push_str("pub unsafe extern \"C\" fn ");
out.push_str(&ffi_name);
out.push_str("(\n");
for (i, p) in params.iter().enumerate() {
out.push_str(p);
if i + 1 < params.len() {
out.push(',');
}
out.push('\n');
}
out.push_str(") -> usize {\n");
if will_be_unimplemented {
out.push_str(" 0\n}");
return out;
}
out.push_str(" last_return_len()\n");
out.push_str("\n}");
out
}
pub(super) fn gen_streaming_method_wrapper(
typ: &TypeDef,
method: &MethodDef,
prefix: &str,
core_import: &str,
body: &str,
) -> String {
let type_snake = c_symbol_component(&typ.name);
let method_name = &method.name;
let fn_name = format!("{prefix}_{type_snake}_{method_name}");
let qualified = core_type_path(typ, core_import);
let callback_type = format!("{}StreamCallback", internal_class_component(prefix));
let doc_comment = ffi_doxygen_block(&method.doc);
let body_indented = format!(" {}", body.replace('\n', "\n "));
crate::backends::ffi::template_env::render(
"streaming_method_wrapper.jinja",
minijinja::context! {
doc_comment => doc_comment.trim_end(),
fn_name => fn_name,
qualified => qualified,
callback_type => callback_type,
body_indented => body_indented,
},
)
}
pub(super) fn gen_method_wrapper(
typ: &TypeDef,
method: &MethodDef,
prefix: &str,
core_import: &str,
path_map: &AHashMap<String, String>,
enum_names: &AHashSet<String>,
serde_names: &AHashSet<String>,
) -> String {
let type_snake = c_symbol_component(&typ.name);
let type_name = &typ.name;
let method_name = &method.name;
let fn_name = format!("{prefix}_{type_snake}_{method_name}");
let doc_comment = ffi_doxygen_block(&method.doc);
let has_error = method.error_type.is_some();
let is_bytes_result = has_error && matches!(method.return_type, TypeRef::Bytes);
let ffi_param_count = (if method.is_static { 0 } else { 1 })
+ method.params.len()
+ method.params.iter().filter(|p| matches!(p.ty, TypeRef::Bytes)).count()
+ if is_bytes_result { 3 } else { 0 };
let allow_clippy = if ffi_param_count > 7 {
Some("clippy::too_many_arguments".to_string())
} else {
None
};
let qualified = core_type_path(typ, core_import);
let mut ret_type = if is_bytes_result {
"i32".to_string()
} else if has_error && is_void_return(&method.return_type) {
"i32".to_string() } else if has_error {
match &method.return_type {
TypeRef::Primitive(_) => c_return_type_with_paths(&method.return_type, core_import, path_map).into_owned(),
_ => c_return_type_with_paths(&method.return_type, core_import, path_map).into_owned(),
}
} else {
c_return_type_with_paths(&method.return_type, core_import, path_map).into_owned()
};
if ret_type.contains("Self") {
ret_type = ret_type.replace("Self", &qualified);
}
if typ.has_lifetime_params {
if let TypeRef::Named(n) = &method.return_type {
if n == type_name {
let bare = format!("*mut {qualified}");
if ret_type == bare {
ret_type = format!("*mut {qualified}<'static>");
}
}
}
}
let return_needs_non_serde_named_method = return_type_needs_non_serde_named(&method.return_type, serde_names);
let will_be_unimplemented =
(method.sanitized && !method_sanitized_recoverable(method)) || return_needs_non_serde_named_method;
let mut params = Vec::new();
if !method.is_static {
let receiver_ty = match method.receiver.as_ref().unwrap_or(&ReceiverKind::Ref) {
ReceiverKind::Ref => format!("*const {qualified}"),
ReceiverKind::RefMut | ReceiverKind::Owned => format!("*mut {qualified}"),
};
let param_name = if will_be_unimplemented { "_this" } else { "this" };
params.push(format!(" {param_name}: {receiver_ty}"));
}
for p in &method.params {
let param_name = if will_be_unimplemented {
format!("_{}", p.name)
} else {
p.name.clone()
};
params.push(format!(
" {}: {}",
param_name,
crate::backends::ffi::type_map::c_param_type_with_paths_and_enums(
&p.ty,
core_import,
path_map,
enum_names,
p.is_mut,
)
));
if matches!(p.ty, TypeRef::Bytes) {
let len_param_name = if will_be_unimplemented {
format!("_{}_len", p.name)
} else {
format!("{}_len", p.name)
};
params.push(format!(" {}: usize", len_param_name));
}
}
if is_bytes_result {
let pfx = if will_be_unimplemented { "_" } else { "" };
params.push(format!(" {pfx}out_ptr: *mut *mut u8"));
params.push(format!(" {pfx}out_len: *mut usize"));
params.push(format!(" {pfx}out_cap: *mut usize"));
}
let return_type = if is_void_return(&method.return_type) && !has_error {
None
} else {
Some(ret_type.clone())
};
let header = crate::backends::ffi::template_env::render(
"method_wrapper_header.jinja",
context! {
doc_comment => doc_comment.trim_end(),
allow_clippy => allow_clippy,
fn_name => fn_name.clone(),
params => params,
return_type => return_type,
},
);
let mut out = header;
if will_be_unimplemented {
out.push_str(&gen_ffi_unimplemented_body(
if is_bytes_result {
&TypeRef::Unit
} else {
&method.return_type
},
&format!("{type_name}::{method_name}"),
has_error || is_bytes_result,
));
out.push_str("\n}");
return out;
}
if is_bytes_result {
out.push_str(&crate::backends::ffi::template_env::render(
"bytes_result_null_check.jinja",
context! {},
));
}
if !method.is_static {
let fail_ret = if is_bytes_result || (has_error && is_void_return(&method.return_type)) {
"return -1;".to_string()
} else if is_void_return(&method.return_type) {
"return;".to_string()
} else {
format!("return {};", null_return_value(&method.return_type))
};
let null_check = match method.receiver.as_ref().unwrap_or(&ReceiverKind::Ref) {
ReceiverKind::Ref => crate::backends::ffi::template_env::render(
"null_check_self_ref.jinja",
context! { fail_ret => fail_ret },
),
ReceiverKind::RefMut => crate::backends::ffi::template_env::render(
"null_check_self_mut.jinja",
context! { fail_ret => fail_ret },
),
ReceiverKind::Owned => crate::backends::ffi::template_env::render(
"null_check_self_owned.jinja",
context! { fail_ret => fail_ret },
),
};
out.push_str(&crate::backends::ffi::template_env::render(
"code_line.jinja",
context! { content => null_check },
));
}
for p in &method.params {
out.push_str(&crate::backends::ffi::template_env::render(
"emitted_code_block.jinja",
context! {
content => gen_param_conversion_with_enums(p, has_error, is_bytes_result, &method.return_type, core_import, enum_names),
},
));
}
for p in &method.params {
if matches!(p.ty, TypeRef::Map(_, _)) && !p.optional && p.is_ref && p.map_is_btree {
let rs = format!("{}_rs", p.name);
let btree = format!("{}_btree", p.name);
out.push_str(&format!(
" let {btree} = {rs}.into_iter().collect::<std::collections::BTreeMap<_, _>>();\n"
));
}
}
let is_owned_receiver = method.receiver.as_ref() == Some(&ReceiverKind::Owned);
let arg_names: Vec<String> = method
.params
.iter()
.map(|p| {
let rs = format!("{}_rs", p.name);
match &p.ty {
TypeRef::Path if !p.optional => {
if p.is_ref { format!("{rs}.as_path()") } else { rs }
}
TypeRef::Named(_) if !p.optional => {
if p.is_mut || is_owned_receiver || !p.is_ref {
rs
} else {
format!("&{rs}")
}
}
TypeRef::String | TypeRef::Char if !p.optional => {
if p.is_ref {
format!("&{rs}")
} else if p.core_wrapper == CoreWrapper::Cow {
format!("{rs}.into()")
} else {
rs
}
}
TypeRef::Bytes if !p.optional => {
if p.is_ref { format!("&{rs}") } else { rs }
}
TypeRef::String | TypeRef::Char | TypeRef::Bytes if p.optional => {
if p.is_ref {
format!("{rs}.as_deref()")
} else if p.core_wrapper == CoreWrapper::Cow {
format!("{rs}.map(std::borrow::Cow::Owned)")
} else {
rs
}
}
TypeRef::Path if p.optional => {
if p.is_ref {
format!("{rs}.as_ref().map(|s| std::path::Path::new(s.as_str()))")
} else {
rs
}
}
TypeRef::Named(_) if p.optional => {
if p.is_ref { format!("{rs}.as_ref()") } else { rs }
}
TypeRef::Json if !p.optional => {
if p.is_ref { format!("&{rs}") } else { rs }
}
TypeRef::Json if p.optional => {
if p.is_ref { format!("{rs}.as_ref()") } else { rs }
}
TypeRef::Vec(_inner) if !p.optional => {
if p.is_mut {
format!("&mut {rs}")
} else if p.is_ref && p.vec_inner_is_ref {
format!("&{rs}.iter().map(|s| s.as_str()).collect::<Vec<&str>>()")
} else if p.is_ref {
format!("&{rs}")
} else {
rs
}
}
TypeRef::Map(_, _) if !p.optional => {
if p.is_mut {
format!("&mut {rs}")
} else if p.is_ref && p.map_is_btree {
format!("&{}_btree", p.name)
} else if p.is_ref {
format!("&{rs}")
} else if p.map_is_btree {
format!("{rs}.into_iter().collect::<std::collections::BTreeMap<_, _>>()")
} else {
rs
}
}
TypeRef::Vec(_) if p.optional => {
if p.is_mut {
format!("{rs}.as_deref_mut()")
} else if p.is_ref {
format!("{rs}.as_deref()")
} else {
rs
}
}
TypeRef::Map(_, _) if p.optional => {
if p.is_mut {
format!("{rs}.as_deref_mut()")
} else if p.is_ref {
format!("{rs}.as_ref()")
} else {
rs
}
}
_ => rs,
}
})
.collect();
let call_args = arg_names.join(", ");
let can_inline = is_passthrough_return(&method.return_type)
&& !has_error
&& !method.returns_ref
&& !method.returns_cow
&& method.return_newtype_wrapper.is_none();
if method.is_async {
let call = if method.is_static {
format!("get_ffi_runtime().block_on(async {{ {qualified}::{method_name}({call_args}).await }})")
} else {
format!("get_ffi_runtime().block_on(async {{ obj.{method_name}({call_args}).await }})")
};
if can_inline {
out.push_str(&crate::backends::ffi::template_env::render(
"call_inline.jinja",
context! { call => call },
));
} else {
out.push_str(&crate::backends::ffi::template_env::render(
"call_with_result.jinja",
context! { call => call },
));
}
} else if method.is_static {
if can_inline {
out.push_str(&crate::backends::ffi::template_env::render("static_method_call.jinja", context! { qualified => qualified.clone(), method_name => method_name.clone(), call_args => call_args.clone() }));
} else {
out.push_str(&crate::backends::ffi::template_env::render("static_method_call_result.jinja", context! { qualified => qualified.clone(), method_name => method_name.clone(), call_args => call_args.clone() }));
}
} else if method_name == "drop" {
out.push_str(" std::mem::drop(obj);\n");
} else if can_inline {
out.push_str(&crate::backends::ffi::template_env::render(
"instance_method_call.jinja",
context! { method_name => method_name.clone(), call_args => call_args.clone() },
));
} else {
out.push_str(&crate::backends::ffi::template_env::render(
"instance_method_call_result.jinja",
context! { method_name => method_name.clone(), call_args => call_args.clone() },
));
}
if is_bytes_result {
out.push_str(&crate::backends::ffi::template_env::render(
"bytes_result_match.jinja",
context! {},
));
} else {
let result_expr =
if method.return_newtype_wrapper.is_some() && matches!(method.return_type, TypeRef::Primitive(_)) {
"result.0"
} else {
"result"
};
if method.returns_ref && !has_error {
match &method.return_type {
TypeRef::String => {
out.push_str(" let result = result.to_owned();\n");
}
TypeRef::Char => {
out.push_str(" let result = *result;\n");
}
TypeRef::Vec(_) => {
out.push_str(" let result = result.to_vec();\n");
}
TypeRef::Map(_, _) => {
out.push_str(" let result = result.clone();\n");
}
TypeRef::Named(_) => {
out.push_str(" let result = result.clone();\n");
}
TypeRef::Optional(inner) => match inner.as_ref() {
TypeRef::String => {
out.push_str(" let result = result.map(str::to_owned);\n");
}
TypeRef::Named(_) | TypeRef::Char | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
out.push_str(" let result = result.cloned();\n");
}
_ => {}
},
_ => {}
}
}
if method.returns_cow && !has_error {
out.push_str(" let result = result.into_owned();\n");
}
if has_error {
if is_void_return(&method.return_type) {
out.push_str(&crate::backends::ffi::template_env::render(
"error_match_void.jinja",
context! {},
));
} else {
let val_expr =
if method.return_newtype_wrapper.is_some() && matches!(method.return_type, TypeRef::Primitive(_)) {
"val.0"
} else {
"val"
};
let ok_body = gen_owned_value_to_c(val_expr, &method.return_type, " ", enum_names);
out.push_str(&crate::backends::ffi::template_env::render(
"error_match_non_void.jinja",
context! {
ok_body => ok_body,
null_ret => null_return_value(&method.return_type),
},
));
}
} else if is_void_return(&method.return_type) {
} else if can_inline {
} else {
out.push_str(&crate::backends::ffi::template_env::render(
"emitted_code_block.jinja",
context! {
content => gen_owned_value_to_c(result_expr, &method.return_type, " ", enum_names),
},
));
}
}
out.push_str("\n}");
out
}
pub(super) fn gen_free_function(
func: &FunctionDef,
prefix: &str,
core_import: &str,
path_map: &AHashMap<String, String>,
enum_names: &AHashSet<String>,
serde_names: &AHashSet<String>,
) -> String {
let fn_name_snake = c_symbol_component(&func.name);
let ffi_name = format!("{prefix}_{fn_name_snake}");
let core_fn_path = {
let path = func.rust_path.replace('-', "_");
if path.starts_with(core_import) {
path
} else {
format!("{core_import}::{}", func.name)
}
};
let func_name = &func.name;
let doc_comment = ffi_doxygen_block(&func.doc);
let has_error = func.error_type.is_some();
let is_bytes_result = has_error && matches!(func.return_type, TypeRef::Bytes);
let ffi_param_count = func.params.len()
+ func.params.iter().filter(|p| matches!(p.ty, TypeRef::Bytes)).count()
+ if is_bytes_result { 3 } else { 0 };
let allow_clippy = if ffi_param_count > 7 {
Some("clippy::too_many_arguments".to_string())
} else {
None
};
let ret_type = if is_bytes_result {
"i32".to_string()
} else if has_error && is_void_return(&func.return_type) {
"i32".to_string()
} else {
c_return_type_with_paths(&func.return_type, core_import, path_map).into_owned()
};
let return_needs_non_serde_named = return_type_needs_non_serde_named(&func.return_type, serde_names);
let will_be_unimplemented = (func.sanitized && !sanitized_recoverable(func)) || return_needs_non_serde_named;
let mut params = Vec::new();
for p in &func.params {
let param_name = if will_be_unimplemented {
format!("_{}", p.name)
} else {
p.name.clone()
};
params.push(format!(
" {}: {}",
param_name,
crate::backends::ffi::type_map::c_param_type_with_paths_and_enums(
&p.ty,
core_import,
path_map,
enum_names,
p.is_mut,
)
));
if matches!(p.ty, TypeRef::Bytes) {
let len_param_name = if will_be_unimplemented {
format!("_{}_len", p.name)
} else {
format!("{}_len", p.name)
};
params.push(format!(" {}: usize", len_param_name));
}
}
if is_bytes_result {
let pfx = if will_be_unimplemented { "_" } else { "" };
params.push(format!(" {pfx}out_ptr: *mut *mut u8"));
params.push(format!(" {pfx}out_len: *mut usize"));
params.push(format!(" {pfx}out_cap: *mut usize"));
}
let return_type = if is_void_return(&func.return_type) && !has_error {
None
} else {
Some(ret_type.clone())
};
let header = crate::backends::ffi::template_env::render(
"free_function_header.jinja",
context! {
doc_comment => doc_comment.trim_end(),
allow_clippy => allow_clippy,
fn_name => ffi_name.clone(),
params => params,
return_type => return_type,
},
);
let mut out = header;
if will_be_unimplemented {
out.push_str(&gen_ffi_unimplemented_body(
if is_bytes_result {
&TypeRef::Unit
} else {
&func.return_type
},
func_name,
has_error || is_bytes_result,
));
out.push_str("\n}");
return out;
}
if is_bytes_result {
out.push_str(&crate::backends::ffi::template_env::render(
"bytes_result_null_check.jinja",
context! {},
));
}
for p in &func.params {
out.push_str(&crate::backends::ffi::template_env::render(
"emitted_code_block.jinja",
context! {
content => gen_param_conversion_with_enums(p, has_error, is_bytes_result, &func.return_type, core_import, enum_names),
},
));
}
for p in &func.params {
if matches!(p.ty, TypeRef::Map(_, _)) && !p.optional && p.is_ref && p.map_is_btree {
let rs = format!("{}_rs", p.name);
let btree = format!("{}_btree", p.name);
out.push_str(&format!(
" let {btree} = {rs}.into_iter().collect::<std::collections::BTreeMap<_, _>>();\n"
));
}
}
let arg_names: Vec<String> = func
.params
.iter()
.map(|p| {
let rs = format!("{}_rs", p.name);
match &p.ty {
TypeRef::Path if !p.optional => {
if p.is_ref { format!("{rs}.as_path()") } else { rs }
}
TypeRef::String | TypeRef::Char if !p.optional => {
if p.is_ref {
format!("&{rs}")
} else if p.core_wrapper == CoreWrapper::Cow {
format!("{rs}.into()")
} else {
rs
}
}
TypeRef::Bytes if !p.optional => {
if p.is_ref { format!("&{rs}") } else { rs }
}
TypeRef::Named(_) if !p.optional => {
if p.is_mut || !p.is_ref { rs } else { format!("&{rs}") }
}
TypeRef::String | TypeRef::Char | TypeRef::Bytes if p.optional => {
if p.is_ref {
format!("{rs}.as_deref()")
} else if p.core_wrapper == CoreWrapper::Cow {
format!("{rs}.map(std::borrow::Cow::Owned)")
} else {
rs
}
}
TypeRef::Path if p.optional => {
if p.is_ref {
format!("{rs}.as_ref().map(|s| std::path::Path::new(s.as_str()))")
} else {
rs
}
}
TypeRef::Named(_) if p.optional => {
if p.is_ref { format!("{rs}.as_ref()") } else { rs }
}
TypeRef::Json if !p.optional => {
if p.is_ref { format!("&{rs}") } else { rs }
}
TypeRef::Json if p.optional => {
if p.is_ref { format!("{rs}.as_ref()") } else { rs }
}
TypeRef::Vec(_inner) if !p.optional => {
if p.is_mut {
format!("&mut {rs}")
} else if p.is_ref && p.vec_inner_is_ref {
format!("&{rs}.iter().map(|s| s.as_str()).collect::<Vec<&str>>()")
} else if p.is_ref {
format!("&{rs}")
} else {
rs
}
}
TypeRef::Map(_, _) if !p.optional => {
if p.is_mut {
format!("&mut {rs}")
} else if p.is_ref && p.map_is_btree {
format!("&{}_btree", p.name)
} else if p.is_ref {
format!("&{rs}")
} else if p.map_is_btree {
format!("{rs}.into_iter().collect::<std::collections::BTreeMap<_, _>>()")
} else {
rs
}
}
TypeRef::Vec(_) if p.optional => {
if p.is_mut {
format!("{rs}.as_deref_mut()")
} else if p.is_ref {
format!("{rs}.as_deref()")
} else {
rs
}
}
TypeRef::Map(_, _) if p.optional => {
if p.is_mut {
format!("{rs}.as_deref_mut()")
} else if p.is_ref {
format!("{rs}.as_ref()")
} else {
rs
}
}
_ => rs,
}
})
.collect();
let call_args = arg_names.join(", ");
let can_inline_fn = is_passthrough_return(&func.return_type)
&& !has_error
&& !func.returns_ref
&& !func.returns_cow
&& func.return_newtype_wrapper.is_none();
if func.is_async {
let call = format!("get_ffi_runtime().block_on(async {{ {core_fn_path}({call_args}).await }})");
if can_inline_fn {
out.push_str(&crate::backends::ffi::template_env::render(
"call_inline.jinja",
context! { call => call },
));
} else {
out.push_str(&crate::backends::ffi::template_env::render(
"call_with_result.jinja",
context! { call => call },
));
}
} else if can_inline_fn {
out.push_str(&crate::backends::ffi::template_env::render(
"call_inline.jinja",
context! { call => format!("{core_fn_path}({call_args})") },
));
} else {
out.push_str(&crate::backends::ffi::template_env::render(
"call_with_result.jinja",
context! { call => format!("{core_fn_path}({call_args})") },
));
}
if is_bytes_result {
out.push_str(&crate::backends::ffi::template_env::render(
"bytes_result_match.jinja",
context! {},
));
} else {
let result_expr = if func.return_newtype_wrapper.is_some() && matches!(func.return_type, TypeRef::Primitive(_))
{
"result.0"
} else {
"result"
};
if func.returns_ref
&& !has_error
&& matches!(&func.return_type, TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Named(_)))
{
out.push_str(" let result = result.cloned();\n");
}
if func.returns_cow && !has_error {
out.push_str(" let result = result.into_owned();\n");
}
if has_error {
if is_void_return(&func.return_type) {
out.push_str(&crate::backends::ffi::template_env::render(
"error_match_void.jinja",
context! {},
));
} else {
let val_expr =
if func.return_newtype_wrapper.is_some() && matches!(func.return_type, TypeRef::Primitive(_)) {
"val.0"
} else {
"val"
};
let ok_body = if returns_c_char(&func.return_type) {
gen_owned_c_char_to_c_with_len(val_expr, &func.return_type, " ")
} else {
gen_owned_value_to_c(val_expr, &func.return_type, " ", enum_names)
};
out.push_str(&crate::backends::ffi::template_env::render(
"error_match_non_void.jinja",
context! {
ok_body => ok_body,
null_ret => null_return_value(&func.return_type),
},
));
}
} else if is_void_return(&func.return_type) {
} else if can_inline_fn {
} else {
let content = if returns_c_char(&func.return_type) {
gen_owned_c_char_to_c_with_len(result_expr, &func.return_type, " ")
} else {
gen_owned_value_to_c(result_expr, &func.return_type, " ", enum_names)
};
out.push_str(&crate::backends::ffi::template_env::render(
"emitted_code_block.jinja",
context! {
content => content,
},
));
}
}
out.push_str("\n}");
out
}
fn type_ref_to_rust_type(ty: &TypeRef, core_import: &str) -> String {
match ty {
TypeRef::String | TypeRef::Char => "String".to_string(),
TypeRef::Bytes => "Vec<u8>".to_string(),
TypeRef::Primitive(prim) => match prim {
crate::core::ir::PrimitiveType::Bool => "bool".to_string(),
crate::core::ir::PrimitiveType::U8 => "u8".to_string(),
crate::core::ir::PrimitiveType::U16 => "u16".to_string(),
crate::core::ir::PrimitiveType::U32 => "u32".to_string(),
crate::core::ir::PrimitiveType::U64 => "u64".to_string(),
crate::core::ir::PrimitiveType::I8 => "i8".to_string(),
crate::core::ir::PrimitiveType::I16 => "i16".to_string(),
crate::core::ir::PrimitiveType::I32 => "i32".to_string(),
crate::core::ir::PrimitiveType::I64 => "i64".to_string(),
crate::core::ir::PrimitiveType::F32 => "f32".to_string(),
crate::core::ir::PrimitiveType::F64 => "f64".to_string(),
crate::core::ir::PrimitiveType::Usize => "usize".to_string(),
crate::core::ir::PrimitiveType::Isize => "isize".to_string(),
},
TypeRef::Named(name) => format!("{core_import}::{name}"),
TypeRef::Vec(inner) => format!("Vec<{}>", type_ref_to_rust_type(inner, core_import)),
TypeRef::Map(key, val) => format!(
"std::collections::HashMap<{}, {}>",
type_ref_to_rust_type(key, core_import),
type_ref_to_rust_type(val, core_import)
),
TypeRef::Optional(inner) => format!("Option<{}>", type_ref_to_rust_type(inner, core_import)),
TypeRef::Path => "std::path::PathBuf".to_string(),
TypeRef::Json => "serde_json::Value".to_string(),
TypeRef::Duration => "std::time::Duration".to_string(),
TypeRef::Unit => "()".to_string(),
}
}
pub(super) fn gen_param_conversion_with_enums(
param: &ParamDef,
has_error: bool,
is_bytes_result: bool,
return_type: &TypeRef,
core_import: &str,
enum_names: &AHashSet<String>,
) -> String {
let name = ¶m.name;
let rs_name = format!("{name}_rs");
let mut out = String::with_capacity(2048);
let fail_ret = if is_bytes_result || (has_error && is_void_return(return_type)) {
"return -1;"
} else if is_void_return(return_type) {
"return;"
} else {
match null_return_value(return_type) {
"()" => "return;",
v => {
let ret = format!("return {};", v);
Box::leak(ret.into_boxed_str()) as &str
}
}
};
if param.optional {
match ¶m.ty {
TypeRef::String | TypeRef::Char => {
out.push_str(&crate::backends::ffi::template_env::render(
"param_optional_string_conversion.jinja",
context! {
rs_name => rs_name.clone(),
name => name.clone(),
fail_ret => fail_ret.to_string(),
},
));
}
TypeRef::Path => {
out.push(' ');
out.push_str(&crate::backends::ffi::template_env::render(
"param_path_conversion.jinja",
context! {
rs_name => rs_name.clone(),
name => name.clone(),
is_ref => param.is_ref,
fail_ret => fail_ret.to_string(),
},
));
}
TypeRef::Json => {
out.push_str(&crate::backends::ffi::template_env::render(
"param_optional_json_conversion.jinja",
context! {
rs_name => rs_name.clone(),
name => name.clone(),
fail_ret => fail_ret.to_string(),
turbofish => String::new(),
},
));
}
TypeRef::Named(type_name) if enum_names.contains(type_name.as_str()) => {
let enum_snake = c_symbol_component(type_name);
out.push_str(&format!(
" let {rs_name} = match {enum_snake}_from_i32_rs({name}) {{\n \
Some(v) => v,\n \
None => {{\n \
set_last_error(1, \"invalid enum discriminant for {type_name}\");\n \
{fail_ret}\n \
}},\n \
}};\n",
));
}
TypeRef::Named(_type_name) => {
out.push_str(&crate::backends::ffi::template_env::render(
"param_optional_named_conversion.jinja",
context! {
rs_name => rs_name.clone(),
name => name.clone(),
is_ref => param.is_ref,
},
));
}
TypeRef::Primitive(crate::core::ir::PrimitiveType::Bool) => {
out.push(' ');
out.push_str(&crate::backends::ffi::template_env::render(
"param_optional_bool_conversion.jinja",
context! {
rs_name => rs_name.clone(),
name => name.clone(),
},
));
}
TypeRef::Primitive(prim) => {
let max_val = match prim {
crate::core::ir::PrimitiveType::U8 => "u8::MAX",
crate::core::ir::PrimitiveType::U16 => "u16::MAX",
crate::core::ir::PrimitiveType::U32 => "u32::MAX",
crate::core::ir::PrimitiveType::U64 => "u64::MAX",
crate::core::ir::PrimitiveType::I8 => "i8::MAX",
crate::core::ir::PrimitiveType::I16 => "i16::MAX",
crate::core::ir::PrimitiveType::I32 => "i32::MAX",
crate::core::ir::PrimitiveType::I64 => "i64::MAX",
crate::core::ir::PrimitiveType::F32 => "f32::NAN",
crate::core::ir::PrimitiveType::F64 => "f64::NAN",
crate::core::ir::PrimitiveType::Usize => "usize::MAX",
crate::core::ir::PrimitiveType::Isize => "isize::MAX",
crate::core::ir::PrimitiveType::Bool => unreachable!("handled above"),
};
let is_float = matches!(
prim,
crate::core::ir::PrimitiveType::F32 | crate::core::ir::PrimitiveType::F64
);
out.push(' ');
out.push_str(&crate::backends::ffi::template_env::render(
"param_optional_numeric_conversion.jinja",
context! {
rs_name => rs_name.clone(),
name => name.clone(),
max_val => max_val,
is_float => is_float,
},
));
}
TypeRef::Vec(_) | TypeRef::Map(_, _) => {
let type_hint = match ¶m.ty {
TypeRef::Vec(_) => {
format!("::<{}>", type_ref_to_rust_type(¶m.ty, core_import))
}
TypeRef::Map(_, val_ty) if param.map_is_ahash => {
let val_rust = type_ref_to_rust_type(val_ty, core_import);
let key_rust = if param.map_key_is_cow {
"std::borrow::Cow<'static, str>".to_string()
} else {
"String".to_string()
};
format!("::<ahash::AHashMap<{key_rust}, {val_rust}>>")
}
TypeRef::Map(_, _) => {
format!("::<{}>", type_ref_to_rust_type(¶m.ty, core_import))
}
_ => String::new(),
};
out.push(' ');
out.push_str(&crate::backends::ffi::template_env::render(
"param_optional_vec_map_conversion.jinja",
context! {
rs_name => rs_name.clone(),
name => name.clone(),
turbofish => type_hint,
fail_ret => fail_ret.to_string(),
},
));
}
_ => {
out.push_str(&crate::backends::ffi::template_env::render(
"param_optional_fallback.jinja",
context! {
rs_name => rs_name.clone(),
name => name.clone(),
fail_ret => fail_ret.to_string(),
},
));
}
}
} else {
match ¶m.ty {
TypeRef::String | TypeRef::Char => {
out.push_str(&crate::backends::ffi::template_env::render(
"param_non_optional_string_conversion.jinja",
context! {
name => name.clone(),
fail_ret => fail_ret.to_string(),
rs_name => rs_name.clone(),
},
));
}
TypeRef::Path => {
out.push_str(&crate::backends::ffi::template_env::render(
"param_non_optional_path_conversion.jinja",
context! {
rs_name => rs_name.clone(),
name => name.clone(),
fail_ret => fail_ret.to_string(),
},
));
}
TypeRef::Json => {
let turbofish = String::new();
let mut_keyword = String::new();
out.push_str(&crate::backends::ffi::template_env::render(
"param_non_optional_json_conversion.jinja",
context! {
name => name.clone(),
fail_ret => fail_ret.to_string(),
rs_name => rs_name.clone(),
turbofish => turbofish,
mut_keyword => mut_keyword,
},
));
}
TypeRef::Primitive(prim) => match prim {
crate::core::ir::PrimitiveType::Bool => {
out.push_str(&crate::backends::ffi::template_env::render(
"param_primitive_bool.jinja",
context! { rs_name => rs_name.clone(), name => name.clone() },
));
}
_ => {
if let Some(newtype_path) = ¶m.newtype_wrapper {
out.push_str(&crate::backends::ffi::template_env::render("param_primitive_newtype.jinja", context! { rs_name => rs_name.clone(), newtype_path => newtype_path.clone(), name => name.clone() }));
} else {
out.push_str(&crate::backends::ffi::template_env::render(
"param_primitive_passthrough.jinja",
context! { rs_name => rs_name.clone(), name => name.clone() },
));
}
}
},
TypeRef::Named(type_name) if enum_names.contains(type_name.as_str()) => {
let enum_snake = c_symbol_component(type_name);
out.push_str(&format!(
" let {rs_name} = match {enum_snake}_from_i32_rs({name}) {{\n \
Some(v) => v,\n \
None => {{\n \
set_last_error(1, \"invalid enum discriminant for {type_name}\");\n \
{fail_ret}\n \
}},\n \
}};\n",
));
}
TypeRef::Named(_type_name) => {
out.push_str(&crate::backends::ffi::template_env::render(
"param_non_optional_named_conversion.jinja",
context! {
rs_name => rs_name.clone(),
name => name.clone(),
fail_ret => fail_ret.to_string(),
is_ref => param.is_ref,
is_mut => param.is_mut,
},
));
}
TypeRef::Bytes => {
out.push_str(&crate::backends::ffi::template_env::render(
"param_non_optional_bytes_conversion.jinja",
context! {
rs_name => rs_name.clone(),
name => name.clone(),
fail_ret => fail_ret.to_string(),
},
));
}
TypeRef::Vec(_) | TypeRef::Map(_, _) => {
let mut_keyword = if param.is_mut { "mut " } else { "" };
let type_hint = match ¶m.ty {
TypeRef::Vec(_) => {
format!("::<{}>", type_ref_to_rust_type(¶m.ty, core_import))
}
TypeRef::Map(_, val_ty) if param.map_is_ahash => {
let val_rust = type_ref_to_rust_type(val_ty, core_import);
let key_rust = if param.map_key_is_cow {
"std::borrow::Cow<'static, str>".to_string()
} else {
"String".to_string()
};
format!("::<ahash::AHashMap<{key_rust}, {val_rust}>>")
}
TypeRef::Map(_, _) => {
format!("::<{}>", type_ref_to_rust_type(¶m.ty, core_import))
}
_ => String::new(),
};
out.push_str(&crate::backends::ffi::template_env::render(
"param_non_optional_json_conversion.jinja",
context! {
name => name.clone(),
fail_ret => fail_ret.to_string(),
rs_name => rs_name.clone(),
turbofish => type_hint,
mut_keyword => mut_keyword,
},
));
}
TypeRef::Optional(_) => {
out.push_str(&crate::backends::ffi::template_env::render(
"param_optional_passthrough.jinja",
context! { rs_name => rs_name.clone(), name => name.clone() },
));
}
TypeRef::Duration => {
out.push_str(&crate::backends::ffi::template_env::render(
"param_duration_conversion.jinja",
context! { rs_name => rs_name.clone(), name => name.clone() },
));
}
TypeRef::Unit => {
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn return_type_needs_non_serde_named_vec_non_serde() {
let mut serde_names: AHashSet<String> = AHashSet::new();
serde_names.insert("ExtractionResult".to_string());
let vec_non_serde = TypeRef::Vec(Box::new(TypeRef::Named("PatternMatch".to_string())));
assert!(
return_type_needs_non_serde_named(&vec_non_serde, &serde_names),
"Vec<PatternMatch> without Serialize must be detected as needing stub"
);
}
#[test]
fn return_type_needs_non_serde_named_vec_serde_ok() {
let mut serde_names: AHashSet<String> = AHashSet::new();
serde_names.insert("ExtractionResult".to_string());
let vec_serde = TypeRef::Vec(Box::new(TypeRef::Named("ExtractionResult".to_string())));
assert!(
!return_type_needs_non_serde_named(&vec_serde, &serde_names),
"Vec<ExtractionResult> with Serialize must NOT be detected as needing stub"
);
}
#[test]
fn return_type_needs_non_serde_named_primitive_vec_not_affected() {
let serde_names: AHashSet<String> = AHashSet::new();
assert!(!return_type_needs_non_serde_named(
&TypeRef::Vec(Box::new(TypeRef::String)),
&serde_names
));
assert!(!return_type_needs_non_serde_named(
&TypeRef::Vec(Box::new(TypeRef::Primitive(crate::core::ir::PrimitiveType::U64))),
&serde_names
));
}
#[test]
fn named_param_is_mut_call_site_passes_local_directly() {
use crate::core::ir::ParamDef;
let p = ParamDef {
name: "result".to_string(),
ty: TypeRef::Named("ExtractionResult".to_string()),
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: false,
is_mut: true,
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,
};
let rs = format!("{}_rs", p.name);
let result = if p.is_mut {
rs.clone()
} else if p.is_ref {
format!("&{rs}")
} else {
rs.clone()
};
assert_eq!(
result, "result_rs",
"is_mut Named param must pass local directly (already &mut T)"
);
}
#[test]
fn enum_param_local_name_uses_param_name_not_type_name() {
use crate::core::ir::ParamDef;
let mut enum_names: AHashSet<String> = AHashSet::new();
enum_names.insert("RedactionStrategy".to_string());
let p = ParamDef {
name: "strategy".to_string(), ty: TypeRef::Named("RedactionStrategy".to_string()),
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: false,
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,
};
let output = gen_param_conversion_with_enums(&p, false, false, &TypeRef::Unit, "sample_crate", &enum_names);
assert!(
output.contains("let strategy_rs ="),
"enum local must be named after param (strategy_rs), got:\n{output}"
);
assert!(
output.contains("redaction_strategy_from_i32_rs(strategy)"),
"enum helper must receive the FFI param name (strategy), got:\n{output}"
);
}
}