Skip to main content

alef_backend_zig/gen_bindings/
mod.rs

1use alef_core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile};
2use alef_core::config::{AdapterPattern, Language, ResolvedCrateConfig, resolve_output_dir};
3use alef_core::ir::{ApiSurface, TypeRef};
4use std::fmt::Write as FmtWrite;
5use std::path::PathBuf;
6
7use crate::trait_bridge::emit_trait_bridge;
8
9mod errors;
10mod functions;
11mod helpers;
12mod opaque_handles;
13mod types;
14
15use errors::emit_error_set;
16use functions::emit_function;
17use helpers::emit_helpers;
18use opaque_handles::{emit_opaque_constructor, emit_opaque_handle};
19use types::{emit_enum, emit_type};
20
21fn zig_module_name(crate_name: &str) -> String {
22    crate_name.replace('-', "_")
23}
24
25pub struct ZigBackend;
26
27fn type_references_excluded(ty: &TypeRef, exclude_types: &std::collections::HashSet<String>) -> bool {
28    exclude_types.iter().any(|name| ty.references_named(name))
29}
30
31fn signature_references_excluded(
32    params: &[alef_core::ir::ParamDef],
33    return_type: &TypeRef,
34    exclude_types: &std::collections::HashSet<String>,
35) -> bool {
36    type_references_excluded(return_type, exclude_types)
37        || params
38            .iter()
39            .any(|param| type_references_excluded(&param.ty, exclude_types))
40}
41
42impl Backend for ZigBackend {
43    fn name(&self) -> &str {
44        "zig"
45    }
46
47    fn language(&self) -> Language {
48        Language::Zig
49    }
50
51    fn capabilities(&self) -> Capabilities {
52        Capabilities {
53            supports_async: false,
54            supports_classes: true,
55            supports_enums: true,
56            supports_option: true,
57            supports_result: true,
58            supports_callbacks: false,
59            supports_streaming: false,
60        }
61    }
62
63    fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
64        let module_name = zig_module_name(&config.name);
65        let header = config.ffi_header_name();
66        let prefix = config.ffi_prefix();
67
68        let mut exclude_functions: std::collections::HashSet<String> = config
69            .zig
70            .as_ref()
71            .map(|c| c.exclude_functions.iter().cloned().collect())
72            .unwrap_or_default();
73        let mut exclude_types: std::collections::HashSet<String> = config
74            .ffi
75            .as_ref()
76            .map(|c| c.exclude_types.iter().cloned().collect())
77            .unwrap_or_default();
78        if let Some(zig) = &config.zig {
79            exclude_types.extend(zig.exclude_types.iter().cloned());
80        }
81        if let Some(ffi) = &config.ffi {
82            exclude_functions.extend(ffi.exclude_functions.iter().cloned());
83        }
84
85        let type_is_visible = |name: &str| !exclude_types.contains(name);
86        let method_is_visible = |method: &alef_core::ir::MethodDef| {
87            !signature_references_excluded(&method.params, &method.return_type, &exclude_types)
88        };
89
90        let visible_api;
91        let api = if exclude_types.is_empty() {
92            api
93        } else {
94            visible_api = {
95                let mut filtered = api.clone();
96                filtered.types.retain(|typ| type_is_visible(&typ.name));
97                for typ in &mut filtered.types {
98                    typ.fields
99                        .retain(|field| !type_references_excluded(&field.ty, &exclude_types));
100                    typ.methods.retain(method_is_visible);
101                }
102                filtered.enums.retain(|en| !exclude_types.contains(&en.name));
103                filtered
104                    .functions
105                    .retain(|func| !signature_references_excluded(&func.params, &func.return_type, &exclude_types));
106                filtered
107            };
108            &visible_api
109        };
110
111        let mut content = String::new();
112        content.push_str("// Generated by alef. Do not edit by hand.\n");
113        content.push('\n');
114        content.push_str("const std = @import(\"std\");\n");
115        content.push_str(&crate::template_env::render(
116            "c_import.jinja",
117            minijinja::context! {
118                header => header,
119            },
120        ));
121        content.push('\n');
122
123        // Emit helper wrappers for FFI error introspection and string ownership.
124        emit_helpers(&prefix, &mut content);
125        content.push('\n');
126
127        // Z1 fix: emit opaque pointer type aliases for every configured trait bridge
128        // that declares a `type_alias`. These aliases are referenced in struct fields
129        // (e.g. `visitor: ?VisitorHandle`) but the Zig backend previously never declared
130        // them, causing "use of undeclared identifier" errors in Zig 0.16+.
131        for bridge in &config.trait_bridges {
132            if bridge.exclude_languages.iter().any(|lang| lang == "zig") {
133                continue;
134            }
135            if let Some(alias) = &bridge.type_alias {
136                let _ = writeln!(content, "/// Opaque handle for a `{alias}` trait-bridge instance.",);
137                let _ = writeln!(content, "pub const {alias} = *anyopaque;");
138                content.push('\n');
139            }
140        }
141
142        for error in &api.errors {
143            emit_error_set(error, &mut content);
144            content.push('\n');
145        }
146
147        for ty in api
148            .types
149            .iter()
150            .filter(|t| !exclude_types.contains(&t.name) && !t.is_opaque && t.has_serde)
151        {
152            emit_type(ty, &mut content);
153            content.push('\n');
154        }
155
156        for en in api.enums.iter().filter(|e| !exclude_types.contains(&e.name)) {
157            emit_enum(en, &mut content);
158            content.push('\n');
159        }
160
161        let declared_errors: Vec<String> = api.errors.iter().map(|e| e.name.clone()).collect();
162        // Collect all top-level decl names so we can rename param names that would
163        // shadow them. Zig 0.16 forbids parameter shadowing of file-scope decls.
164        let mut top_level_names: std::collections::HashSet<String> = std::collections::HashSet::new();
165        for f in &api.functions {
166            top_level_names.insert(f.name.clone());
167        }
168        for ty in &api.types {
169            top_level_names.insert(ty.name.clone());
170        }
171        for en in &api.enums {
172            top_level_names.insert(en.name.clone());
173        }
174        // Set of struct (non-enum, non-trait) type names. Function parameters
175        // typed as `Named(name)` where `name ∈ struct_names` are passed across
176        // the FFI as opaque handles via JSON, since cbindgen emits them as
177        // opaque types in the generated header.
178        let struct_names: std::collections::HashSet<String> = api
179            .types
180            .iter()
181            .filter(|t| !t.is_trait && !t.is_opaque && t.has_serde)
182            .map(|t| t.name.clone())
183            .collect();
184        // For opaque handle types (is_opaque = true or has_serde = false), find the
185        // creator function in api.functions that returns that type. The map stores:
186        //   opaque_type_name -> (creator_fn_name, config_type_snake_case)
187        // Used by emit_function to generate create+use+free patterns for handle params.
188        let opaque_creator_map: std::collections::HashMap<String, (String, String)> = {
189            let mut map = std::collections::HashMap::new();
190            for opaque_ty in api
191                .types
192                .iter()
193                .filter(|t| !t.is_trait && (t.is_opaque || !t.has_serde))
194            {
195                if let Some(creator) = api
196                    .functions
197                    .iter()
198                    .find(|f| matches!(&f.return_type, alef_core::ir::TypeRef::Named(n) if n == &opaque_ty.name))
199                {
200                    if let Some(config_param) = creator.params.first() {
201                        if let Some(config_name) = functions::opaque_type_name_inner(&config_param.ty) {
202                            map.insert(
203                                opaque_ty.name.clone(),
204                                (creator.name.clone(), heck::AsSnakeCase(config_name).to_string()),
205                            );
206                        }
207                    }
208                }
209            }
210            map
211        };
212
213        // Functions matching `register_{trait_snake}` / `unregister_{trait_snake}` /
214        // the configured `clear_fn` for any configured trait bridge are emitted by
215        // `emit_trait_bridge` with a proper vtable signature. Skip the regular
216        // C-FFI shim to avoid duplicate function definitions.
217        let trait_bridge_fn_names: std::collections::HashSet<String> = config
218            .trait_bridges
219            .iter()
220            .filter(|b| !b.exclude_languages.iter().any(|lang| lang == "zig"))
221            .flat_map(|b| {
222                let mut names = Vec::new();
223                if let Some(trait_def) = api.types.iter().find(|t| t.name == b.trait_name && t.is_trait) {
224                    let snake = heck::AsSnakeCase(&trait_def.name).to_string();
225                    names.push(format!("register_{snake}"));
226                    names.push(format!("unregister_{snake}"));
227                }
228                if let Some(clear_fn) = b.clear_fn.as_deref() {
229                    names.push(clear_fn.to_string());
230                }
231                names
232            })
233            .collect();
234        for f in api.functions.iter().filter(|f| !exclude_functions.contains(&f.name)) {
235            if trait_bridge_fn_names.contains(&f.name) {
236                continue;
237            }
238            emit_function(
239                f,
240                &prefix,
241                &declared_errors,
242                &top_level_names,
243                &struct_names,
244                &opaque_creator_map,
245                &mut content,
246            );
247            content.push('\n');
248        }
249
250        // Emit C-vtable structs and registration shims for every configured trait bridge.
251        // Zig has no inheritance; trait bridges use an `extern struct` of function pointers.
252        for bridge_cfg in &config.trait_bridges {
253            // Skip if "zig" is listed in exclude_languages for this bridge.
254            if bridge_cfg.exclude_languages.iter().any(|lang| lang == "zig") {
255                continue;
256            }
257            if let Some(trait_def) = api.types.iter().find(|t| t.name == bridge_cfg.trait_name && t.is_trait) {
258                emit_trait_bridge(&prefix, bridge_cfg, trait_def, &mut content);
259                content.push('\n');
260            }
261        }
262
263        // Build a map of method_name -> item_type for Streaming adapters.
264        // Used by emit_opaque_handle to emit iterator-based bodies instead of
265        // the generic method wrapper (which would call the callback-based C symbol
266        // with the wrong argument count).
267        let streaming_item_types: std::collections::HashMap<String, String> = config
268            .adapters
269            .iter()
270            .filter(|a| matches!(a.pattern, AdapterPattern::Streaming))
271            .filter_map(|a| a.item_type.as_ref().map(|item| (a.name.clone(), item.clone())))
272            .collect();
273
274        // Emit Zig struct wrappers for all opaque handle types, including those
275        // with no instance methods (e.g. a bare Language handle returned by
276        // get_language()). Every opaque type that appears in a function signature
277        // must be declared; omitting method-less types causes "use of undeclared
278        // identifier" compile errors in Zig.
279        for ty in api
280            .types
281            .iter()
282            .filter(|t| !t.is_trait && (t.is_opaque || !t.has_serde))
283            .filter(|t| !exclude_types.contains(&t.name))
284        {
285            emit_opaque_handle(
286                ty,
287                &prefix,
288                &declared_errors,
289                &struct_names,
290                &streaming_item_types,
291                &mut content,
292            );
293            content.push('\n');
294            // Client constructor — emit create_<type_snake> when configured.
295            if let Some(ctor) = config.client_constructors.get(&ty.name) {
296                emit_opaque_constructor(ty, &prefix, ctor, &mut content);
297                content.push('\n');
298            }
299        }
300
301        let dir = resolve_output_dir(None, &config.name, "packages/zig/src");
302        let path = PathBuf::from(dir).join(format!("{module_name}.zig"));
303
304        Ok(vec![GeneratedFile {
305            path,
306            content,
307            generated_header: false,
308        }])
309    }
310
311    fn build_config(&self) -> Option<BuildConfig> {
312        Some(BuildConfig {
313            tool: "zig",
314            crate_suffix: "",
315            build_dep: BuildDependency::Ffi,
316            post_build: vec![],
317        })
318    }
319}