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, ErrorBinding, FnBinding, InterfaceBinding,
30 ListenerBinding, ModuleBinding, 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 the typed error surface for a module that *declares* an error
132 /// domain: the target's error enum/class hierarchy mapping each
133 /// [`ErrorBinding`] code to a case the consumer can match on. Override when
134 /// using [`emit_members`](Self::emit_members); inheriting modules reference
135 /// the ancestor's type, so this hook only fires where
136 /// [`ModuleBinding::declares_error`] is true.
137 fn render_error(
138 &self,
139 out: &mut String,
140 module: &ModuleBinding,
141 e: &ErrorBinding,
142 config: &Self::Config,
143 ) {
144 let _ = (out, module, e, config);
145 }
146
147 /// Render one interface: the wrapper class with its constructors, methods,
148 /// statics, and destructor wiring. Override when using
149 /// [`emit_members`](Self::emit_members).
150 fn render_interface(
151 &self,
152 out: &mut String,
153 module: &ModuleBinding,
154 i: &InterfaceBinding,
155 config: &Self::Config,
156 ) {
157 let _ = (out, module, i, config);
158 }
159
160 /// Render a module-scope callback typedef. Default: no output (most idiomatic
161 /// backends express callbacks inline at the async/listener call site).
162 fn render_callback(
163 &self,
164 out: &mut String,
165 module: &ModuleBinding,
166 c: &CallbackBinding,
167 config: &Self::Config,
168 ) {
169 let _ = (out, module, c, config);
170 }
171
172 /// Render a listener's register/unregister surface. Default: no output.
173 fn render_listener(
174 &self,
175 out: &mut String,
176 module: &ModuleBinding,
177 l: &ListenerBinding,
178 config: &Self::Config,
179 ) {
180 let _ = (out, module, l, config);
181 }
182
183 /// Render one function. Implementations match on `f.shape` (sync / async /
184 /// iterator) and emit the idiomatic wrapper plus its doc comment. Override
185 /// when using [`emit_members`](Self::emit_members).
186 fn render_function(
187 &self,
188 out: &mut String,
189 module: &ModuleBinding,
190 f: &FnBinding,
191 config: &Self::Config,
192 ) {
193 let _ = (out, module, f, config);
194 }
195
196 /// Emit every member of `module` in canonical order (error domain → enums
197 /// → structs → interfaces → callbacks → listeners → functions). Backends
198 /// call this from within their own module scoping; overriding the
199 /// per-entity hooks is what guarantees a single-pass backend cannot
200 /// silently skip an entity kind.
201 fn emit_members(&self, out: &mut String, module: &ModuleBinding, config: &Self::Config) {
202 if let Some(e) = module.error.as_ref().filter(|e| e.declared_here) {
203 self.render_error(out, module, e, config);
204 }
205 for e in &module.enums {
206 self.render_enum(out, e, config);
207 }
208 for s in &module.structs {
209 self.render_struct(out, module, s, config);
210 }
211 for i in &module.interfaces {
212 self.render_interface(out, module, i, config);
213 }
214 for c in &module.callbacks {
215 self.render_callback(out, module, c, config);
216 }
217 for l in &module.listeners {
218 self.render_listener(out, module, l, config);
219 }
220 for f in &module.functions {
221 self.render_function(out, module, f, config);
222 }
223 }
224
225 /// Assemble the complete output set. The driver has already built `model`
226 /// (via [`BindingModel::build`] with [`prefix`](Self::prefix)) and passes
227 /// the source `api` too, for the rare file (e.g. a `.pyi` stub) that needs
228 /// the raw IR. Most backends render a primary source file by composing
229 /// [`emit_members`](Self::emit_members) over `model.modules`, then append
230 /// package manifests (`package.json`, `pyproject.toml`, `go.mod`, …) as
231 /// additional [`OutputFile`]s.
232 fn files(
233 &self,
234 api: &Api,
235 model: &BindingModel,
236 out_dir: &Utf8Path,
237 config: &Self::Config,
238 ) -> Vec<OutputFile>;
239
240 /// Assemble a distributable package that bundles a prebuilt native library
241 /// for each platform in `ctx.binaries`, returning `None` when this target
242 /// does not support packaging yet.
243 ///
244 /// This is the `weaveffi package` analogue of [`files`](Self::files): it
245 /// returns [`PackagedFile`]s (rendered manifests, loaders, and binding
246 /// source as [`FileContent::Text`](crate::package::FileContent::Text), plus
247 /// the bundled libraries as
248 /// [`FileContent::Copy`](crate::package::FileContent::Copy)) anchored under
249 /// `out_dir`, and the [`write_package`](crate::package::write_package)
250 /// driver does the I/O. Override this to emit the ecosystem's idiomatic
251 /// per-platform layout (npm `optionalDependencies`, a NuGet `runtimes/`
252 /// tree, platform-tagged Python wheels, …). The default returns `None`.
253 fn package(
254 &self,
255 api: &Api,
256 model: &BindingModel,
257 ctx: &PackageContext,
258 out_dir: &Utf8Path,
259 config: &Self::Config,
260 ) -> Option<Vec<PackagedFile>> {
261 let _ = (api, model, ctx, out_dir, config);
262 None
263 }
264}
265
266/// Build the model and write every file a backend produces.
267///
268/// This is the body of the [`Generator::generate`](crate::codegen::Generator)
269/// impl that [`impl_generator_via_backend!`](crate::impl_generator_via_backend) generates.
270///
271/// # Errors
272///
273/// Returns an error if a parent directory cannot be created or any file the
274/// backend produced cannot be written.
275pub fn run<B: LanguageBackend>(
276 backend: &B,
277 api: &Api,
278 out_dir: &Utf8Path,
279 config: &B::Config,
280) -> Result<()> {
281 let model = BindingModel::build(api, backend.prefix(config));
282 for file in backend.files(api, &model, out_dir, config) {
283 if let Some(parent) = file.path.parent() {
284 std::fs::create_dir_all(parent.as_std_path())?;
285 }
286 std::fs::write(file.path.as_std_path(), file.contents)?;
287 }
288 Ok(())
289}
290
291/// Render a path for listing with `/` separators on every platform.
292///
293/// `Utf8Path::join` emits the platform separator, so on Windows a backend's
294/// `out_dir.join("c").join("weaveffi.h")` yields `c\weaveffi.h`. The listing
295/// surfaced by `--dry-run` and `weaveffi diff` (and asserted by the snapshot
296/// and unit suites) must be OS-independent, so fold `\` back to `/`. A no-op
297/// off Windows, where `\` is a legal filename byte we must not rewrite.
298fn forward_slashes(path: Utf8PathBuf) -> String {
299 let s = path.into_string();
300 if cfg!(windows) {
301 s.replace('\\', "/")
302 } else {
303 s
304 }
305}
306
307/// The sorted list of paths a backend would write, the body of the
308/// [`Generator::output_files`](crate::codegen::Generator::output_files) impl
309/// that [`impl_generator_via_backend!`](crate::impl_generator_via_backend) generates. Used by `--dry-run` and
310/// `weaveffi diff`. Paths are normalised to `/` separators so the listing is
311/// identical across operating systems.
312pub fn output_files<B: LanguageBackend>(
313 backend: &B,
314 api: &Api,
315 out_dir: &Utf8Path,
316 config: &B::Config,
317) -> Vec<String> {
318 let model = BindingModel::build(api, backend.prefix(config));
319 let mut paths: Vec<String> = backend
320 .files(api, &model, out_dir, config)
321 .into_iter()
322 .map(|f| forward_slashes(f.path))
323 .collect();
324 paths.sort();
325 paths
326}
327
328/// Build the model and assemble the package a backend produces, the body of
329/// the [`Generator::package`](crate::codegen::Generator::package) impl that
330/// [`impl_generator_via_backend!`](crate::impl_generator_via_backend)
331/// generates. Returns `None` when the backend does not support packaging.
332pub fn package_files<B: LanguageBackend>(
333 backend: &B,
334 api: &Api,
335 ctx: &PackageContext,
336 out_dir: &Utf8Path,
337 config: &B::Config,
338) -> Option<Vec<PackagedFile>> {
339 let model = BindingModel::build(api, backend.prefix(config));
340 backend.package(api, &model, ctx, out_dir, config)
341}
342
343/// Re-export of `anyhow` so [`impl_generator_via_backend!`](crate::impl_generator_via_backend)
344/// can name the `Generator::generate` return type in its expansion without
345/// forcing every backend crate to declare a direct `anyhow` dependency it never
346/// references in its own source. Not part of the public API.
347#[doc(hidden)]
348pub use anyhow as __anyhow;
349
350/// Implement the object-safe [`Generator`](crate::codegen::Generator) trait for
351/// a type that implements [`LanguageBackend`], delegating to the shared driver.
352///
353/// ```ignore
354/// pub struct PythonGenerator;
355/// impl weaveffi_core::backend::LanguageBackend for PythonGenerator { /* … */ }
356/// weaveffi_core::impl_generator_via_backend!(PythonGenerator);
357/// ```
358#[macro_export]
359macro_rules! impl_generator_via_backend {
360 ($backend:ty) => {
361 impl $crate::codegen::Generator for $backend {
362 type Config = <$backend as $crate::backend::LanguageBackend>::Config;
363
364 fn name(&self) -> &'static str {
365 <$backend as $crate::backend::LanguageBackend>::name(self)
366 }
367
368 fn capabilities(&self) -> $crate::capabilities::TargetCapabilities {
369 <$backend as $crate::backend::LanguageBackend>::capabilities(self)
370 }
371
372 fn allows_unsupported(&self, config: &Self::Config) -> bool {
373 <$backend as $crate::backend::LanguageBackend>::allows_unsupported(self, config)
374 }
375
376 fn generate(
377 &self,
378 api: &::weaveffi_ir::ir::Api,
379 out_dir: &::camino::Utf8Path,
380 config: &Self::Config,
381 ) -> $crate::backend::__anyhow::Result<()> {
382 $crate::backend::run(self, api, out_dir, config)
383 }
384
385 fn output_files(
386 &self,
387 api: &::weaveffi_ir::ir::Api,
388 out_dir: &::camino::Utf8Path,
389 config: &Self::Config,
390 ) -> ::std::vec::Vec<::std::string::String> {
391 $crate::backend::output_files(self, api, out_dir, config)
392 }
393
394 fn package(
395 &self,
396 api: &::weaveffi_ir::ir::Api,
397 ctx: &$crate::package::PackageContext,
398 out_dir: &::camino::Utf8Path,
399 config: &Self::Config,
400 ) -> ::core::option::Option<::std::vec::Vec<$crate::package::PackagedFile>> {
401 $crate::backend::package_files(self, api, ctx, out_dir, config)
402 }
403 }
404 };
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
410 use crate::codegen::Generator;
411 use weaveffi_ir::ir::{Function, Module, Param, TypeRef};
412
413 #[derive(Default, Clone, serde::Serialize)]
414 struct FakeConfig {
415 prefix: Option<String>,
416 }
417
418 /// A trivial backend that records the canonical traversal order so we can
419 /// assert the driver walks and dispatches correctly.
420 struct FakeBackend;
421
422 impl LanguageBackend for FakeBackend {
423 type Config = FakeConfig;
424
425 fn name(&self) -> &'static str {
426 "fake"
427 }
428
429 fn capabilities(&self) -> TargetCapabilities {
430 TargetCapabilities::full()
431 }
432
433 fn prefix<'a>(&self, config: &'a Self::Config) -> &'a str {
434 config.prefix.as_deref().unwrap_or("weaveffi")
435 }
436
437 fn render_enum(&self, out: &mut String, e: &EnumBinding, _c: &Self::Config) {
438 out.push_str(&format!("enum {}\n", e.name));
439 }
440
441 fn render_struct(
442 &self,
443 out: &mut String,
444 _m: &ModuleBinding,
445 s: &StructBinding,
446 _c: &Self::Config,
447 ) {
448 out.push_str(&format!("struct {}\n", s.name));
449 }
450
451 fn render_function(
452 &self,
453 out: &mut String,
454 _m: &ModuleBinding,
455 f: &FnBinding,
456 _c: &Self::Config,
457 ) {
458 let shape = match &f.shape {
459 crate::model::CallShape::Sync(_) => "sync",
460 crate::model::CallShape::Async(_) => "async",
461 crate::model::CallShape::Iterator(_) => "iter",
462 };
463 out.push_str(&format!("fn {} [{}] {}\n", f.name, shape, f.c_base));
464 }
465
466 fn files(
467 &self,
468 _api: &Api,
469 model: &BindingModel,
470 out_dir: &Utf8Path,
471 config: &Self::Config,
472 ) -> Vec<OutputFile> {
473 let mut out = String::new();
474 for m in &model.modules {
475 out.push_str(&format!("module {}\n", m.path));
476 self.emit_members(&mut out, m, config);
477 }
478 vec![OutputFile::new(out_dir.join("fake/out.txt"), out)]
479 }
480 }
481
482 fn func(name: &str, returns: Option<TypeRef>, is_async: bool) -> Function {
483 Function {
484 name: name.into(),
485 params: vec![Param {
486 name: "x".into(),
487 ty: TypeRef::I32,
488 mutable: false,
489 doc: None,
490 }],
491 returns,
492 doc: None,
493 throws: false,
494 r#async: is_async,
495 cancellable: false,
496 deprecated: None,
497 since: None,
498 }
499 }
500
501 fn api() -> Api {
502 Api {
503 version: "0.5.0".into(),
504 modules: vec![Module {
505 name: "math".into(),
506 functions: vec![
507 func("add", Some(TypeRef::I32), false),
508 func("fetch", Some(TypeRef::StringUtf8), true),
509 ],
510 interfaces: vec![],
511 structs: vec![],
512 enums: vec![],
513 callbacks: vec![],
514 listeners: vec![],
515 errors: None,
516 modules: vec![],
517 }],
518 generators: None,
519 package: None,
520 }
521 }
522
523 #[test]
524 fn driver_walks_and_dispatches_in_canonical_order() {
525 let dir = tempfile::tempdir().unwrap();
526 let out_dir = Utf8Path::from_path(dir.path()).unwrap();
527 run(&FakeBackend, &api(), out_dir, &FakeConfig::default()).unwrap();
528 let body = std::fs::read_to_string(out_dir.join("fake/out.txt")).unwrap();
529 assert_eq!(
530 body,
531 "module math\nfn add [sync] weaveffi_math_add\nfn fetch [async] weaveffi_math_fetch\n"
532 );
533 }
534
535 #[test]
536 fn prefix_flows_into_symbols() {
537 let dir = tempfile::tempdir().unwrap();
538 let out_dir = Utf8Path::from_path(dir.path()).unwrap();
539 let cfg = FakeConfig {
540 prefix: Some("acme".into()),
541 };
542 run(&FakeBackend, &api(), out_dir, &cfg).unwrap();
543 let body = std::fs::read_to_string(out_dir.join("fake/out.txt")).unwrap();
544 assert!(
545 body.contains("acme_math_add"),
546 "prefix must reach symbols: {body}"
547 );
548 assert!(!body.contains("weaveffi_math_add"));
549 }
550
551 #[test]
552 fn output_files_are_sorted_paths() {
553 let dir = tempfile::tempdir().unwrap();
554 let out_dir = Utf8Path::from_path(dir.path()).unwrap();
555 let files = output_files(&FakeBackend, &api(), out_dir, &FakeConfig::default());
556 assert_eq!(files.len(), 1);
557 assert!(files[0].ends_with("fake/out.txt"));
558 }
559
560 // Exercise the generated Generator impl.
561 impl_generator_via_backend!(FakeBackend);
562
563 #[test]
564 fn generator_bridge_delegates_to_driver() {
565 let dir = tempfile::tempdir().unwrap();
566 let out_dir = Utf8Path::from_path(dir.path()).unwrap();
567 let g = FakeBackend;
568 Generator::generate(&g, &api(), out_dir, &FakeConfig::default()).unwrap();
569 assert!(out_dir.join("fake/out.txt").exists());
570 let listed = Generator::output_files(&g, &api(), out_dir, &FakeConfig::default());
571 assert_eq!(listed.len(), 1);
572 }
573}