Skip to main content

alef_backend_zig/gen_bindings/
mod.rs

1use alef_core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile};
2use alef_core::config::{Language, ResolvedCrateConfig, resolve_output_dir};
3use alef_core::ir::ApiSurface;
4use std::path::PathBuf;
5
6use crate::trait_bridge::emit_trait_bridge;
7
8mod errors;
9mod functions;
10mod helpers;
11mod types;
12
13use errors::emit_error_set;
14use functions::emit_function;
15use helpers::emit_helpers;
16use types::{emit_enum, emit_type};
17
18fn zig_module_name(crate_name: &str) -> String {
19    crate_name.replace('-', "_")
20}
21
22pub struct ZigBackend;
23
24impl Backend for ZigBackend {
25    fn name(&self) -> &str {
26        "zig"
27    }
28
29    fn language(&self) -> Language {
30        Language::Zig
31    }
32
33    fn capabilities(&self) -> Capabilities {
34        Capabilities {
35            supports_async: false,
36            supports_classes: false,
37            supports_enums: true,
38            supports_option: true,
39            supports_result: true,
40            supports_callbacks: false,
41            supports_streaming: false,
42        }
43    }
44
45    fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
46        let module_name = zig_module_name(&config.name);
47        let header = config.ffi_header_name();
48        let prefix = config.ffi_prefix();
49
50        let exclude_functions: std::collections::HashSet<&str> = config
51            .zig
52            .as_ref()
53            .map(|c| c.exclude_functions.iter().map(String::as_str).collect())
54            .unwrap_or_default();
55        let exclude_types: std::collections::HashSet<&str> = config
56            .zig
57            .as_ref()
58            .map(|c| c.exclude_types.iter().map(String::as_str).collect())
59            .unwrap_or_default();
60
61        let has_async = api
62            .functions
63            .iter()
64            .filter(|f| !exclude_functions.contains(f.name.as_str()))
65            .any(|f| f.is_async);
66
67        let mut content = String::new();
68        content.push_str("// Generated by alef. Do not edit by hand.\n");
69        if has_async {
70            content.push_str("// Async functions are not supported in this backend.\n");
71        }
72        content.push('\n');
73        content.push_str("const std = @import(\"std\");\n");
74        content.push_str(&crate::template_env::render(
75            "c_import.jinja",
76            minijinja::context! {
77                header => header,
78            },
79        ));
80        content.push('\n');
81
82        // Emit helper wrappers for FFI error introspection and string ownership.
83        emit_helpers(&prefix, &mut content);
84        content.push('\n');
85
86        for error in &api.errors {
87            emit_error_set(error, &mut content);
88            content.push('\n');
89        }
90
91        for ty in api
92            .types
93            .iter()
94            .filter(|t| !exclude_types.contains(t.name.as_str()) && !t.is_opaque && t.has_serde)
95        {
96            emit_type(ty, &mut content);
97            content.push('\n');
98        }
99
100        for en in api.enums.iter().filter(|e| !exclude_types.contains(e.name.as_str())) {
101            emit_enum(en, &mut content);
102            content.push('\n');
103        }
104
105        let declared_errors: Vec<String> = api.errors.iter().map(|e| e.name.clone()).collect();
106        // Collect all top-level decl names so we can rename param names that would
107        // shadow them. Zig 0.16 forbids parameter shadowing of file-scope decls.
108        let mut top_level_names: std::collections::HashSet<String> = std::collections::HashSet::new();
109        for f in &api.functions {
110            top_level_names.insert(f.name.clone());
111        }
112        for ty in &api.types {
113            top_level_names.insert(ty.name.clone());
114        }
115        for en in &api.enums {
116            top_level_names.insert(en.name.clone());
117        }
118        // Set of struct (non-enum, non-trait) type names. Function parameters
119        // typed as `Named(name)` where `name ∈ struct_names` are passed across
120        // the FFI as opaque handles via JSON, since cbindgen emits them as
121        // opaque types in the generated header.
122        let struct_names: std::collections::HashSet<String> = api
123            .types
124            .iter()
125            .filter(|t| !t.is_trait && !t.is_opaque && t.has_serde)
126            .map(|t| t.name.clone())
127            .collect();
128        // For opaque handle types (is_opaque = true or has_serde = false), find the
129        // creator function in api.functions that returns that type. The map stores:
130        //   opaque_type_name -> (creator_fn_name, config_type_snake_case)
131        // Used by emit_function to generate create+use+free patterns for handle params.
132        let opaque_creator_map: std::collections::HashMap<String, (String, String)> = {
133            let mut map = std::collections::HashMap::new();
134            for opaque_ty in api
135                .types
136                .iter()
137                .filter(|t| !t.is_trait && (t.is_opaque || !t.has_serde))
138            {
139                if let Some(creator) = api
140                    .functions
141                    .iter()
142                    .find(|f| matches!(&f.return_type, alef_core::ir::TypeRef::Named(n) if n == &opaque_ty.name))
143                {
144                    if let Some(config_param) = creator.params.first() {
145                        if let Some(config_name) = functions::opaque_type_name_inner(&config_param.ty) {
146                            map.insert(
147                                opaque_ty.name.clone(),
148                                (creator.name.clone(), heck::AsSnakeCase(config_name).to_string()),
149                            );
150                        }
151                    }
152                }
153            }
154            map
155        };
156
157        // Functions matching `register_{trait_snake}` / `unregister_{trait_snake}` for
158        // any configured trait bridge are emitted by `emit_trait_bridge` with a
159        // proper vtable signature. Skip the regular C-FFI shim to avoid duplicate
160        // function definitions.
161        let trait_bridge_fn_names: std::collections::HashSet<String> = config
162            .trait_bridges
163            .iter()
164            .filter(|b| !b.exclude_languages.iter().any(|lang| lang == "zig"))
165            .filter_map(|b| {
166                api.types
167                    .iter()
168                    .find(|t| t.name == b.trait_name && t.is_trait)
169                    .map(|t| heck::AsSnakeCase(&t.name).to_string())
170            })
171            .flat_map(|snake| [format!("register_{snake}"), format!("unregister_{snake}")])
172            .collect();
173        for f in api
174            .functions
175            .iter()
176            .filter(|f| !exclude_functions.contains(f.name.as_str()))
177        {
178            if trait_bridge_fn_names.contains(&f.name) {
179                continue;
180            }
181            emit_function(
182                f,
183                &prefix,
184                &declared_errors,
185                &top_level_names,
186                &struct_names,
187                &opaque_creator_map,
188                &mut content,
189            );
190            content.push('\n');
191        }
192
193        // Emit C-vtable structs and registration shims for every configured trait bridge.
194        // Zig has no inheritance; trait bridges use an `extern struct` of function pointers.
195        for bridge_cfg in &config.trait_bridges {
196            // Skip if "zig" is listed in exclude_languages for this bridge.
197            if bridge_cfg.exclude_languages.iter().any(|lang| lang == "zig") {
198                continue;
199            }
200            if let Some(trait_def) = api.types.iter().find(|t| t.name == bridge_cfg.trait_name && t.is_trait) {
201                emit_trait_bridge(&prefix, bridge_cfg, trait_def, &mut content);
202                content.push('\n');
203            }
204        }
205
206        let dir = resolve_output_dir(None, &config.name, "packages/zig/src");
207        let path = PathBuf::from(dir).join(format!("{module_name}.zig"));
208
209        Ok(vec![GeneratedFile {
210            path,
211            content,
212            generated_header: false,
213        }])
214    }
215
216    fn build_config(&self) -> Option<BuildConfig> {
217        Some(BuildConfig {
218            tool: "zig",
219            crate_suffix: "",
220            build_dep: BuildDependency::Ffi,
221            post_build: vec![],
222        })
223    }
224}