alef 0.62.4

Opinionated polyglot binding generator for Rust libraries
Documentation
use super::super::StreamingMethodMeta;
use super::adapters::{gen_adapter_wrapper, gen_opaque_streaming_static_wrapper};
use super::bridge_fields::gen_bridge_field_wrapper_function;
use super::wrappers::{gen_wrapper_function, gen_wrapper_method};
use crate::codegen::generators::trait_bridge::find_bridge_field;
use crate::codegen::naming::{csharp_type_name, to_csharp_name};
use crate::core::config::{AdapterConfig, HostCapsuleTypeConfig};
use crate::core::ir::{ApiSurface, FunctionDef, TypeRef};
use std::collections::{HashMap, HashSet};

/// Check if a function returns a capsule type (Language passthrough).
///
/// Delegates to the shared predicate so this router and `pinvoke_return_type_with_capsules`
/// can never disagree about which functions return a raw host pointer. ~keep
fn is_capsule_function(func: &FunctionDef, capsule_types: &HashMap<String, HostCapsuleTypeConfig>) -> bool {
    super::super::is_capsule_return(&func.return_type, capsule_types)
}

/// Get the capsule config for a function's return type, if it is a capsule.
fn get_capsule_config<'a>(
    func: &FunctionDef,
    capsule_types: &'a HashMap<String, HostCapsuleTypeConfig>,
) -> Option<&'a HostCapsuleTypeConfig> {
    match &func.return_type {
        TypeRef::Named(name) => capsule_types.get(name),
        _ => None,
    }
}

/// Skip methods that take opaque handle FFI pointers as first arg but operate on non-opaque types.
/// These are validation/property functions that shouldn't be exposed as static methods.
/// Examples: header_metadata_is_valid, conversion_options_default (Rust naming, snake_case
/// as stored in FunctionDef.name).
fn should_skip_ffi_method(func: &FunctionDef) -> bool {
    let name = &func.name;

    if name.ends_with("_is_valid") || name == "is_valid" {
        return true;
    }

    if name.ends_with("_default") || name == "default" {
        return true;
    }

    false
}

#[allow(clippy::too_many_arguments)]
pub(in crate::backends::csharp::gen_bindings) fn gen_wrapper_class(
    api: &ApiSurface,
    namespace: &str,
    class_name: &str,
    exception_name: &str,
    prefix: &str,
    bridge_param_names: &HashSet<String>,
    bridge_type_aliases: &HashSet<String>,
    has_visitor_callbacks: bool,
    streaming_methods: &HashSet<String>,
    _streaming_methods_meta: &HashMap<String, StreamingMethodMeta>,
    exclude_functions: &HashSet<String>,
    trait_bridges: &[crate::core::config::TraitBridgeConfig],
    _all_opaque_type_names: &HashSet<String>,
    adapters: &[AdapterConfig],
    capsule_types: &std::collections::HashMap<String, crate::core::config::HostCapsuleTypeConfig>,
) -> String {
    use crate::backends::csharp::template_env::render;
    use minijinja::Value;

    let has_async =
        api.functions.iter().any(|f| f.is_async) || api.types.iter().flat_map(|t| t.methods.iter()).any(|m| m.is_async);

    let mut out = render(
        "wrapper_class_header.jinja",
        Value::from_serialize(serde_json::json!({
            "namespace": namespace,
            "class_name": class_name,
            "has_async": has_async,
        })),
    );
    out.push('\n');

    let enum_names: HashSet<String> = api.enums.iter().map(|e| csharp_type_name(&e.name)).collect();

    let true_opaque_types: HashSet<String> = api
        .types
        .iter()
        .filter(|t| t.is_opaque)
        .map(|t| t.name.clone())
        .collect();

    let handle_returned_types = super::super::errors::compute_handle_returned_types(api);
    let enum_data_variant_names = super::super::marshalling::enum_names_with_data_variants(api);

    for func in api.functions.iter().filter(|f| {
        !exclude_functions.contains(&f.name)
            && !should_skip_ffi_method(f)
            && !crate::codegen::generators::trait_bridge::is_trait_bridge_managed_fn(&f.name, trait_bridges)
    }) {
        let bridge_field = find_bridge_field(func, &api.types, trait_bridges);
        if let Some(bm) = bridge_field {
            out.push_str(&gen_bridge_field_wrapper_function(
                func,
                &bm,
                exception_name,
                &enum_names,
                &true_opaque_types,
                &handle_returned_types,
            ));
        } else if is_capsule_function(func, capsule_types) {
            if let Some(cfg) = get_capsule_config(func, capsule_types) {
                out.push_str(&super::wrappers::gen_capsule_function_wrapper(
                    func,
                    exception_name,
                    prefix,
                    cfg,
                ));
            }
        } else {
            out.push_str(&gen_wrapper_function(
                func,
                exception_name,
                prefix,
                &enum_names,
                &true_opaque_types,
                &handle_returned_types,
                &enum_data_variant_names,
                bridge_param_names,
                bridge_type_aliases,
                has_visitor_callbacks,
                &api.types,
            ));
        }
    }

    for typ in api.types.iter().filter(|typ| !typ.is_trait) {
        if typ.is_opaque {
            continue;
        }
        for method in &typ.methods {
            if streaming_methods.contains(&method.name) {
                continue;
            }
            if method.name == "is_valid" || method.name == "default" {
                continue;
            }
            out.push_str(&gen_wrapper_method(
                method,
                exception_name,
                prefix,
                &typ.name,
                &enum_names,
                &true_opaque_types,
                &handle_returned_types,
                &enum_data_variant_names,
                bridge_param_names,
                bridge_type_aliases,
                &api.types,
            ));
        }
    }

    for typ in api.types.iter().filter(|typ| typ.is_opaque) {
        for method in &typ.methods {
            if !streaming_methods.contains(&method.name) {
                continue;
            }
            if let Some(meta) = _streaming_methods_meta.get(&method.name) {
                out.push_str(&gen_opaque_streaming_static_wrapper(
                    method,
                    &typ.name,
                    meta,
                    exception_name,
                ));
            }
        }
    }

    for adapter in adapters {
        if matches!(adapter.pattern, crate::core::config::AdapterPattern::Streaming) {
            out.push_str(&gen_adapter_wrapper(adapter, prefix, exception_name, api));
        }
    }

    for bridge_cfg in trait_bridges {
        let trait_pascal = csharp_type_name(&bridge_cfg.trait_name);
        let has_super = bridge_cfg.super_trait.is_some();

        // ~keep Gate on `register_fn` exactly as the unregister facade below does. The facade
        // calls `NativeMethods.Register<Trait>`, which is only declared when the bridge has a
        // native register function; emitting it unconditionally produced CS0117 against a
        // NativeMethods class with no such member. Declaring the extern instead would be worse
        // -- no register-shaped symbol exists in the exports, header or dylib, so it would turn
        // a build error into a runtime EntryPointNotFoundException. Java (facade.rs) and Go
        // (gen_bindings/mod.rs) already gate on this field; C# was the outlier.
        if bridge_cfg.register_fn.is_some() {
            let register_method_name = format!("Register{trait_pascal}");
            out.push_str(&render(
                "trait_register_facade.jinja",
                minijinja::context! {
                    trait_name => trait_pascal,
                    method_name => register_method_name,
                    has_super,
                    exception_name,
                },
            ));
        }

        if bridge_cfg.unregister_fn.is_some() {
            let unregister_method_name = format!("Unregister{trait_pascal}");
            out.push_str(&render(
                "trait_unregister_facade.jinja",
                minijinja::context! {
                    trait_name => trait_pascal,
                    method_name => unregister_method_name,
                    exception_name,
                },
            ));
        }
    }

    for bridge_cfg in trait_bridges {
        if let Some(clear_fn) = &bridge_cfg.clear_fn {
            let trait_pascal = csharp_type_name(&bridge_cfg.trait_name);
            let clear_method_name = to_csharp_name(clear_fn);

            out.push_str(&render(
                "trait_clear_facade.jinja",
                minijinja::context! {
                    trait_name => trait_pascal,
                    method_name => clear_method_name,
                },
            ));
        }
    }

    let has_base_error = !api.errors.is_empty();
    let (base_exception_class, variant_dispatch_lines) = if has_base_error {
        let base_error = &api.errors[0];
        let base_ex = format!("{}Exception", base_error.name);
        // Every variant dispatches by message prefix here, `InvalidInput` included: the FFI
        // layer's numeric code 1 is the infrastructure `ALEF_FFI_CONVERSION_ERROR`, not a slot
        // reserved for a user variant that happens to share that name, and
        // `ApiSurface::validate_error_taxonomy` forbids user `error_code`s below 100 — so no
        // legitimate user variant can ever earn code 1. A prior version special-cased
        // `code == 1` straight to `InvalidInputException`, which mislabeled every real
        // conversion failure as that variant whenever an error enum happened to declare one. ~keep
        let mut variants_with_prefix: Vec<(String, String)> = base_error
            .variants
            .iter()
            .filter_map(|v| {
                let template = v.message_template.as_deref()?;
                let prefix_end = template.find('{').unwrap_or(template.len());
                let prefix = template[..prefix_end].trim_end().to_string();
                if prefix.is_empty() {
                    return None;
                }
                Some((format!("{}Exception", v.name), prefix))
            })
            .collect();
        variants_with_prefix.sort_by_key(|item| std::cmp::Reverse(item.1.len()));
        let dispatch_lines: Vec<String> = variants_with_prefix
            .into_iter()
            .map(|(class, prefix)| {
                let escaped_prefix = prefix.replace('\\', "\\\\").replace('"', "\\\"");
                format!("        if (message.StartsWith(\"{escaped_prefix}\")) return new {class}(message);")
            })
            .collect();
        (base_ex, dispatch_lines)
    } else {
        (String::new(), Vec::new())
    };

    out.push_str(&render(
        "error_helper_method.jinja",
        Value::from_serialize(serde_json::json!({
            "exception_name": exception_name,
            "has_base_error": has_base_error,
            "base_exception_class": base_exception_class,
            "variant_dispatch_lines": variant_dispatch_lines,
        })),
    ));

    out.push_str("}\n");

    out
}