Skip to main content

js_component_bindgen/
lib.rs

1use std::collections::HashSet;
2
3use anyhow::{Context as _, Result, anyhow, bail, ensure};
4use ts_bindgen::ts_bindgen;
5use wasmtime_environ::component::{CanonicalOptions, ComponentTypesBuilder, StaticModuleIndex};
6use wasmtime_environ::wasmparser::WasmFeatures;
7use wasmtime_environ::{PrimaryMap, ScopeVec, Tunables};
8use wit_bindgen_core::wit_parser::Function;
9use wit_component::DecodedWasm;
10use wit_parser::{Package, Resolve, Stability, Type, TypeDefKind, TypeId, WorldId};
11
12mod core;
13mod files;
14mod transpile_bindgen;
15mod ts_bindgen;
16
17pub mod esm_bindgen;
18pub mod function_bindgen;
19pub mod names;
20pub mod source;
21
22pub mod intrinsics;
23use intrinsics::Intrinsic;
24
25use transpile_bindgen::transpile_bindgen;
26pub use transpile_bindgen::{
27    AsyncMode, BindingsMode, ExportKind, InstantiationMode, TranspileOpts,
28};
29
30/// Calls [`write!`] with the passed arguments and unwraps the result.
31///
32/// Useful for writing to things with infallible `Write` implementations like
33/// `Source` and `String`.
34///
35/// [`write!`]: std::write
36#[macro_export]
37macro_rules! uwrite {
38    ($dst:expr, $($arg:tt)*) => {
39        write!($dst, $($arg)*).unwrap()
40    };
41}
42
43/// Calls [`writeln!`] with the passed arguments and unwraps the result.
44///
45/// Useful for writing to things with infallible `Write` implementations like
46/// `Source` and `String`.
47///
48/// [`writeln!`]: std::writeln
49#[macro_export]
50macro_rules! uwriteln {
51    ($dst:expr, $($arg:tt)*) => {
52        writeln!($dst, $($arg)*).unwrap()
53    };
54}
55
56pub struct Transpiled {
57    pub files: Vec<(String, Vec<u8>)>,
58    pub imports: Vec<String>,
59    pub exports: Vec<(String, transpile_bindgen::ExportKind)>,
60}
61
62pub struct ComponentInfo {
63    pub imports: Vec<String>,
64    pub exports: Vec<(String, transpile_bindgen::ExportKind)>,
65}
66
67pub fn generate_types(
68    name: &str,
69    resolve: Resolve,
70    world_id: WorldId,
71    opts: TranspileOpts,
72) -> Result<Vec<(String, Vec<u8>)>> {
73    let mut files = files::Files::default();
74
75    ts_bindgen(name, &resolve, world_id, &opts, &mut files)
76        .context("failed to generate Typescript bindings")?;
77
78    let mut files_out: Vec<(String, Vec<u8>)> = Vec::new();
79    for (name, source) in files.iter() {
80        files_out.push((name.to_string(), source.to_vec()));
81    }
82    Ok(files_out)
83}
84
85/// Generate the JS transpilation bindgen for a given Wasm component binary
86/// Outputs the file map and import and export metadata for the Transpilation
87#[cfg(feature = "transpile-bindgen")]
88pub fn transpile(component: &[u8], opts: TranspileOpts) -> Result<Transpiled> {
89    use wasmtime_environ::component::{Component, Translator};
90
91    let name = opts.name.clone();
92    let mut files = files::Files::default();
93
94    // Use the `wit-component` crate here to parse `binary` and discover
95    // the type-level descriptions and `Resolve` corresponding to the
96    // component binary. This will synthesize a `Resolve` which has a top-level
97    // package which has a single document and `world` within it which describes
98    // the state of the component. This is then further used afterwards for
99    // bindings Transpilation as-if a `*.wit` file was input.
100    let decoded = wit_component::decode(component)
101        .context("failed to extract interface information from component")?;
102
103    let (resolve, world_id) = match decoded {
104        DecodedWasm::WitPackage(_, _) => bail!("unexpected wit package as input"),
105        DecodedWasm::Component(resolve, world_id) => (resolve, world_id),
106    };
107
108    // Components are complicated, there's no real way around that. To
109    // handle all the work of parsing a component and figuring out how to
110    // instantiate core wasm modules and such all the work is offloaded to
111    // Wasmtime itself. This crate generator is based on Wasmtime's
112    // low-level `wasmtime-environ` crate which is technically not a public
113    // dependency but the same author who worked on that in Wasmtime wrote
114    // this as well so... "seems fine".
115    //
116    // Note that we're not pulling in the entire Wasmtime engine here,
117    // moreso just the "spine" of validating a component. This enables using
118    // Wasmtime's internal `Component` representation as a much easier to
119    // process version of a component that has decompiled everything
120    // internal to a component to a straight linear list of initializers
121    // that need to be executed to instantiate a component.
122    let scope = ScopeVec::new();
123    let tunables = Tunables::default_u32();
124
125    // The validator that will be used on the component must enable support for all
126    // CM features we expect components to use.
127    //
128    // This does not require the correct execution of the related features post-transpilation,
129    // but without the right features specified, components won't load at all.
130    let mut features = WasmFeatures::WASM3
131        | WasmFeatures::WIDE_ARITHMETIC
132        | WasmFeatures::COMPONENT_MODEL
133        | WasmFeatures::CM_ASYNC
134        | WasmFeatures::CM_MORE_ASYNC_BUILTINS
135        | WasmFeatures::CM_ASYNC_STACKFUL
136        | WasmFeatures::CM_ERROR_CONTEXT
137        | WasmFeatures::CM_FIXED_LENGTH_LISTS
138        | WasmFeatures::CM_MAP
139        | WasmFeatures::CM_IMPLEMENTS;
140
141    // Unless the target engine is known to support the exception handling
142    // proposal, mask exception handling off: with the feature enabled,
143    // wasmtime-environ's FACT-generated adapters wrap calls in exception
144    // barriers (`try_table`), which only runs behind a flag (e.g.
145    // --experimental-wasm-exnref) in today's JS engines.
146    if !opts.supports_wasm_exnref {
147        features = features.difference(WasmFeatures::EXCEPTIONS);
148    }
149
150    let mut validator = wasmtime_environ::wasmparser::Validator::new_with_features(features);
151
152    let mut types = ComponentTypesBuilder::new(&validator);
153
154    let (component, modules) = Translator::new(&tunables, &mut validator, &mut types, &scope)
155        .translate(component)
156        .map_err(|e| anyhow!(e).context("failed to translate component"))?;
157
158    let modules: PrimaryMap<StaticModuleIndex, core::Translation<'_>> = modules
159        .into_iter()
160        .map(|(_i, module)| core::Translation::new(module, opts.multi_memory))
161        .collect::<Result<_>>()?;
162
163    let wasmtime_component = Component::default();
164    let types = types.finish(&wasmtime_component);
165
166    // Insert all core wasm modules into the generated `Files` which will
167    // end up getting used in the `generate_instantiate` method.
168    for (i, module) in modules.iter() {
169        files.push(&core_file_name(&name, i.as_u32()), module.wasm());
170    }
171
172    if !opts.no_typescript {
173        ts_bindgen(&name, &resolve, world_id, &opts, &mut files)
174            .context("failed to generate Typescript bindings")?;
175    }
176
177    let (imports, exports) = transpile_bindgen(
178        &name, &component, &modules, &types.0, &resolve, world_id, opts, &mut files,
179    );
180
181    let mut files_out: Vec<(String, Vec<u8>)> = Vec::new();
182    for (name, source) in files.iter() {
183        files_out.push((name.to_string(), source.to_vec()));
184    }
185    Ok(Transpiled {
186        files: files_out,
187        imports,
188        exports,
189    })
190}
191
192fn core_file_name(name: &str, idx: u32) -> String {
193    let i_str = if idx == 0 {
194        String::from("")
195    } else {
196        (idx + 1).to_string()
197    };
198    format!("{name}.core{i_str}.wasm")
199}
200
201pub fn dealias(resolve: &Resolve, mut id: TypeId) -> TypeId {
202    loop {
203        match &resolve.types[id].kind {
204            TypeDefKind::Type(Type::Id(that_id)) => id = *that_id,
205            _ => break id,
206        }
207    }
208}
209
210/// Check if an item (usually some form of [`WorldItem`]) should be allowed through the feature gate
211/// of a given package.
212fn feature_gate_allowed(
213    resolve: &Resolve,
214    package: &Package,
215    stability: &Stability,
216    item_name: &str,
217) -> Result<bool> {
218    Ok(match stability {
219        Stability::Unknown => true,
220        Stability::Stable { since, .. } => {
221            let Some(package_version) = package.name.version.as_ref() else {
222                // If the package version is missing (we're likely dealing with an unresolved package)
223                // and we can't really check much.
224                return Ok(true);
225            };
226
227            ensure!(
228                package_version >= since,
229                "feature gate on [{item_name}] refers to an unreleased (future) package version [{since}] (current package version is [{package_version}])"
230            );
231
232            // Stabilization (@since annotation) overrides features and deprecation
233            true
234        }
235        Stability::Unstable {
236            feature,
237            deprecated: _,
238        } => {
239            // If a @unstable feature is present but the related feature was not enabled
240            // or all features was not selected, exclude
241            resolve.all_features || resolve.features.contains(feature)
242        }
243    })
244}
245
246/// Utility function for deducing whether a type can throw
247pub fn get_thrown_type(
248    resolve: &Resolve,
249    return_type: Option<Type>,
250) -> Option<(Option<&Type>, Option<&Type>)> {
251    match return_type {
252        None => None,
253        Some(Type::Id(id)) => match &resolve.types[id].kind {
254            TypeDefKind::Result(r) => Some((r.ok.as_ref(), r.err.as_ref())),
255            _ => None,
256        },
257        _ => None,
258    }
259}
260
261/// Check whether a given function is an async fn
262///
263/// Functions that are designated as guest async represent use of
264/// the WASI p3 async feature.
265///
266/// These functions must be called from transpiled javsacript much differently
267/// than they would otherwise be, i.e. in accordance to the Component Model
268/// async feature.
269pub(crate) fn is_async_fn(func: &Function, canon_opts: &CanonicalOptions) -> bool {
270    if canon_opts.async_ {
271        return true;
272    }
273    func.kind.is_async()
274}
275
276/// Identifier for a function used
277enum FunctionIdentifier<'a> {
278    Fn(&'a Function),
279    CanonFnName(&'a str),
280}
281
282/// Check whether a function has been marked or async binding generation
283///
284/// When dealing with imports, functions that are designated to require async porcelain
285/// are usually asynchronous host functions -- they will have code generated
286/// that enables use of techniques like JSPI for exposing asynchronous host/platform
287/// imports to WebAssembly guests.
288///
289/// When dealing with an export, functions that require async porcelain simply provide
290/// an interface in the transpiled codebase that produces a `Promise`, i.e. one that can
291/// be called in an *already* asynchronous context (JS `async` function) or resolved with a`.then()`.
292///
293/// Exports do not indicate a use of JSPI, as JSPI is only for bridging asynchronous *host* behavior
294/// to synchronous WebAssembly modules
295///
296/// This function is *not* for detecting WASI P3 asynchronous behavior -- see [`is_guest_async_lifted_fn`].
297pub(crate) fn requires_async_porcelain(
298    func: FunctionIdentifier<'_>,
299    id: &str,
300    async_funcs: &HashSet<String>,
301) -> bool {
302    let name = match func {
303        FunctionIdentifier::Fn(func) => func.name.as_str(),
304        FunctionIdentifier::CanonFnName(name) => name,
305    }
306    .trim_start_matches("[async]");
307
308    if async_funcs.contains(name) {
309        return true;
310    }
311
312    let qualified_name = format!("{id}#{name}");
313    if async_funcs.contains(&qualified_name) {
314        return true;
315    }
316
317    if let Some(pos) = id.find('@') {
318        let namespace = &id[..pos];
319        let namespaced_name = format!("{namespace}#{name}");
320
321        if async_funcs.contains(&namespaced_name) {
322            return true;
323        }
324    }
325    false
326}
327
328/// Objects that can control the printing/setup of intrinsics (normally in some final codegen output)
329trait ManagesIntrinsics {
330    /// Add an intrinsic, supplying it's name afterwards
331    fn add_intrinsic(&mut self, intrinsic: Intrinsic);
332}