mod call_body;
mod helpers;
mod registration;
mod vtable;
use crate::codegen::generators::trait_bridge::{TraitBridgeSpec, gen_bridge_plugin_impl};
use crate::codegen::naming::{pascal_to_snake, to_class_name};
use crate::core::config::TraitBridgeConfig;
use crate::core::ir::{ApiSurface, TypeDef, TypeRef};
use std::collections::{HashMap, HashSet};
use helpers::prim_to_c;
pub struct FfiBridgeGenerator {
pub prefix: String,
pub core_import: String,
pub type_paths: HashMap<String, String>,
pub error_type: String,
pub plugin_error_constructor: Option<String>,
pub lifetime_type_names: HashSet<String>,
}
impl FfiBridgeGenerator {
pub(super) fn vtable_name(&self, spec: &TraitBridgeSpec) -> String {
let pascal = to_class_name(&self.prefix);
format!("{}{}VTable", pascal, spec.trait_def.name)
}
pub(super) fn bridge_name(&self, spec: &TraitBridgeSpec) -> String {
let pascal = to_class_name(&self.prefix);
format!("{}{}Bridge", pascal, spec.trait_def.name)
}
pub(super) fn c_param_type(ty: &TypeRef) -> String {
match ty {
TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "*const std::ffi::c_char".to_string(),
TypeRef::Bytes => "*const u8".to_string(),
TypeRef::Primitive(p) => prim_to_c(p).to_string(),
TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
"*const std::ffi::c_char".to_string()
}
TypeRef::Optional(inner) => {
match inner.as_ref() {
TypeRef::Primitive(p) => prim_to_c(p).to_string(),
_ => "*const std::ffi::c_char".to_string(),
}
}
TypeRef::Unit => "()".to_string(),
TypeRef::Duration => "u64".to_string(),
}
}
pub(super) fn c_return_convention(ty: &TypeRef, has_error: bool) -> (Vec<String>, String) {
let needs_out_error = matches!(
ty,
TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::String | TypeRef::Json
) || has_error;
let out_params = match ty {
TypeRef::Unit => {
if has_error {
vec!["out_error: *mut *mut std::ffi::c_char".to_string()]
} else {
vec![]
}
}
TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => {
let mut v = vec!["out_result: *mut *mut std::ffi::c_char".to_string()];
if needs_out_error {
v.push("out_error: *mut *mut std::ffi::c_char".to_string());
}
v
}
TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
let mut v = vec!["out_result: *mut *mut std::ffi::c_char".to_string()];
if needs_out_error {
v.push("out_error: *mut *mut std::ffi::c_char".to_string());
}
v
}
_ => {
if has_error {
vec!["out_error: *mut *mut std::ffi::c_char".to_string()]
} else {
vec![]
}
}
};
let ret = if has_error || needs_out_error {
"i32".to_string()
} else {
match ty {
TypeRef::Primitive(p) => prim_to_c(p).to_string(),
TypeRef::Unit => "()".to_string(),
TypeRef::Duration => "u64".to_string(),
TypeRef::Optional(inner) => match inner.as_ref() {
TypeRef::Primitive(p) => prim_to_c(p).to_string(),
_ => "i32".to_string(), },
_ => "i32".to_string(),
}
};
(out_params, ret)
}
}
pub fn gen_ffi_set_out_error_helper() -> String {
crate::backends::ffi::template_env::render("ffi_set_out_error_helper.jinja", minijinja::context! {})
}
#[allow(clippy::too_many_arguments)]
pub fn gen_trait_bridge(
trait_type: &TypeDef,
bridge_cfg: &TraitBridgeConfig,
prefix: &str,
core_import: &str,
error_type: &str,
error_constructor: &str,
plugin_error_constructor: Option<&str>,
api: &ApiSurface,
) -> String {
let type_paths: HashMap<String, String> = api
.types
.iter()
.map(|t| (t.name.clone(), t.rust_path.replace('-', "_")))
.chain(
api.enums
.iter()
.map(|e| (e.name.clone(), e.rust_path.replace('-', "_"))),
)
.chain(
api.excluded_type_paths
.iter()
.map(|(name, path)| (name.clone(), path.replace('-', "_"))),
)
.collect();
let lifetime_type_names: HashSet<String> = api
.types
.iter()
.filter(|t| t.has_lifetime_params)
.map(|t| t.name.clone())
.collect();
let generator = FfiBridgeGenerator {
prefix: prefix.to_string(),
core_import: core_import.to_string(),
type_paths: type_paths.clone(),
error_type: error_type.to_string(),
plugin_error_constructor: plugin_error_constructor.map(str::to_string),
lifetime_type_names,
};
let wrapper_prefix = to_class_name(prefix);
let spec_lifetime_type_names: HashSet<String> = api
.types
.iter()
.filter(|t| t.has_lifetime_params)
.map(|t| t.name.clone())
.collect();
let spec = TraitBridgeSpec {
trait_def: trait_type,
bridge_config: bridge_cfg,
core_import,
wrapper_prefix: &wrapper_prefix,
type_paths,
lifetime_type_names: spec_lifetime_type_names,
error_type: error_type.to_string(),
error_constructor: error_constructor.to_string(),
};
let mut out = String::with_capacity(4096);
out.push_str(&generator.gen_vtable_struct(&spec));
out.push('\n');
out.push_str(&generator.gen_bridge_struct(&spec));
out.push('\n');
out.push_str(&generator.gen_bridge_drop(&spec));
out.push('\n');
out.push_str(&generator.gen_constructor_impl(&spec));
out.push('\n');
if let Some(plugin_impl) = generator.gen_ffi_plugin_impl(&spec) {
out.push_str(&plugin_impl);
out.push('\n');
} else {
if let Some(plugin_impl) = gen_bridge_plugin_impl(&spec, &generator) {
out.push_str(&plugin_impl);
out.push('\n');
}
}
out.push_str(&generator.gen_ffi_trait_impl(&spec));
out.push('\n');
if spec.bridge_config.register_fn.is_some() {
out.push('\n');
out.push_str(&generator.gen_registration_fn_impl(&spec));
}
out
}
pub fn gen_bridge_new_free(prefix: &str, pascal_prefix: &str, trait_name: &str) -> String {
let bridge_name = format!("{pascal_prefix}{trait_name}Bridge");
let vtable_name = format!("{pascal_prefix}{trait_name}VTable");
let bridge_snake = ffi_symbol_component(&bridge_name);
let fn_new = format!("{prefix}_{bridge_snake}_new");
let fn_free = format!("{prefix}_{bridge_snake}_free");
format!(
r#"/// Create a new `{bridge_name}` from a vtable and opaque user_data pointer.
///
/// Returns a heap-allocated `{bridge_name}` on success, or null if `vtable` is null.
/// The caller is responsible for calling `{fn_free}` exactly once when the bridge is
/// no longer needed.
///
/// # Safety
///
/// `vtable` must be a non-null pointer to a fully initialised `{vtable_name}` that
/// remains valid for the lifetime of the returned bridge. `user_data` must be valid
/// for any thread that calls methods on this bridge.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn {fn_new}(
vtable: *const {vtable_name},
user_data: *const std::ffi::c_void,
) -> *mut {bridge_name} {{
if vtable.is_null() {{
return std::ptr::null_mut();
}}
// SAFETY: vtable is non-null (checked above); caller guarantees it is valid for this call.
let bridge = unsafe {{ {bridge_name}::new(String::new(), *vtable, user_data) }};
Box::into_raw(Box::new(bridge))
}}
/// Free a `{bridge_name}` created by `{fn_new}`.
///
/// After this call `ptr` is invalid. Passing null is a no-op.
///
/// # Safety
///
/// `ptr` must be either null or a non-null pointer returned by `{fn_new}` that has
/// not yet been freed.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn {fn_free}(ptr: *mut {bridge_name}) {{
if !ptr.is_null() {{
// SAFETY: ptr is non-null and was created via Box::into_raw in {fn_new}.
drop(unsafe {{ Box::from_raw(ptr) }});
}}
}}"#,
)
}
fn ffi_symbol_component(s: &str) -> String {
pascal_to_snake(s)
}
#[cfg(test)]
mod tests;