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; 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}