Skip to main content

componentize_qjs/
lib.rs

1pub mod codegen;
2pub mod stubwasi;
3
4use std::path::Path;
5
6use anyhow::{Context, Result, anyhow};
7use bytes::Bytes;
8use stubwasi::stub_wasi_imports;
9use wasi_preview1_component_adapter_provider::WASI_SNAPSHOT_PREVIEW1_REACTOR_ADAPTER;
10use wasmtime::component::{Component, Linker, ResourceTable};
11use wasmtime::{Config, Engine, Store};
12use wasmtime_wasi::p2::pipe::{MemoryInputPipe, MemoryOutputPipe};
13use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};
14use wasmtime_wizer::{WasmtimeWizerComponent, Wizer};
15use wit_parser::Resolve;
16
17include!(concat!(env!("OUT_DIR"), "/output.rs"));
18
19wasmtime::component::bindgen!({
20    path: "wit/init.wit",
21    world: "init",
22    exports: { default: async },
23});
24
25struct Ctx {
26    wasi: WasiCtx,
27    table: ResourceTable,
28}
29
30impl WasiView for Ctx {
31    fn ctx(&mut self) -> WasiCtxView<'_> {
32        WasiCtxView {
33            ctx: &mut self.wasi,
34            table: &mut self.table,
35        }
36    }
37}
38
39/// Options for componentizing a JavaScript source file.
40pub struct ComponentizeOpts<'a> {
41    /// Path to the WIT file or directory
42    pub wit_path: &'a Path,
43    /// JavaScript source code
44    pub js_source: &'a str,
45    /// World name to use from the WIT (None = default world)
46    pub world_name: Option<&'a str>,
47    /// Stub all WASI imports with traps
48    pub stub_wasi: bool,
49    /// Disable automatic garbage collection in the QuickJS runtime
50    pub disable_gc: bool,
51    /// Runtime to embed before Wizer initialization
52    pub runtime: Runtime<'a>,
53}
54
55/// QuickJS runtime variant to embed in the generated component.
56#[derive(Clone, Copy, Debug)]
57pub enum Runtime<'a> {
58    /// Standard runtime optimized for speed.
59    ///
60    /// Built with component-model async support when the `component-model-async`
61    /// feature is enabled (the default); otherwise this is the non-async runtime.
62    Default,
63    /// Runtime optimized for smaller generated components.
64    ///
65    /// Built with component-model async support when the `component-model-async`
66    /// feature is enabled (the default); otherwise this is the non-async runtime.
67    OptSize,
68    /// Non-async runtime optimized for speed.
69    ///
70    /// Produces components that do not use the component-model async ABI, so they
71    /// run on hosts without async support. Always available regardless of Cargo
72    /// features.
73    DefaultSync,
74    /// Non-async runtime optimized for smaller generated components.
75    ///
76    /// Produces components that do not use the component-model async ABI, so they
77    /// run on hosts without async support. Always available regardless of Cargo
78    /// features.
79    OptSizeSync,
80    /// Caller-provided runtime Wasm bytes.
81    Custom(&'a [u8]),
82}
83
84impl Default for Runtime<'_> {
85    fn default() -> Self {
86        default_builtin_runtime()
87    }
88}
89
90/// Return the built-in runtime selected by Cargo features.
91pub fn default_builtin_runtime() -> Runtime<'static> {
92    if cfg!(feature = "opt-size") {
93        Runtime::OptSize
94    } else {
95        Runtime::Default
96    }
97}
98
99/// Convert JavaScript source code into a WebAssembly component.
100pub async fn componentize(opts: &ComponentizeOpts<'_>) -> Result<Vec<u8>> {
101    let mut resolve = Resolve::default();
102    let (pkg_id, _) = resolve.push_path(opts.wit_path)?;
103    let world_id = resolve.select_world(&[pkg_id], opts.world_name)?;
104
105    let shim = codegen::generate_shim(&resolve, world_id);
106    let mut wit_dylib = wit_dylib::create(&resolve, world_id, None);
107
108    wit_component::embed_component_metadata(
109        &mut wit_dylib,
110        &resolve,
111        world_id,
112        wit_component::StringEncoding::UTF8,
113    )?;
114
115    let pre_wizer_component = wit_component::Linker::default()
116        .validate(true)
117        .library(
118            "componentize_qjs_runtime.wasm",
119            runtime_wasm(opts.runtime),
120            false,
121        )?
122        .library("wit-dylib.wasm", &wit_dylib, false)?
123        .adapter(
124            "wasi_snapshot_preview1",
125            WASI_SNAPSHOT_PREVIEW1_REACTOR_ADAPTER,
126        )?
127        .encode()
128        .context("failed to link and encode component")?;
129
130    let mut component =
131        wizer_init(&pre_wizer_component, &shim, opts.js_source, opts.disable_gc).await?;
132
133    if opts.stub_wasi {
134        component = stub_wasi_imports(&component).context("failed to stub WASI imports")?;
135    }
136
137    Ok(component)
138}
139
140/// Return the built-in default runtime Wasm bytes.
141pub fn default_runtime_wasm() -> &'static [u8] {
142    DEFAULT_RUNTIME_WASM
143}
144
145/// Return the built-in opt-size runtime Wasm bytes.
146pub fn opt_size_runtime_wasm() -> &'static [u8] {
147    OPT_SIZE_RUNTIME_WASM
148}
149
150/// Return the built-in non-async runtime Wasm bytes.
151pub fn default_sync_runtime_wasm() -> &'static [u8] {
152    DEFAULT_SYNC_RUNTIME_WASM
153}
154
155/// Return the built-in non-async opt-size runtime Wasm bytes.
156pub fn opt_size_sync_runtime_wasm() -> &'static [u8] {
157    OPT_SIZE_SYNC_RUNTIME_WASM
158}
159
160fn runtime_wasm(runtime: Runtime<'_>) -> &[u8] {
161    match runtime {
162        Runtime::Default => DEFAULT_RUNTIME_WASM,
163        Runtime::OptSize => OPT_SIZE_RUNTIME_WASM,
164        Runtime::DefaultSync => DEFAULT_SYNC_RUNTIME_WASM,
165        Runtime::OptSizeSync => OPT_SIZE_SYNC_RUNTIME_WASM,
166        Runtime::Custom(wasm) => wasm,
167    }
168}
169
170async fn wizer_init(component: &[u8], shim: &str, js: &str, disable_gc: bool) -> Result<Vec<u8>> {
171    let stdout = MemoryOutputPipe::new(10000);
172    let stderr = MemoryOutputPipe::new(10000);
173
174    let mut wasi = WasiCtxBuilder::new();
175    let wasi = wasi
176        .stdin(MemoryInputPipe::new(Bytes::new()))
177        .stdout(stdout.clone())
178        .stderr(stderr.clone())
179        .build();
180
181    let table = ResourceTable::new();
182    let mut config = Config::new();
183    config.wasm_component_model(true);
184    config.wasm_component_model_async(true);
185
186    let engine = Engine::new(&config)?;
187    let mut store = Store::new(&engine, Ctx { wasi, table });
188
189    let wizer = Wizer::new();
190    let (cx, instrumented) = wizer.instrument_component(component)?;
191    let comp = Component::new(&engine, &instrumented)?;
192
193    let mut linker = Linker::new(&engine);
194    wasmtime_wasi::p2::add_to_linker_async(&mut linker)?;
195    wasmtime_wasi::p3::add_to_linker(&mut linker)?;
196    let instance = linker.instantiate_async(&mut store, &comp).await?;
197
198    let init = Init::new(&mut store, &instance)?;
199    init.call_init(&mut store, shim, js, disable_gc)
200        .await?
201        .map_err(|e| anyhow!("{e}"))
202        .with_context(move || {
203            format!(
204                "{}{}",
205                String::from_utf8_lossy(&stdout.contents()),
206                String::from_utf8_lossy(&stderr.contents())
207            )
208        })?;
209
210    let component = wizer
211        .snapshot_component(
212            cx,
213            &mut WasmtimeWizerComponent {
214                store: &mut store,
215                instance,
216            },
217        )
218        .await?;
219
220    Ok(component)
221}