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