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;
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_helpers(&prefix, &mut content);
78 content.push('\n');
79
80 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 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 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 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 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 for bridge_cfg in &config.trait_bridges {
205 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 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 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}