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