alef-backend-zig 0.15.14

Zig backend for alef
Documentation
use alef_core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile};
use alef_core::config::{Language, ResolvedCrateConfig, resolve_output_dir};
use alef_core::ir::ApiSurface;
use std::path::PathBuf;

use crate::trait_bridge::emit_trait_bridge;

mod errors;
mod functions;
mod helpers;
mod types;

use errors::emit_error_set;
use functions::emit_function;
use helpers::emit_helpers;
use types::{emit_enum, emit_type};

fn zig_module_name(crate_name: &str) -> String {
    crate_name.replace('-', "_")
}

pub struct ZigBackend;

impl Backend for ZigBackend {
    fn name(&self) -> &str {
        "zig"
    }

    fn language(&self) -> Language {
        Language::Zig
    }

    fn capabilities(&self) -> Capabilities {
        Capabilities {
            supports_async: false,
            supports_classes: false,
            supports_enums: true,
            supports_option: true,
            supports_result: true,
            supports_callbacks: false,
            supports_streaming: false,
        }
    }

    fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
        let module_name = zig_module_name(&config.name);
        let header = config.ffi_header_name();
        let prefix = config.ffi_prefix();

        let exclude_functions: std::collections::HashSet<&str> = config
            .zig
            .as_ref()
            .map(|c| c.exclude_functions.iter().map(String::as_str).collect())
            .unwrap_or_default();
        let exclude_types: std::collections::HashSet<&str> = config
            .zig
            .as_ref()
            .map(|c| c.exclude_types.iter().map(String::as_str).collect())
            .unwrap_or_default();

        let has_async = api
            .functions
            .iter()
            .filter(|f| !exclude_functions.contains(f.name.as_str()))
            .any(|f| f.is_async);

        let mut content = String::new();
        content.push_str("// Generated by alef. Do not edit by hand.\n");
        if has_async {
            content.push_str("// Async functions are not supported in this backend.\n");
        }
        content.push('\n');
        content.push_str("const std = @import(\"std\");\n");
        content.push_str(&crate::template_env::render(
            "c_import.jinja",
            minijinja::context! {
                header => header,
            },
        ));
        content.push('\n');

        // Emit helper wrappers for FFI error introspection and string ownership.
        emit_helpers(&prefix, &mut content);
        content.push('\n');

        for error in &api.errors {
            emit_error_set(error, &mut content);
            content.push('\n');
        }

        for ty in api.types.iter().filter(|t| !exclude_types.contains(t.name.as_str())) {
            emit_type(ty, &mut content);
            content.push('\n');
        }

        for en in api.enums.iter().filter(|e| !exclude_types.contains(e.name.as_str())) {
            emit_enum(en, &mut content);
            content.push('\n');
        }

        let declared_errors: Vec<String> = api.errors.iter().map(|e| e.name.clone()).collect();
        // Collect all top-level decl names so we can rename param names that would
        // shadow them. Zig 0.16 forbids parameter shadowing of file-scope decls.
        let mut top_level_names: std::collections::HashSet<String> = std::collections::HashSet::new();
        for f in &api.functions {
            top_level_names.insert(f.name.clone());
        }
        for ty in &api.types {
            top_level_names.insert(ty.name.clone());
        }
        for en in &api.enums {
            top_level_names.insert(en.name.clone());
        }
        // Set of struct (non-enum, non-trait) type names. Function parameters
        // typed as `Named(name)` where `name ∈ struct_names` are passed across
        // the FFI as opaque handles via JSON, since cbindgen emits them as
        // opaque types in the generated header.
        let struct_names: std::collections::HashSet<String> = api
            .types
            .iter()
            .filter(|t| !t.is_trait)
            .map(|t| t.name.clone())
            .collect();
        // Functions matching `register_{trait_snake}` / `unregister_{trait_snake}` for
        // any configured trait bridge are emitted by `emit_trait_bridge` with a
        // proper vtable signature. Skip the regular C-FFI shim to avoid duplicate
        // function definitions.
        let trait_bridge_fn_names: std::collections::HashSet<String> = config
            .trait_bridges
            .iter()
            .filter(|b| !b.exclude_languages.iter().any(|lang| lang == "zig"))
            .filter_map(|b| {
                api.types
                    .iter()
                    .find(|t| t.name == b.trait_name && t.is_trait)
                    .map(|t| heck::AsSnakeCase(&t.name).to_string())
            })
            .flat_map(|snake| [format!("register_{snake}"), format!("unregister_{snake}")])
            .collect();
        for f in api
            .functions
            .iter()
            .filter(|f| !exclude_functions.contains(f.name.as_str()))
        {
            // Async functions are not supported — skip silently.
            if f.is_async {
                continue;
            }
            if trait_bridge_fn_names.contains(&f.name) {
                continue;
            }
            emit_function(
                f,
                &prefix,
                &declared_errors,
                &top_level_names,
                &struct_names,
                &mut content,
            );
            content.push('\n');
        }

        // Emit C-vtable structs and registration shims for every configured trait bridge.
        // Zig has no inheritance; trait bridges use an `extern struct` of function pointers.
        for bridge_cfg in &config.trait_bridges {
            // Skip if "zig" is listed in exclude_languages for this bridge.
            if bridge_cfg.exclude_languages.iter().any(|lang| lang == "zig") {
                continue;
            }
            if let Some(trait_def) = api.types.iter().find(|t| t.name == bridge_cfg.trait_name && t.is_trait) {
                emit_trait_bridge(&prefix, bridge_cfg, trait_def, &mut content);
                content.push('\n');
            }
        }

        let dir = resolve_output_dir(None, &config.name, "packages/zig/src");
        let path = PathBuf::from(dir).join(format!("{module_name}.zig"));

        Ok(vec![GeneratedFile {
            path,
            content,
            generated_header: false,
        }])
    }

    fn build_config(&self) -> Option<BuildConfig> {
        Some(BuildConfig {
            tool: "zig",
            crate_suffix: "",
            build_dep: BuildDependency::Ffi,
            post_build: vec![],
        })
    }
}