use crate::backends::ffi::type_map::is_void_return;
use crate::codegen::c_consumer;
use crate::codegen::doc_emission::emit_c_doxygen;
use crate::core::ir::TypeRef;
use ahash::AHashSet;
fn render_doxygen_typedef_block(doc: &str) -> String {
if doc.trim().is_empty() {
return String::new();
}
let mut raw = String::new();
emit_c_doxygen(&mut raw, doc, "");
let mut out = String::with_capacity(raw.len() + 16);
out.push_str("/**\n");
for line in raw.lines() {
let body = line.strip_prefix("/// ").unwrap_or(line.trim_start_matches("///"));
if body.is_empty() {
out.push_str(" *\n");
} else {
out.push_str(" * ");
out.push_str(body);
out.push('\n');
}
}
out.push_str(" */\n");
out
}
fn copy_expr(expr: &str) -> String {
if expr.starts_with("obj.") || expr.starts_with("(*") {
expr.to_string()
} else {
format!("*{expr}")
}
}
pub(super) fn gen_value_to_c(
expr: &str,
ty: &TypeRef,
indent: &str,
enum_names: &AHashSet<String>,
clone_names: &AHashSet<String>,
) -> String {
match ty {
TypeRef::Primitive(p) => {
let type_class = if matches!(p, crate::core::ir::PrimitiveType::Bool) {
"primitive_bool"
} else {
"primitive_other"
};
crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
crate::alef_context! {
type_class => type_class,
expr => expr,
indent => indent,
},
)
}
TypeRef::String | TypeRef::Char => crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
crate::alef_context! {
type_class => "string",
expr => expr,
indent => indent,
},
),
TypeRef::Path => crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
crate::alef_context! {
type_class => "path",
expr => expr,
indent => indent,
},
),
TypeRef::Json => crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
crate::alef_context! {
type_class => "json_or_vec_or_map",
expr => expr,
indent => indent,
},
),
TypeRef::Named(name) => {
if enum_names.contains(name.as_str()) {
let copy = copy_expr(expr);
crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
crate::alef_context! {
type_class => "named_enum",
expr => expr,
copy_expr => ©,
indent => indent,
},
)
} else if clone_names.contains(name.as_str()) {
crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
crate::alef_context! {
type_class => "named_clone",
expr => expr,
indent => indent,
},
)
} else {
crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
crate::alef_context! {
type_class => "named_non_clone",
expr => expr,
indent => indent,
},
)
}
}
TypeRef::Vec(_) | TypeRef::Map(_, _) => crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
crate::alef_context! {
type_class => "json_or_vec_or_map",
expr => expr,
indent => indent,
},
),
TypeRef::Bytes => crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
crate::alef_context! {
type_class => "bytes",
expr => expr,
indent => indent,
},
),
TypeRef::Duration => crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
crate::alef_context! {
type_class => "duration",
expr => expr,
indent => indent,
},
),
TypeRef::Unit => String::new(),
TypeRef::Optional(inner) => {
let inner_conversion = gen_value_to_c("val", inner, &format!("{indent} "), enum_names, clone_names);
let null_value = null_return_value(&TypeRef::Optional(inner.clone()));
crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
crate::alef_context! {
type_class => "optional",
expr => expr,
indent => indent,
inner_conversion => &inner_conversion,
null_value => null_value,
},
)
}
}
}
pub(super) fn gen_ffi_unimplemented_body(return_type: &TypeRef, fn_name: &str, has_error: bool) -> String {
let err_msg = format!("Not implemented: {fn_name}");
if has_error && is_void_return(return_type) {
format!(" set_last_error(99, \"{err_msg}\");\n -1")
} else if is_void_return(return_type) {
format!(" set_last_error(99, \"{err_msg}\");")
} else {
let ret = null_return_value(return_type);
format!(" set_last_error(99, \"{err_msg}\");\n {ret}")
}
}
pub(super) fn null_return_value(ty: &TypeRef) -> &'static str {
use crate::core::ir::PrimitiveType;
match ty {
TypeRef::Primitive(p) => match p {
PrimitiveType::F32 | PrimitiveType::F64 => "0.0",
_ => "0",
},
TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "std::ptr::null_mut()",
TypeRef::Bytes => "std::ptr::null_mut()",
TypeRef::Named(_) => "0",
TypeRef::Vec(_) | TypeRef::Map(_, _) => "std::ptr::null_mut()",
TypeRef::Duration => "0",
TypeRef::Optional(inner) => match inner.as_ref() {
TypeRef::Primitive(p) => match p {
PrimitiveType::F32 | PrimitiveType::F64 => "0.0",
_ => "0",
},
TypeRef::Optional(inner2) => match inner2.as_ref() {
TypeRef::Primitive(p) => match p {
PrimitiveType::F32 | PrimitiveType::F64 => "0.0",
_ => "0",
},
TypeRef::Named(_) => "0",
_ => "std::ptr::null_mut()",
},
TypeRef::Duration => "0",
TypeRef::Named(_) => "0",
_ => "std::ptr::null_mut()",
},
TypeRef::Unit => "()",
}
}
pub(super) fn ffi_null_return_value<'a>(ty: &TypeRef, ffi_return_type: Option<&'a str>) -> &'a str {
match ffi_return_type {
Some("AlefHandle") => "0",
Some(return_type) if return_type.starts_with("*const ") => "std::ptr::null()",
Some(return_type) if return_type.starts_with("*mut ") => "std::ptr::null_mut()",
_ => null_return_value(ty),
}
}
pub(super) fn gen_owned_value_to_c(expr: &str, ty: &TypeRef, indent: &str, _enum_names: &AHashSet<String>) -> String {
match ty {
TypeRef::Primitive(prim) => match prim {
crate::core::ir::PrimitiveType::Bool => crate::backends::ffi::template_env::render(
"owned_value_to_c_bool.jinja",
crate::alef_context! {
expr => expr,
indent => indent,
},
),
_ => crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
crate::alef_context! {
type_class => "primitive_other",
expr => expr,
indent => indent,
},
),
},
TypeRef::String | TypeRef::Char => crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
crate::alef_context! {
type_class => "string",
expr => expr,
indent => indent,
},
),
TypeRef::Json => crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
crate::alef_context! {
type_class => "json_or_vec_or_map",
expr => expr,
indent => indent,
},
),
TypeRef::Path => crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
crate::alef_context! {
type_class => "path",
expr => expr,
indent => indent,
},
),
TypeRef::Named(_) => crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
crate::alef_context! {
type_class => "named_owned",
expr => expr,
indent => indent,
},
),
TypeRef::Vec(_) | TypeRef::Map(_, _) => crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
crate::alef_context! {
type_class => "json_or_vec_or_map",
expr => expr,
indent => indent,
},
),
TypeRef::Bytes => crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
crate::alef_context! {
type_class => "bytes",
expr => expr,
indent => indent,
},
),
TypeRef::Optional(inner) => {
if let TypeRef::Primitive(prim) = inner.as_ref()
&& !matches!(prim, crate::core::ir::PrimitiveType::Bool)
{
let null_value = null_return_value(&TypeRef::Optional(inner.clone()));
return format!("{indent}{expr}.unwrap_or({null_value})");
}
let inner_conversion = gen_owned_value_to_c("val", inner, &format!("{indent} "), _enum_names);
let null_value = null_return_value(&TypeRef::Optional(inner.clone()));
crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
crate::alef_context! {
type_class => "optional_owned",
expr => expr,
indent => indent,
inner_conversion => &inner_conversion,
null_value => null_value,
},
)
}
TypeRef::Duration => crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
crate::alef_context! {
type_class => "duration",
expr => expr,
indent => indent,
},
),
TypeRef::Unit => String::new(),
}
}
pub(super) fn cbindgen_exclude_type_names(
api: &crate::core::ir::ApiSurface,
config: &crate::core::config::ResolvedCrateConfig,
) -> std::collections::BTreeSet<String> {
let mut exclude_types: std::collections::BTreeSet<String> = config
.ffi
.as_ref()
.map(|c| {
c.exclude_types
.iter()
.filter_map(|name| bare_rust_type_name(name))
.collect()
})
.unwrap_or_default();
exclude_types.extend(api.types.iter().filter(|t| t.binding_excluded).map(|t| t.name.clone()));
exclude_types.extend(api.enums.iter().filter(|e| e.binding_excluded).map(|e| e.name.clone()));
exclude_types.extend(api.errors.iter().filter(|e| e.binding_excluded).map(|e| e.name.clone()));
let live_binding_type_names: std::collections::BTreeSet<&str> = api
.types
.iter()
.filter(|t| !t.binding_excluded)
.map(|t| t.name.as_str())
.chain(
api.enums
.iter()
.filter(|e| !e.binding_excluded)
.map(|e| e.name.as_str()),
)
.chain(
api.errors
.iter()
.filter(|e| !e.binding_excluded)
.map(|e| e.name.as_str()),
)
.collect();
exclude_types.extend(
api.excluded_type_paths
.keys()
.filter_map(|name| bare_rust_type_name(name))
.filter(|name| !live_binding_type_names.contains(name.as_str())),
);
exclude_types
}
pub(super) fn gen_cbindgen_toml(
prefix: &str,
api: &crate::core::ir::ApiSurface,
capsule_types: &std::collections::HashMap<String, crate::core::config::FfiCapsuleTypeConfig>,
exclude_types: &std::collections::BTreeSet<String>,
) -> String {
let prefix_upper = c_consumer::export_type_prefix(prefix);
let feature_defines = cbindgen_feature_defines(api, &prefix_upper);
let capsule_used_as_opaque: std::collections::HashSet<&str> = api
.types
.iter()
.flat_map(|t| t.methods.iter())
.filter_map(|m| match &m.return_type {
crate::core::ir::TypeRef::Named(name) if capsule_types.contains_key(name) => Some(name.as_str()),
_ => None,
})
.collect();
let mut entries: Vec<(String, String, bool)> = api
.types
.iter()
.filter(|t| !exclude_types.contains(&t.name))
.filter(|t| !capsule_types.contains_key(t.name.as_str()) || capsule_used_as_opaque.contains(t.name.as_str()))
.map(|t| (format!("{prefix_upper}{}", t.name), t.doc.clone(), t.is_opaque))
.collect();
{
let c_names = super::capsule::capsule_forward_declared_c_types(api, capsule_types, &capsule_used_as_opaque);
for c_name in c_names {
if !entries.iter().any(|(name, _, _)| name == c_name) {
entries.push((c_name.to_string(), String::new(), false));
}
}
}
for e in api.enums.iter().filter(|e| !exclude_types.contains(&e.name)) {
let c_name = format!("{prefix_upper}{}", e.name);
if !entries.iter().any(|(name, _, _)| name == &c_name) {
entries.push((c_name, e.doc.clone(), false));
}
}
for err in api.errors.iter().filter(|err| !exclude_types.contains(&err.name)) {
if !err.methods.is_empty() {
let c_name = format!("{prefix_upper}{}", err.name);
if !entries.iter().any(|(name, _, _)| name == &c_name) {
entries.push((c_name, err.doc.clone(), false));
}
}
}
for svc in api.services.iter() {
let c_name = format!("{prefix_upper}{}", svc.name);
if !entries.iter().any(|(name, _, _)| name == &c_name) {
entries.push((c_name, svc.doc.clone(), false));
}
}
entries.sort_by(|a, b| a.0.cmp(&b.0));
let forward_decls: String = entries
.iter()
.map(|(name, doc, is_handle)| {
let doc_block = render_doxygen_typedef_block(doc);
if *is_handle {
format!("{doc_block}typedef uint64_t {name};")
} else if doc_block.is_empty() {
format!("typedef struct {name} {name};")
} else {
format!("{doc_block}typedef struct {name} {name};")
}
})
.collect::<Vec<_>>()
.join("\n");
let after_includes = if forward_decls.is_empty() {
String::new()
} else {
toml_multiline_basic_string(&format!("/* Opaque type forward declarations */\n{forward_decls}\n"))
};
crate::backends::ffi::template_env::render(
"cbindgen_toml.jinja",
crate::alef_context! {
prefix_upper => &prefix_upper,
after_includes => &after_includes,
feature_defines => feature_defines,
export_exclude => exclude_types.iter().cloned().collect::<Vec<_>>(),
},
)
}
pub(super) fn cbindgen_feature_defines(api: &crate::core::ir::ApiSurface, prefix_upper: &str) -> Vec<(String, String)> {
let method_cfgs = api
.types
.iter()
.flat_map(|item| item.methods.iter())
.chain(api.enums.iter().flat_map(|item| item.methods.iter()))
.chain(api.errors.iter().flat_map(|item| item.methods.iter()))
.filter_map(|method| method.cfg.as_deref());
let mut cfgs = api
.types
.iter()
.filter_map(|item| item.cfg.as_deref())
.chain(api.enums.iter().filter_map(|item| item.cfg.as_deref()))
.chain(api.functions.iter().filter_map(|item| item.cfg.as_deref()))
.chain(api.services.iter().filter_map(|item| item.cfg.as_deref()))
.chain(method_cfgs);
let mut features = std::collections::BTreeSet::new();
for cfg in &mut cfgs {
collect_cfg_feature_names(cfg, &mut features);
}
features
.into_iter()
.map(|feature| {
let macro_name = feature
.chars()
.map(|character| {
if character.is_ascii_alphanumeric() {
character.to_ascii_uppercase()
} else {
'_'
}
})
.collect::<String>();
(
format!("feature = {feature}"),
format!("{prefix_upper}_FEATURE_{macro_name}"),
)
})
.collect()
}
fn collect_cfg_feature_names<'a>(cfg: &'a str, features: &mut std::collections::BTreeSet<&'a str>) {
const FEATURE_PREFIX: &str = "feature = \"";
let mut remainder = cfg;
while let Some(start) = remainder.find(FEATURE_PREFIX) {
let value = &remainder[start + FEATURE_PREFIX.len()..];
let Some(end) = value.find('"') else {
return;
};
if end > 0 {
features.insert(&value[..end]);
}
remainder = &value[end + 1..];
}
}
fn bare_rust_type_name(name: &str) -> Option<String> {
let bare = name.rsplit("::").next()?.trim();
if bare.is_empty() { None } else { Some(bare.to_string()) }
}
fn toml_multiline_basic_string(value: &str) -> String {
let escaped = value
.replace('\\', "\\\\")
.replace("\"\"\"", "\\\"\\\"\\\"")
.replace('\u{8}', "\\b")
.replace('\u{c}', "\\f");
format!("\"\"\"\n{escaped}\"\"\"")
}
pub(super) fn gen_build_rs(
header_name: &str,
lib_name: &str,
ffi_crate_root: &str,
go_output_dir: Option<&str>,
prefix: &str,
capsule_types: &std::collections::HashMap<String, crate::core::config::FfiCapsuleTypeConfig>,
) -> anyhow::Result<String> {
let prefix_upper = c_consumer::export_type_prefix(prefix);
let escaped_header_name = super::rust_literal::escape_rust_str_literal(header_name);
let capsule_header_fixup = super::rust_literal::capsule_header_fixup(capsule_types, &prefix_upper);
let go_header_destination = match go_output_dir {
Some(go_dir) => {
let go_dir = go_dir.trim_end_matches('/');
let target = format!("{go_dir}/include/{header_name}");
let relative = crate::core::config::abi_grammar::relative_repo_path(ffi_crate_root, &target)
.map_err(anyhow::Error::msg)?;
let destination = super::rust_literal::escape_rust_str_literal(&relative);
format!(" Path::new(\"{destination}\"),\n")
}
None => String::new(),
};
Ok(crate::backends::ffi::template_env::render(
"build_rs.jinja",
crate::alef_context! {
header_name => &escaped_header_name,
lib_name => lib_name,
prefix_upper => &prefix_upper,
go_header_destination => go_header_destination,
capsule_header_fixup => capsule_header_fixup,
},
))
}
pub(super) fn gen_free_string(prefix: &str) -> String {
crate::backends::ffi::template_env::render(
"free_string.jinja",
crate::alef_context! {
prefix => prefix,
},
)
}
pub(super) fn gen_version(prefix: &str) -> String {
crate::backends::ffi::template_env::render(
"version_fn.jinja",
crate::alef_context! {
prefix => prefix,
},
)
}
pub(super) fn gen_free_bytes(prefix: &str) -> String {
crate::backends::ffi::template_env::render("free_bytes.jinja", crate::alef_context! { prefix => prefix })
}
pub(super) fn gen_ffi_tokio_runtime() -> String {
crate::backends::ffi::template_env::render("ffi_tokio_runtime.jinja", crate::alef_context! {})
}
pub(super) fn gen_ffi_cancel_token(prefix: &str) -> String {
crate::backends::ffi::template_env::render(
"ffi_cancel_token.jinja",
crate::alef_context! {
prefix => prefix,
builtin_prefix => crate::codegen::naming::ffi_builtin_error_code_prefix(prefix),
new_symbol => c_consumer::cancel_token_new_symbol(prefix),
cancel_symbol => c_consumer::cancel_token_cancel_symbol(prefix),
free_symbol => c_consumer::cancel_token_free_symbol(prefix),
},
)
}
pub(super) fn gen_stream_handle_functions(
prefix: &str,
owner_type: &str,
adapter_name: &str,
core_path: &str,
item_type: &str,
request_type: &str,
core_import: &str,
) -> anyhow::Result<String> {
use heck::{ToPascalCase, ToSnakeCase};
let pascal_prefix = prefix.to_pascal_case();
let pascal_owner = owner_type.to_pascal_case();
let pascal_name = adapter_name.to_pascal_case();
let owner_snake = owner_type.to_snake_case();
let handle_name = format!("{pascal_prefix}{pascal_owner}{pascal_name}StreamHandle");
let fn_start = c_consumer::stream_adapter_symbol(prefix, owner_type, adapter_name, "start");
let fn_next = c_consumer::stream_adapter_symbol(prefix, owner_type, adapter_name, "next");
let fn_free = c_consumer::stream_adapter_symbol(prefix, owner_type, adapter_name, "free");
let core_item = format!("{core_import}::{item_type}");
let error_type = format!("{handle_name}Error");
let stream_ty = format!("futures_util::stream::BoxStream<'static, Result<{core_item}, {error_type}>>");
let owner_ty = format!("{core_import}::{owner_type}");
let error_carrier = crate::backends::ffi::template_env::render(
"stream_error_carrier.rs.jinja",
crate::alef_context! { error_type => error_type.as_str() },
);
let error_map = crate::backends::ffi::template_env::render(
"stream_error_map.rs.jinja",
crate::alef_context! { error_type => error_type.as_str() },
);
let error_capture =
crate::backends::ffi::template_env::render("stream_error_capture.rs.jinja", crate::alef_context! {});
let core_call = crate::adapters::rust_callable::receiver_call(core_path, "client_ref", "client_ref", "req_owned")?;
Ok(crate::backends::ffi::template_env::render(
"stream_handle_functions.rs.jinja",
crate::alef_context! {
core_call => core_call, error_capture => error_capture,
error_carrier => error_carrier, error_map => error_map,
fn_free => fn_free, fn_next => fn_next,
fn_start => fn_start, handle_name => handle_name,
item_type => item_type, owner_snake => owner_snake,
owner_ty => owner_ty, prefix => prefix,
request_type => request_type, stream_ty => stream_ty,
},
)
.trim_end()
.to_owned())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_rs_feature_stamp_matches_the_cbindgen_defines_and_include_guard() {
let api = crate::core::ir::ApiSurface {
crate_name: "sample_core".to_string(),
version: "0.1.0".to_string(),
functions: vec![crate::core::ir::FunctionDef {
name: "download".to_string(),
rust_path: "sample_core::download".to_string(),
cfg: Some(r#"feature = "download""#.to_string()),
..crate::core::ir::FunctionDef::default()
}],
..crate::core::ir::ApiSurface::default()
};
let capsule_types = std::collections::HashMap::new();
let cbindgen = gen_cbindgen_toml("SampleCore", &api, &capsule_types, &std::collections::BTreeSet::new());
let build = gen_build_rs(
"sample.h",
"libsample_ffi",
"crates/sample-ffi",
None,
"SampleCore",
&capsule_types,
)
.expect("valid build.rs paths");
let guard_macro = cbindgen
.lines()
.find_map(|line| line.trim().strip_prefix(r#""feature = download" = ""#))
.map(|rest| rest.trim_end_matches('"'))
.expect("control: cbindgen.toml must map the gated feature to a guard macro");
assert_eq!(guard_macro, "SAMPLE_CORE_FEATURE_DOWNLOAD");
let include_guard = cbindgen
.lines()
.find_map(|line| line.trim().strip_prefix(r#"include_guard = ""#))
.map(|rest| rest.trim_end_matches('"'))
.expect("control: cbindgen.toml must declare the header's include guard");
assert_eq!(include_guard, "SAMPLE_CORE_H");
let macro_stem = guard_macro
.strip_suffix("DOWNLOAD")
.expect("guard macro ends with the feature name");
assert!(
build.contains(macro_stem),
"build.rs must probe and define `{macro_stem}*`, the macros cbindgen guards with:\n{build}"
);
assert!(
build.contains(&format!(r##""#define {include_guard}\n""##)),
"build.rs must anchor the stamp on cbindgen's own include guard `{include_guard}`:\n{build}"
);
assert!(
!build.contains("SAMPLECORE"),
"build.rs must not fall back to a bare-uppercase prefix for the feature stamp:\n{build}"
);
}
}