alef_backend_zig/gen_bindings/
mod.rs1use 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(¶m.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_helpers(&prefix, &mut content);
125 content.push('\n');
126
127 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 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 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 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 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 for bridge_cfg in &config.trait_bridges {
253 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 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 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 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}