Skip to main content

js_component_bindgen/
lib.rs

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