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