Skip to main content

javy_codegen/
lib.rs

1//! WebAssembly Code Generation for JavaScript
2//!
3//! This module provides functionality to emit Wasm modules which will run
4//! JavaScript source code with the QuickJS interpreter.
5//!
6//! Javy supports two main code generation paths:
7//!
8//! 1. Static code generation
9//! 2. Dynamic code generation
10//!
11//! ## Static code generation
12//!
13//! A single unit of code is generated, which is a Wasm module consisting of the
14//! bytecode representation of a given JavaScript program and the code for
15//! a particular version of the QuickJS engine compiled to Wasm.
16//!
17//! The generated Wasm module is self contained and the bytecode version matches
18//! the exact requirements of the embedded QuickJs engine. Use
19//! [`Generator::deterministic`] for reproducible builds (e.g., for verification
20//! or caching).
21//!
22//! ## Dynamic code generation
23//!
24//! A single unit of code is generated, which is a Wasm module consisting of the
25//! bytecode representation of a given JavaScript program. The JavaScript
26//! bytecode is stored as part of the data section of the module which also
27//! contains instructions to execute that bytecode through dynamic linking
28//! at runtime.
29//!
30//! Dynamic code generation requires a plugin module to be used and linked
31//! against at runtime in order to execute the JavaScript bytecode. This
32//! operation involves carefully ensuring that a given plugin version matches
33//! the plugin version of the imports requested by the generated Wasm module
34//! as well as ensuring that any features available in the plugin match the
35//! features requsted by the JavaScript bytecode.
36//!
37//! ## Examples
38//!
39//! Simple Wasm module generation:
40//!
41//! ```no_run
42//! use std::path::Path;
43//! use javy_codegen::{Generator, LinkingKind, Plugin, JS};
44//!
45//! #[tokio::main]
46//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
47//!     // Load your target Javascript.
48//!     let js = JS::from_file(Path::new("example.js"))?;
49//!
50//!     // Load existing pre-initialized Javy plugin.
51//!     let plugin = Plugin::new_from_path(Path::new("example-plugin.wasm"))?;
52//!
53//!     // Configure code generator.
54//!     let mut generator = Generator::new(plugin);
55//!     generator.linking(LinkingKind::Static);
56//!
57//!     // Generate your Wasm module.
58//!     let wasm = generator.generate(&js).await?;
59//!
60//!     Ok(())
61//! }
62//! ```
63//!
64//! ## Core concepts
65//! * [`Generator`] - The main entry point for generating Wasm modules.
66//! * [`Plugin`] - An initialized Javy plugin.
67//! * [`JS`] - JavaScript source code.
68//!
69//! ## Features
70//!
71//! * `plugin_internal` - Enables additional code generation options for
72//!   internal use. Please note that this flag enables an unstable feature. The
73//!   unstable API's exposed by this future may break in the future without
74//!   notice.
75
76use std::fs;
77
78pub(crate) mod bytecode;
79pub(crate) mod exports;
80pub(crate) mod transform;
81
82pub(crate) mod js;
83pub(crate) mod plugin;
84pub(crate) mod wit;
85
86use crate::exports::Exports;
87pub use crate::js::JS;
88pub use crate::plugin::Plugin;
89pub use crate::wit::WitOptions;
90
91use transform::SourceCodeSection;
92use walrus::{
93    DataId, DataKind, ExportItem, FunctionBuilder, FunctionId, LocalId, MemoryId, Module, ValType,
94};
95use wasm_opt::{OptimizationOptions, ShrinkLevel};
96use wasmtime::{Engine, Linker, Store};
97use wasmtime_wasi::{WasiCtxBuilder, p2::pipe::MemoryInputPipe};
98
99use anyhow::Result;
100use wasmtime_wizer::Wizer;
101
102/// The kind of linking to use.
103#[derive(Debug, Clone, Default)]
104pub enum LinkingKind {
105    #[default]
106    /// Static linking
107    Static,
108    /// Dynamic linking
109    Dynamic,
110}
111
112/// Source code embedding options for the generated Wasm module.
113#[derive(Debug, Clone, Default)]
114pub enum SourceEmbedding {
115    #[default]
116    /// Embed the source code without compression.
117    Uncompressed,
118    /// Embed the source code with compression.
119    Compressed,
120    /// Don't embed the source code.
121    Omitted,
122}
123
124/// Identifiers used by the generated module.
125// This is an internal detail of this module.
126#[derive(Debug)]
127pub(crate) struct Identifiers {
128    cabi_realloc: FunctionId,
129    invoke: FunctionId,
130    memory: MemoryId,
131}
132
133impl Identifiers {
134    fn new(cabi_realloc: FunctionId, invoke: FunctionId, memory: MemoryId) -> Self {
135        Self {
136            cabi_realloc,
137            invoke,
138            memory,
139        }
140    }
141}
142
143/// Helper struct to keep track of bytecode metadata.
144// This is an internal detail of this module.
145#[derive(Debug)]
146pub(crate) struct BytecodeMetadata {
147    ptr: LocalId,
148    len: i32,
149    data_section: DataId,
150}
151
152impl BytecodeMetadata {
153    fn new(ptr: LocalId, len: i32, data_section: DataId) -> Self {
154        Self {
155            ptr,
156            len,
157            data_section,
158        }
159    }
160}
161
162/// Generator used to produce Wasm binaries from JS source code.
163#[derive(Debug, Default, Clone)]
164pub struct Generator {
165    /// Plugin to use.
166    pub(crate) plugin: Plugin,
167    /// What kind of linking to use when generating a module.
168    pub(crate) linking: LinkingKind,
169    /// Source code embedding option for the generated module.
170    pub(crate) source_embedding: SourceEmbedding,
171    /// WIT options for code generation.
172    pub(crate) wit_opts: WitOptions,
173    /// JavaScript function exports.
174    pub(crate) function_exports: Exports,
175    /// An optional JS runtime config provided as JSON bytes.
176    js_runtime_config: Vec<u8>,
177    /// The version string to include in the producers custom section.
178    producer_version: Option<String>,
179    /// Whether to use fixed clocks for deterministic builds.
180    deterministic: bool,
181}
182
183impl Generator {
184    /// Create a new [`Generator`].
185    pub fn new(plugin: Plugin) -> Self {
186        Self {
187            plugin,
188            ..Self::default()
189        }
190    }
191
192    /// Set the kind of linking (default: [`LinkingKind::Static`])
193    pub fn linking(&mut self, linking: LinkingKind) -> &mut Self {
194        self.linking = linking;
195        self
196    }
197
198    /// Set the source embedding option (default: [`SourceEmbedding::Compressed`])
199    pub fn source_embedding(&mut self, source_embedding: SourceEmbedding) -> &mut Self {
200        self.source_embedding = source_embedding;
201        self
202    }
203
204    /// Set the wit options. (default: Empty [`WitOptions`])
205    pub fn wit_opts(&mut self, wit_opts: wit::WitOptions) -> &mut Self {
206        self.wit_opts = wit_opts;
207        self
208    }
209
210    #[cfg(feature = "plugin_internal")]
211    /// Set the JS runtime configuration options to pass to the module.
212    pub fn js_runtime_config(&mut self, js_runtime_config: Vec<u8>) -> &mut Self {
213        self.js_runtime_config = js_runtime_config;
214        self
215    }
216
217    /// Sets the version string to use in the producers custom section.
218    pub fn producer_version(&mut self, producer_version: String) -> &mut Self {
219        self.producer_version = Some(producer_version);
220        self
221    }
222
223    /// Enable deterministic builds by using fixed clocks during Wizer
224    /// pre-initialization, ensuring identical output for identical input.
225    pub fn deterministic(&mut self, deterministic: bool) -> &mut Self {
226        self.deterministic = deterministic;
227        self
228    }
229}
230
231impl Generator {
232    /// Generate the starting module.
233    async fn generate_initial_module(&self) -> Result<Module> {
234        let config = transform::module_config();
235        let module = match &self.linking {
236            LinkingKind::Static => {
237                let engine = Engine::default();
238                let mut builder = WasiCtxBuilder::new();
239                builder
240                    .stdin(MemoryInputPipe::new(self.js_runtime_config.clone()))
241                    .inherit_stdout()
242                    .inherit_stderr();
243                if self.deterministic {
244                    deterministic_wasi_ctx::add_determinism_to_wasi_ctx_builder(&mut builder);
245                }
246                let wasi = builder.build_p1();
247                let mut store = Store::new(&engine, wasi);
248                let wasm = Wizer::new()
249                    .init_func("initialize-runtime")
250                    .run(&mut store, self.plugin.as_bytes(), async |store, module| {
251                        let engine = store.engine();
252                        let mut linker = Linker::new(engine);
253                        wasmtime_wasi::p1::add_to_linker_async(&mut linker, |cx| cx)?;
254                        linker.define_unknown_imports_as_traps(module)?;
255                        let instance = linker.instantiate_async(store, module).await?;
256                        Ok(instance)
257                    })
258                    .await?;
259                config.parse(&wasm)?
260            }
261            LinkingKind::Dynamic => Module::with_config(config),
262        };
263        Ok(module)
264    }
265
266    /// Resolve identifiers for functions and memory.
267    pub(crate) fn resolve_identifiers(&self, module: &mut Module) -> Result<Identifiers> {
268        match self.linking {
269            LinkingKind::Static => {
270                let cabi_realloc = module.exports.get_func("cabi_realloc")?;
271                let invoke = module.exports.get_func("invoke")?;
272                let ExportItem::Memory(memory) = module
273                    .exports
274                    .iter()
275                    .find(|e| e.name == "memory")
276                    .ok_or_else(|| anyhow::anyhow!("Missing memory export"))?
277                    .item
278                else {
279                    anyhow::bail!("Export with name memory must be of type memory")
280                };
281                Ok(Identifiers::new(cabi_realloc, invoke, memory))
282            }
283            LinkingKind::Dynamic => {
284                // All code by default is assumed to be linking against a default
285                // or a user provided plugin.
286                let import_namespace = self.plugin.import_namespace()?;
287
288                let cabi_realloc_type = module.types.add(
289                    &[ValType::I32, ValType::I32, ValType::I32, ValType::I32],
290                    &[ValType::I32],
291                );
292                let (cabi_realloc_fn_id, _) =
293                    module.add_import_func(&import_namespace, "cabi_realloc", cabi_realloc_type);
294
295                let invoke_params = [
296                    ValType::I32,
297                    ValType::I32,
298                    ValType::I32,
299                    ValType::I32,
300                    ValType::I32,
301                ]
302                .as_slice();
303                let invoke_type = module.types.add(invoke_params, &[]);
304                let (invoke_fn_id, _) =
305                    module.add_import_func(&import_namespace, "invoke", invoke_type);
306
307                let (memory_id, _) = module.add_import_memory(
308                    &import_namespace,
309                    "memory",
310                    false,
311                    false,
312                    0,
313                    None,
314                    None,
315                );
316
317                Ok(Identifiers::new(
318                    cabi_realloc_fn_id,
319                    invoke_fn_id,
320                    memory_id,
321                ))
322            }
323        }
324    }
325
326    /// Generate the main function.
327    fn generate_main(
328        &self,
329        module: &mut Module,
330        js: &js::JS,
331        imports: &Identifiers,
332    ) -> Result<BytecodeMetadata> {
333        let bytecode = bytecode::compile_source(&self.plugin, js.as_bytes())?;
334        let bytecode_len: i32 = bytecode.len().try_into()?;
335        let bytecode_data = module.data.add(DataKind::Passive, bytecode);
336
337        let mut main = FunctionBuilder::new(&mut module.types, &[], &[]);
338        let bytecode_ptr_local = module.locals.add(ValType::I32);
339        let mut instructions = main.func_body();
340        instructions
341            // Allocate memory in plugin instance for bytecode array.
342            .i32_const(0) // orig ptr
343            .i32_const(0) // orig size
344            .i32_const(1) // alignment
345            .i32_const(bytecode_len) // new size
346            .call(imports.cabi_realloc)
347            // Copy bytecode array into allocated memory.
348            .local_tee(bytecode_ptr_local) // save returned address to local and set as dest addr for mem.init
349            .i32_const(0) // offset into data segment for mem.init
350            .i32_const(bytecode_len) // size to copy from data segment
351            // top-2: dest addr, top-1: offset into source, top-0: size of memory region in bytes.
352            .memory_init(imports.memory, bytecode_data);
353        // Evaluate top level scope.
354        instructions
355            .local_get(bytecode_ptr_local) // ptr to bytecode
356            .i32_const(bytecode_len)
357            .i32_const(0) // set option discriminator to none
358            .i32_const(0) // set function name ptr to null
359            .i32_const(0) // set function name len to 0
360            .call(imports.invoke);
361        let main = main.finish(vec![], &mut module.funcs);
362
363        module.exports.add("_start", main);
364        Ok(BytecodeMetadata::new(
365            bytecode_ptr_local,
366            bytecode_len,
367            bytecode_data,
368        ))
369    }
370
371    /// Generate function exports.
372    fn generate_exports(
373        &self,
374        module: &mut Module,
375        identifiers: &Identifiers,
376        bc_metadata: &BytecodeMetadata,
377    ) -> Result<()> {
378        if !self.function_exports.is_empty() {
379            let fn_name_ptr_local = module.locals.add(ValType::I32);
380            for export in &self.function_exports {
381                // For each JS function export, add an export that copies the name of the function into memory and invokes it.
382                let js_export_bytes = export.js.as_bytes();
383                let js_export_len: i32 = js_export_bytes.len().try_into().unwrap();
384                let fn_name_data = module.data.add(DataKind::Passive, js_export_bytes.to_vec());
385
386                let mut export_fn = FunctionBuilder::new(&mut module.types, &[], &[]);
387                export_fn
388                    .func_body()
389                    // Copy bytecode.
390                    .i32_const(0) // orig ptr
391                    .i32_const(0) // orig len
392                    .i32_const(1) // alignment
393                    .i32_const(bc_metadata.len) // size to copy
394                    .call(identifiers.cabi_realloc)
395                    .local_tee(bc_metadata.ptr)
396                    .i32_const(0) // offset into data segment
397                    .i32_const(bc_metadata.len) // size to copy
398                    .memory_init(identifiers.memory, bc_metadata.data_section) // copy bytecode into allocated memory
399                    .data_drop(bc_metadata.data_section)
400                    // Copy function name.
401                    .i32_const(0) // orig ptr
402                    .i32_const(0) // orig len
403                    .i32_const(1) // alignment
404                    .i32_const(js_export_len) // new size
405                    .call(identifiers.cabi_realloc)
406                    .local_tee(fn_name_ptr_local)
407                    .i32_const(0) // offset into data segment
408                    .i32_const(js_export_len) // size to copy
409                    .memory_init(identifiers.memory, fn_name_data) // copy fn name into allocated memory
410                    .data_drop(fn_name_data)
411                    // Call invoke.
412                    .local_get(bc_metadata.ptr)
413                    .i32_const(bc_metadata.len)
414                    .i32_const(1) // set function name option discriminator to some
415                    .local_get(fn_name_ptr_local)
416                    .i32_const(js_export_len)
417                    .call(identifiers.invoke);
418                let export_fn = export_fn.finish(vec![], &mut module.funcs);
419                module.exports.add(&export.wit, export_fn);
420            }
421        }
422        Ok(())
423    }
424
425    /// Clean-up the generated Wasm.
426    fn postprocess(&self, module: &mut Module) -> Result<Vec<u8>> {
427        match self.linking {
428            LinkingKind::Static => {
429                // Remove no longer necessary exports.
430                module.exports.remove("invoke")?;
431                module.exports.remove("compile-src")?;
432
433                // Run wasm-opt to optimize.
434                let tempdir = tempfile::tempdir()?;
435                let tempfile_path = tempdir.path().join("temp.wasm");
436
437                module.emit_wasm_file(&tempfile_path)?;
438
439                OptimizationOptions::new_opt_level_3() // Aggressively optimize for speed.
440                    .shrink_level(ShrinkLevel::Level0) // Don't optimize for size at the expense of performance.
441                    .debug_info(false)
442                    .run(&tempfile_path, &tempfile_path)?;
443
444                Ok(fs::read(&tempfile_path)?)
445            }
446            LinkingKind::Dynamic => Ok(module.emit_wasm()),
447        }
448    }
449
450    /// Generate a Wasm module which will run the provided JS source code.
451    pub async fn generate(&mut self, js: &js::JS) -> Result<Vec<u8>> {
452        if self.wit_opts.defined() {
453            self.function_exports = exports::process_exports(
454                js,
455                self.wit_opts.unwrap_path(),
456                self.wit_opts.unwrap_world(),
457            )?;
458        }
459
460        let mut module = self.generate_initial_module().await?;
461        let identifiers = self.resolve_identifiers(&mut module)?;
462        let bc_metadata = self.generate_main(&mut module, js, &identifiers)?;
463        self.generate_exports(&mut module, &identifiers, &bc_metadata)?;
464
465        transform::add_producers_section(
466            &mut module.producers,
467            self.producer_version
468                .as_deref()
469                .unwrap_or(env!("CARGO_PKG_VERSION")),
470        );
471        match self.source_embedding {
472            SourceEmbedding::Omitted => {}
473            SourceEmbedding::Uncompressed => {
474                module.customs.add(SourceCodeSection::uncompressed(js)?);
475            }
476            SourceEmbedding::Compressed => {
477                module.customs.add(SourceCodeSection::compressed(js)?);
478            }
479        }
480
481        let wasm = self.postprocess(&mut module)?;
482        Ok(wasm)
483    }
484}