Skip to main content

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