js_component_bindgen/
lib.rs

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