weaveffi_core/backend/mod.rs
1//! The language-backend framework.
2//!
3//! Every idiomatic WeaveFFI generator does the same three things: it walks the
4//! [`BindingModel`] in a fixed order (enums → structs → callbacks → listeners
5//! → functions), dispatches each function on its [`CallShape`], and writes a
6//! primary source file plus a handful of package manifests. Before this module
7//! existed, all eleven generators hand-rolled that walk, that dispatch, that
8//! file I/O, and their own copy of the [`Generator`] glue; they drifted.
9//!
10//! [`LanguageBackend`] captures the common structure as a trait whose hooks a
11//! backend implements, and the free [`run`]/[`output_files`] functions plus the
12//! [`impl_generator_via_backend!`](crate::impl_generator_via_backend) macro provide the shared driver. A backend
13//! now owns *only* language-specific rendering: type mapping, marshalling, and
14//! the exact text of each declaration. The traversal order, the call-shape
15//! dispatch, the model construction, and the bridge to the object-safe
16//! [`Generator`]/`DynGenerator` layer all live here, once.
17//!
18//! [`BindingModel`]: crate::model::BindingModel
19//! [`CallShape`]: crate::model::CallShape
20//! [`Generator`]: crate::codegen::Generator
21
22use anyhow::Result;
23use camino::{Utf8Path, Utf8PathBuf};
24use serde::Serialize;
25use weaveffi_ir::ir::Api;
26
27use crate::capabilities::TargetCapabilities;
28use crate::model::{
29 BindingModel, CallbackBinding, EnumBinding, FnBinding, ListenerBinding, ModuleBinding,
30 StructBinding,
31};
32use crate::package::{PackageContext, PackagedFile};
33
34/// A single generated file: its full path (under the output directory) and the
35/// rendered contents. Backends return these from [`LanguageBackend::files`];
36/// the driver creates parent directories and writes them.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct OutputFile {
39 /// Full path to write, under (or anchored at) the output directory.
40 pub path: Utf8PathBuf,
41 /// The rendered file contents.
42 pub contents: String,
43}
44
45impl OutputFile {
46 /// Pair a destination path with its rendered contents.
47 pub fn new(path: impl Into<Utf8PathBuf>, contents: impl Into<String>) -> Self {
48 Self {
49 path: path.into(),
50 contents: contents.into(),
51 }
52 }
53}
54
55/// An idiomatic language backend over the shared [`BindingModel`].
56///
57/// The single required method is [`files`](Self::files), which assembles the
58/// complete output set; pair it with [`impl_generator_via_backend!`](crate::impl_generator_via_backend) to wire
59/// the type into the [`Generator`](crate::codegen::Generator) trait the CLI and
60/// orchestrator consume. That alone gives every backend the shared driver, the
61/// [`OutputFile`] model (rendering is pure; the driver does the I/O), an
62/// automatically-derived `output_files`, and one uniform `Generator` bridge.
63///
64/// Backends whose primary file is a straightforward per-module walk override
65/// the per-entity hooks (`render_enum`, `render_struct`, `render_function`, and
66/// optionally `render_callback`/`render_listener`) and call the provided
67/// [`emit_members`](Self::emit_members) from inside their module scoping; that
68/// is what removes the hand-rolled walk + call-shape dispatch each generator
69/// used to carry. Multi-pass backends (Ruby, .NET, Node, Android) instead build
70/// their own layout directly in [`files`](Self::files) and leave the hooks at
71/// their no-op defaults.
72///
73/// Each hook renders into a `String` (matching how generators accumulate
74/// output) and is responsible for emitting its own doc comments; doc-comment
75/// shape varies too much between targets (docstrings, `///`, KDoc, `<summary>`)
76/// to centralise here, but every backend shares
77/// [`emit_doc`](crate::codegen::common::emit_doc) for the line/block flavours.
78pub trait LanguageBackend: Send + Sync {
79 /// Per-target, fully-typed configuration. Mirrors
80 /// [`Generator::Config`](crate::codegen::Generator::Config).
81 type Config: Serialize + Default + Clone + Send + Sync;
82
83 /// Stable short name (`"swift"`, `"python"`, …): the `--target` token.
84 fn name(&self) -> &'static str;
85
86 /// The gated IDL features this backend implements (async functions,
87 /// callbacks, listeners, iterators). Required: declaring capabilities
88 /// explicitly is what lets the orchestrator fail loudly instead of a
89 /// backend silently skipping a feature it never implemented.
90 fn capabilities(&self) -> TargetCapabilities;
91
92 /// Whether the bound config explicitly opted in to generating despite
93 /// unsupported features (see
94 /// [`Generator::allows_unsupported`](crate::codegen::Generator::allows_unsupported)).
95 /// Backends with partial capabilities override this to read their
96 /// `allow_unsupported` config flag; full-capability backends keep the
97 /// `false` default.
98 fn allows_unsupported(&self, config: &Self::Config) -> bool {
99 let _ = config;
100 false
101 }
102
103 /// The C ABI symbol prefix the producer used. The driver builds the
104 /// [`BindingModel`] with it so every emitted call targets the right
105 /// exported symbol. Defaults to `"weaveffi"`; override when the config
106 /// carries a configurable `c_prefix`.
107 fn prefix<'a>(&self, config: &'a Self::Config) -> &'a str {
108 let _ = config;
109 "weaveffi"
110 }
111
112 /// Render one enum (its declaration and any helpers), including doc
113 /// comments. Override when using [`emit_members`](Self::emit_members).
114 fn render_enum(&self, out: &mut String, e: &EnumBinding, config: &Self::Config) {
115 let _ = (out, e, config);
116 }
117
118 /// Render one struct: the wrapper type, its getters, lifecycle, and the
119 /// optional builder. `module` is the owning module (for symbol paths).
120 /// Override when using [`emit_members`](Self::emit_members).
121 fn render_struct(
122 &self,
123 out: &mut String,
124 module: &ModuleBinding,
125 s: &StructBinding,
126 config: &Self::Config,
127 ) {
128 let _ = (out, module, s, config);
129 }
130
131 /// Render a module-scope callback typedef. Default: no output (most idiomatic
132 /// backends express callbacks inline at the async/listener call site).
133 fn render_callback(
134 &self,
135 out: &mut String,
136 module: &ModuleBinding,
137 c: &CallbackBinding,
138 config: &Self::Config,
139 ) {
140 let _ = (out, module, c, config);
141 }
142
143 /// Render a listener's register/unregister surface. Default: no output.
144 fn render_listener(
145 &self,
146 out: &mut String,
147 module: &ModuleBinding,
148 l: &ListenerBinding,
149 config: &Self::Config,
150 ) {
151 let _ = (out, module, l, config);
152 }
153
154 /// Render one function. Implementations match on `f.shape` (sync / async /
155 /// iterator) and emit the idiomatic wrapper plus its doc comment. Override
156 /// when using [`emit_members`](Self::emit_members).
157 fn render_function(
158 &self,
159 out: &mut String,
160 module: &ModuleBinding,
161 f: &FnBinding,
162 config: &Self::Config,
163 ) {
164 let _ = (out, module, f, config);
165 }
166
167 /// Emit every member of `module` in canonical order (enums → structs →
168 /// callbacks → listeners → functions). Backends call this from within their
169 /// own module scoping; overriding the per-entity hooks is what guarantees a
170 /// single-pass backend cannot silently skip an entity kind.
171 fn emit_members(&self, out: &mut String, module: &ModuleBinding, config: &Self::Config) {
172 for e in &module.enums {
173 self.render_enum(out, e, config);
174 }
175 for s in &module.structs {
176 self.render_struct(out, module, s, config);
177 }
178 for c in &module.callbacks {
179 self.render_callback(out, module, c, config);
180 }
181 for l in &module.listeners {
182 self.render_listener(out, module, l, config);
183 }
184 for f in &module.functions {
185 self.render_function(out, module, f, config);
186 }
187 }
188
189 /// Assemble the complete output set. The driver has already built `model`
190 /// (via [`BindingModel::build`] with [`prefix`](Self::prefix)) and passes
191 /// the source `api` too, for the rare file (e.g. a `.pyi` stub) that needs
192 /// the raw IR. Most backends render a primary source file by composing
193 /// [`emit_members`](Self::emit_members) over `model.modules`, then append
194 /// package manifests (`package.json`, `pyproject.toml`, `go.mod`, …) as
195 /// additional [`OutputFile`]s.
196 fn files(
197 &self,
198 api: &Api,
199 model: &BindingModel,
200 out_dir: &Utf8Path,
201 config: &Self::Config,
202 ) -> Vec<OutputFile>;
203
204 /// Assemble a distributable package that bundles a prebuilt native library
205 /// for each platform in `ctx.binaries`, returning `None` when this target
206 /// does not support packaging yet.
207 ///
208 /// This is the `weaveffi package` analogue of [`files`](Self::files): it
209 /// returns [`PackagedFile`]s (rendered manifests, loaders, and binding
210 /// source as [`FileContent::Text`](crate::package::FileContent::Text), plus
211 /// the bundled libraries as
212 /// [`FileContent::Copy`](crate::package::FileContent::Copy)) anchored under
213 /// `out_dir`, and the [`write_package`](crate::package::write_package)
214 /// driver does the I/O. Override this to emit the ecosystem's idiomatic
215 /// per-platform layout (npm `optionalDependencies`, a NuGet `runtimes/`
216 /// tree, platform-tagged Python wheels, …). The default returns `None`.
217 fn package(
218 &self,
219 api: &Api,
220 model: &BindingModel,
221 ctx: &PackageContext,
222 out_dir: &Utf8Path,
223 config: &Self::Config,
224 ) -> Option<Vec<PackagedFile>> {
225 let _ = (api, model, ctx, out_dir, config);
226 None
227 }
228}
229
230/// Build the model and write every file a backend produces.
231///
232/// This is the body of the [`Generator::generate`](crate::codegen::Generator)
233/// impl that [`impl_generator_via_backend!`](crate::impl_generator_via_backend) generates.
234///
235/// # Errors
236///
237/// Returns an error if a parent directory cannot be created or any file the
238/// backend produced cannot be written.
239pub fn run<B: LanguageBackend>(
240 backend: &B,
241 api: &Api,
242 out_dir: &Utf8Path,
243 config: &B::Config,
244) -> Result<()> {
245 let model = BindingModel::build(api, backend.prefix(config));
246 for file in backend.files(api, &model, out_dir, config) {
247 if let Some(parent) = file.path.parent() {
248 std::fs::create_dir_all(parent.as_std_path())?;
249 }
250 std::fs::write(file.path.as_std_path(), file.contents)?;
251 }
252 Ok(())
253}
254
255/// Render a path for listing with `/` separators on every platform.
256///
257/// `Utf8Path::join` emits the platform separator, so on Windows a backend's
258/// `out_dir.join("c").join("weaveffi.h")` yields `c\weaveffi.h`. The listing
259/// surfaced by `--dry-run` and `weaveffi diff` (and asserted by the snapshot
260/// and unit suites) must be OS-independent, so fold `\` back to `/`. A no-op
261/// off Windows, where `\` is a legal filename byte we must not rewrite.
262fn forward_slashes(path: Utf8PathBuf) -> String {
263 let s = path.into_string();
264 if cfg!(windows) {
265 s.replace('\\', "/")
266 } else {
267 s
268 }
269}
270
271/// The sorted list of paths a backend would write, the body of the
272/// [`Generator::output_files`](crate::codegen::Generator::output_files) impl
273/// that [`impl_generator_via_backend!`](crate::impl_generator_via_backend) generates. Used by `--dry-run` and
274/// `weaveffi diff`. Paths are normalised to `/` separators so the listing is
275/// identical across operating systems.
276pub fn output_files<B: LanguageBackend>(
277 backend: &B,
278 api: &Api,
279 out_dir: &Utf8Path,
280 config: &B::Config,
281) -> Vec<String> {
282 let model = BindingModel::build(api, backend.prefix(config));
283 let mut paths: Vec<String> = backend
284 .files(api, &model, out_dir, config)
285 .into_iter()
286 .map(|f| forward_slashes(f.path))
287 .collect();
288 paths.sort();
289 paths
290}
291
292/// Build the model and assemble the package a backend produces, the body of
293/// the [`Generator::package`](crate::codegen::Generator::package) impl that
294/// [`impl_generator_via_backend!`](crate::impl_generator_via_backend)
295/// generates. Returns `None` when the backend does not support packaging.
296pub fn package_files<B: LanguageBackend>(
297 backend: &B,
298 api: &Api,
299 ctx: &PackageContext,
300 out_dir: &Utf8Path,
301 config: &B::Config,
302) -> Option<Vec<PackagedFile>> {
303 let model = BindingModel::build(api, backend.prefix(config));
304 backend.package(api, &model, ctx, out_dir, config)
305}
306
307/// Re-export of `anyhow` so [`impl_generator_via_backend!`](crate::impl_generator_via_backend)
308/// can name the `Generator::generate` return type in its expansion without
309/// forcing every backend crate to declare a direct `anyhow` dependency it never
310/// references in its own source. Not part of the public API.
311#[doc(hidden)]
312pub use anyhow as __anyhow;
313
314/// Implement the object-safe [`Generator`](crate::codegen::Generator) trait for
315/// a type that implements [`LanguageBackend`], delegating to the shared driver.
316///
317/// ```ignore
318/// pub struct PythonGenerator;
319/// impl weaveffi_core::backend::LanguageBackend for PythonGenerator { /* … */ }
320/// weaveffi_core::impl_generator_via_backend!(PythonGenerator);
321/// ```
322#[macro_export]
323macro_rules! impl_generator_via_backend {
324 ($backend:ty) => {
325 impl $crate::codegen::Generator for $backend {
326 type Config = <$backend as $crate::backend::LanguageBackend>::Config;
327
328 fn name(&self) -> &'static str {
329 <$backend as $crate::backend::LanguageBackend>::name(self)
330 }
331
332 fn capabilities(&self) -> $crate::capabilities::TargetCapabilities {
333 <$backend as $crate::backend::LanguageBackend>::capabilities(self)
334 }
335
336 fn allows_unsupported(&self, config: &Self::Config) -> bool {
337 <$backend as $crate::backend::LanguageBackend>::allows_unsupported(self, config)
338 }
339
340 fn generate(
341 &self,
342 api: &::weaveffi_ir::ir::Api,
343 out_dir: &::camino::Utf8Path,
344 config: &Self::Config,
345 ) -> $crate::backend::__anyhow::Result<()> {
346 $crate::backend::run(self, api, out_dir, config)
347 }
348
349 fn output_files(
350 &self,
351 api: &::weaveffi_ir::ir::Api,
352 out_dir: &::camino::Utf8Path,
353 config: &Self::Config,
354 ) -> ::std::vec::Vec<::std::string::String> {
355 $crate::backend::output_files(self, api, out_dir, config)
356 }
357
358 fn package(
359 &self,
360 api: &::weaveffi_ir::ir::Api,
361 ctx: &$crate::package::PackageContext,
362 out_dir: &::camino::Utf8Path,
363 config: &Self::Config,
364 ) -> ::core::option::Option<::std::vec::Vec<$crate::package::PackagedFile>> {
365 $crate::backend::package_files(self, api, ctx, out_dir, config)
366 }
367 }
368 };
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374 use crate::codegen::Generator;
375 use weaveffi_ir::ir::{Function, Module, Param, TypeRef};
376
377 #[derive(Default, Clone, serde::Serialize)]
378 struct FakeConfig {
379 prefix: Option<String>,
380 }
381
382 /// A trivial backend that records the canonical traversal order so we can
383 /// assert the driver walks and dispatches correctly.
384 struct FakeBackend;
385
386 impl LanguageBackend for FakeBackend {
387 type Config = FakeConfig;
388
389 fn name(&self) -> &'static str {
390 "fake"
391 }
392
393 fn capabilities(&self) -> TargetCapabilities {
394 TargetCapabilities::full()
395 }
396
397 fn prefix<'a>(&self, config: &'a Self::Config) -> &'a str {
398 config.prefix.as_deref().unwrap_or("weaveffi")
399 }
400
401 fn render_enum(&self, out: &mut String, e: &EnumBinding, _c: &Self::Config) {
402 out.push_str(&format!("enum {}\n", e.name));
403 }
404
405 fn render_struct(
406 &self,
407 out: &mut String,
408 _m: &ModuleBinding,
409 s: &StructBinding,
410 _c: &Self::Config,
411 ) {
412 out.push_str(&format!("struct {}\n", s.name));
413 }
414
415 fn render_function(
416 &self,
417 out: &mut String,
418 _m: &ModuleBinding,
419 f: &FnBinding,
420 _c: &Self::Config,
421 ) {
422 let shape = match &f.shape {
423 crate::model::CallShape::Sync(_) => "sync",
424 crate::model::CallShape::Async(_) => "async",
425 crate::model::CallShape::Iterator(_) => "iter",
426 };
427 out.push_str(&format!("fn {} [{}] {}\n", f.name, shape, f.c_base));
428 }
429
430 fn files(
431 &self,
432 _api: &Api,
433 model: &BindingModel,
434 out_dir: &Utf8Path,
435 config: &Self::Config,
436 ) -> Vec<OutputFile> {
437 let mut out = String::new();
438 for m in &model.modules {
439 out.push_str(&format!("module {}\n", m.path));
440 self.emit_members(&mut out, m, config);
441 }
442 vec![OutputFile::new(out_dir.join("fake/out.txt"), out)]
443 }
444 }
445
446 fn func(name: &str, returns: Option<TypeRef>, is_async: bool) -> Function {
447 Function {
448 name: name.into(),
449 params: vec![Param {
450 name: "x".into(),
451 ty: TypeRef::I32,
452 mutable: false,
453 doc: None,
454 }],
455 returns,
456 doc: None,
457 r#async: is_async,
458 cancellable: false,
459 deprecated: None,
460 since: None,
461 }
462 }
463
464 fn api() -> Api {
465 Api {
466 version: "0.4.0".into(),
467 modules: vec![Module {
468 name: "math".into(),
469 functions: vec![
470 func("add", Some(TypeRef::I32), false),
471 func("fetch", Some(TypeRef::StringUtf8), true),
472 ],
473 structs: vec![],
474 enums: vec![],
475 callbacks: vec![],
476 listeners: vec![],
477 errors: None,
478 modules: vec![],
479 }],
480 generators: None,
481 package: None,
482 }
483 }
484
485 #[test]
486 fn driver_walks_and_dispatches_in_canonical_order() {
487 let dir = tempfile::tempdir().unwrap();
488 let out_dir = Utf8Path::from_path(dir.path()).unwrap();
489 run(&FakeBackend, &api(), out_dir, &FakeConfig::default()).unwrap();
490 let body = std::fs::read_to_string(out_dir.join("fake/out.txt")).unwrap();
491 assert_eq!(
492 body,
493 "module math\nfn add [sync] weaveffi_math_add\nfn fetch [async] weaveffi_math_fetch\n"
494 );
495 }
496
497 #[test]
498 fn prefix_flows_into_symbols() {
499 let dir = tempfile::tempdir().unwrap();
500 let out_dir = Utf8Path::from_path(dir.path()).unwrap();
501 let cfg = FakeConfig {
502 prefix: Some("acme".into()),
503 };
504 run(&FakeBackend, &api(), out_dir, &cfg).unwrap();
505 let body = std::fs::read_to_string(out_dir.join("fake/out.txt")).unwrap();
506 assert!(
507 body.contains("acme_math_add"),
508 "prefix must reach symbols: {body}"
509 );
510 assert!(!body.contains("weaveffi_math_add"));
511 }
512
513 #[test]
514 fn output_files_are_sorted_paths() {
515 let dir = tempfile::tempdir().unwrap();
516 let out_dir = Utf8Path::from_path(dir.path()).unwrap();
517 let files = output_files(&FakeBackend, &api(), out_dir, &FakeConfig::default());
518 assert_eq!(files.len(), 1);
519 assert!(files[0].ends_with("fake/out.txt"));
520 }
521
522 // Exercise the generated Generator impl.
523 impl_generator_via_backend!(FakeBackend);
524
525 #[test]
526 fn generator_bridge_delegates_to_driver() {
527 let dir = tempfile::tempdir().unwrap();
528 let out_dir = Utf8Path::from_path(dir.path()).unwrap();
529 let g = FakeBackend;
530 Generator::generate(&g, &api(), out_dir, &FakeConfig::default()).unwrap();
531 assert!(out_dir.join("fake/out.txt").exists());
532 let listed = Generator::output_files(&g, &api(), out_dir, &FakeConfig::default());
533 assert_eq!(listed.len(), 1);
534 }
535}