From 66f41b578e7f406b45f36cf3cd238dcdd44d72a3 Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Fri, 25 Oct 2024 17:58:02 -0500 Subject: [PATCH 01/35] wip --- .../src/bindings.rs | 964 ++++++++++++++++++ .../js-component-bindgen-component/src/lib.rs | 12 + .../wit/js-component-bindgen.wit | 18 + .../src/function_bindgen.rs | 16 +- crates/js-component-bindgen/src/lib.rs | 2 +- .../src/transpile_bindgen.rs | 103 +- 6 files changed, 1105 insertions(+), 10 deletions(-) create mode 100644 crates/js-component-bindgen-component/src/bindings.rs diff --git a/crates/js-component-bindgen-component/src/bindings.rs b/crates/js-component-bindgen-component/src/bindings.rs new file mode 100644 index 000000000..1889827ff --- /dev/null +++ b/crates/js-component-bindgen-component/src/bindings.rs @@ -0,0 +1,964 @@ +pub type Files = _rt::Vec<(_rt::String, _rt::Vec)>; +pub type Maps = _rt::Vec<(_rt::String, _rt::String)>; +#[derive(Clone, Copy)] +pub enum InstantiationMode { + Async, + Sync, +} +impl ::core::fmt::Debug for InstantiationMode { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + match self { + InstantiationMode::Async => { + f.debug_tuple("InstantiationMode::Async").finish() + } + InstantiationMode::Sync => f.debug_tuple("InstantiationMode::Sync").finish(), + } + } +} +#[derive(Clone, Copy)] +pub enum BindingsMode { + Js, + Hybrid, + Optimized, + DirectOptimized, +} +impl ::core::fmt::Debug for BindingsMode { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + match self { + BindingsMode::Js => f.debug_tuple("BindingsMode::Js").finish(), + BindingsMode::Hybrid => f.debug_tuple("BindingsMode::Hybrid").finish(), + BindingsMode::Optimized => f.debug_tuple("BindingsMode::Optimized").finish(), + BindingsMode::DirectOptimized => { + f.debug_tuple("BindingsMode::DirectOptimized").finish() + } + } + } +} +#[derive(Clone)] +pub struct GenerateOptions { + /// Name to use for the generated component + pub name: _rt::String, + /// Disables generation of `*.d.ts` files and instead only generates `*.js` + /// source files. + pub no_typescript: Option, + /// Provide a custom JS instantiation API for the component instead + /// of the direct importable native ESM output. + pub instantiation: Option, + /// Import bindings generation mode + pub import_bindings: Option, + /// Mappings of component import specifiers to JS import specifiers. + pub map: Option, + /// Enables all compat flags: --tla-compat. + pub compat: Option, + /// Disables compatibility in Node.js without a fetch global. + pub no_nodejs_compat: Option, + /// Set the cutoff byte size for base64 inlining core Wasm in instantiation mode + /// (set to 0 to disable all base64 inlining) + pub base64_cutoff: Option, + /// Enables compatibility for JS environments without top-level await support + /// via an async $init promise export to wait for instead. + pub tla_compat: Option, + /// Disable verification of component Wasm data structures when + /// lifting as a production optimization + pub valid_lifting_optimization: Option, + /// Whether or not to emit `tracing` calls on function entry/exit. + pub tracing: Option, + /// Whether to generate namespaced exports like `foo as "local:package/foo"`. + /// These exports can break typescript builds. + pub no_namespaced_exports: Option, + /// Whether to output core Wasm utilizing multi-memory or to polyfill + /// this handling. + pub multi_memory: Option, +} +impl ::core::fmt::Debug for GenerateOptions { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct("GenerateOptions") + .field("name", &self.name) + .field("no-typescript", &self.no_typescript) + .field("instantiation", &self.instantiation) + .field("import-bindings", &self.import_bindings) + .field("map", &self.map) + .field("compat", &self.compat) + .field("no-nodejs-compat", &self.no_nodejs_compat) + .field("base64-cutoff", &self.base64_cutoff) + .field("tla-compat", &self.tla_compat) + .field("valid-lifting-optimization", &self.valid_lifting_optimization) + .field("tracing", &self.tracing) + .field("no-namespaced-exports", &self.no_namespaced_exports) + .field("multi-memory", &self.multi_memory) + .finish() + } +} +#[derive(Clone)] +pub enum Wit { + /// wit is provided as an inline WIT string + Source(_rt::String), + /// wit is provided from a component binary + Binary(_rt::Vec), + /// wit is provided from a filesystem path + Path(_rt::String), +} +impl ::core::fmt::Debug for Wit { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + match self { + Wit::Source(e) => f.debug_tuple("Wit::Source").field(e).finish(), + Wit::Binary(e) => f.debug_tuple("Wit::Binary").field(e).finish(), + Wit::Path(e) => f.debug_tuple("Wit::Path").field(e).finish(), + } + } +} +/// Enumerate enabled features +#[derive(Clone)] +pub enum EnabledFeatureSet { + /// Enable only the given list of features + List(_rt::Vec<_rt::String>), + /// Enable all features + All, +} +impl ::core::fmt::Debug for EnabledFeatureSet { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + match self { + EnabledFeatureSet::List(e) => { + f.debug_tuple("EnabledFeatureSet::List").field(e).finish() + } + EnabledFeatureSet::All => f.debug_tuple("EnabledFeatureSet::All").finish(), + } + } +} +#[derive(Clone)] +pub struct TypeGenerationOptions { + /// wit to generate typing from + pub wit: Wit, + /// world to generate typing for + pub world: Option<_rt::String>, + pub tla_compat: Option, + pub instantiation: Option, + pub map: Option, + /// Features that should be enabled as part of feature gating + pub features: Option, +} +impl ::core::fmt::Debug for TypeGenerationOptions { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct("TypeGenerationOptions") + .field("wit", &self.wit) + .field("world", &self.world) + .field("tla-compat", &self.tla_compat) + .field("instantiation", &self.instantiation) + .field("map", &self.map) + .field("features", &self.features) + .finish() + } +} +#[repr(u8)] +#[derive(Clone, Copy, Eq, PartialEq)] +pub enum ExportType { + Function, + Instance, +} +impl ::core::fmt::Debug for ExportType { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + match self { + ExportType::Function => f.debug_tuple("ExportType::Function").finish(), + ExportType::Instance => f.debug_tuple("ExportType::Instance").finish(), + } + } +} +impl ExportType { + #[doc(hidden)] + pub unsafe fn _lift(val: u8) -> ExportType { + if !cfg!(debug_assertions) { + return ::core::mem::transmute(val); + } + match val { + 0 => ExportType::Function, + 1 => ExportType::Instance, + _ => panic!("invalid enum discriminant"), + } + } +} +#[derive(Clone)] +pub struct Transpiled { + pub files: Files, + pub imports: _rt::Vec<_rt::String>, + pub exports: _rt::Vec<(_rt::String, ExportType)>, +} +impl ::core::fmt::Debug for Transpiled { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct("Transpiled") + .field("files", &self.files) + .field("imports", &self.imports) + .field("exports", &self.exports) + .finish() + } +} +#[doc(hidden)] +#[allow(non_snake_case)] +pub unsafe fn _export_generate_cabi(arg0: *mut u8) -> *mut u8 { + #[cfg(target_arch = "wasm32")] _rt::run_ctors_once(); + let l0 = *arg0.add(0).cast::<*mut u8>(); + let l1 = *arg0.add(4).cast::(); + let len2 = l1; + let l3 = *arg0.add(8).cast::<*mut u8>(); + let l4 = *arg0.add(12).cast::(); + let len5 = l4; + let bytes5 = _rt::Vec::from_raw_parts(l3.cast(), len5, len5); + let l6 = i32::from(*arg0.add(16).cast::()); + let l8 = i32::from(*arg0.add(18).cast::()); + let l11 = i32::from(*arg0.add(20).cast::()); + let l14 = i32::from(*arg0.add(24).cast::()); + let l24 = i32::from(*arg0.add(36).cast::()); + let l26 = i32::from(*arg0.add(38).cast::()); + let l28 = i32::from(*arg0.add(40).cast::()); + let l30 = i32::from(*arg0.add(48).cast::()); + let l32 = i32::from(*arg0.add(50).cast::()); + let l34 = i32::from(*arg0.add(52).cast::()); + let l36 = i32::from(*arg0.add(54).cast::()); + let l38 = i32::from(*arg0.add(56).cast::()); + let result40 = T::generate( + _rt::Vec::from_raw_parts(l0.cast(), len2, len2), + GenerateOptions { + name: _rt::string_lift(bytes5), + no_typescript: match l6 { + 0 => None, + 1 => { + let e = { + let l7 = i32::from(*arg0.add(17).cast::()); + _rt::bool_lift(l7 as u8) + }; + Some(e) + } + _ => _rt::invalid_enum_discriminant(), + }, + instantiation: match l8 { + 0 => None, + 1 => { + let e = { + let l9 = i32::from(*arg0.add(19).cast::()); + let v10 = match l9 { + 0 => InstantiationMode::Async, + n => { + debug_assert_eq!(n, 1, "invalid enum discriminant"); + InstantiationMode::Sync + } + }; + v10 + }; + Some(e) + } + _ => _rt::invalid_enum_discriminant(), + }, + import_bindings: match l11 { + 0 => None, + 1 => { + let e = { + let l12 = i32::from(*arg0.add(21).cast::()); + let v13 = match l12 { + 0 => BindingsMode::Js, + 1 => BindingsMode::Hybrid, + 2 => BindingsMode::Optimized, + n => { + debug_assert_eq!(n, 3, "invalid enum discriminant"); + BindingsMode::DirectOptimized + } + }; + v13 + }; + Some(e) + } + _ => _rt::invalid_enum_discriminant(), + }, + map: match l14 { + 0 => None, + 1 => { + let e = { + let l15 = *arg0.add(28).cast::<*mut u8>(); + let l16 = *arg0.add(32).cast::(); + let base23 = l15; + let len23 = l16; + let mut result23 = _rt::Vec::with_capacity(len23); + for i in 0..len23 { + let base = base23.add(i * 16); + let e23 = { + let l17 = *base.add(0).cast::<*mut u8>(); + let l18 = *base.add(4).cast::(); + let len19 = l18; + let bytes19 = _rt::Vec::from_raw_parts( + l17.cast(), + len19, + len19, + ); + let l20 = *base.add(8).cast::<*mut u8>(); + let l21 = *base.add(12).cast::(); + let len22 = l21; + let bytes22 = _rt::Vec::from_raw_parts( + l20.cast(), + len22, + len22, + ); + (_rt::string_lift(bytes19), _rt::string_lift(bytes22)) + }; + result23.push(e23); + } + _rt::cabi_dealloc(base23, len23 * 16, 4); + result23 + }; + Some(e) + } + _ => _rt::invalid_enum_discriminant(), + }, + compat: match l24 { + 0 => None, + 1 => { + let e = { + let l25 = i32::from(*arg0.add(37).cast::()); + _rt::bool_lift(l25 as u8) + }; + Some(e) + } + _ => _rt::invalid_enum_discriminant(), + }, + no_nodejs_compat: match l26 { + 0 => None, + 1 => { + let e = { + let l27 = i32::from(*arg0.add(39).cast::()); + _rt::bool_lift(l27 as u8) + }; + Some(e) + } + _ => _rt::invalid_enum_discriminant(), + }, + base64_cutoff: match l28 { + 0 => None, + 1 => { + let e = { + let l29 = *arg0.add(44).cast::(); + l29 as u32 + }; + Some(e) + } + _ => _rt::invalid_enum_discriminant(), + }, + tla_compat: match l30 { + 0 => None, + 1 => { + let e = { + let l31 = i32::from(*arg0.add(49).cast::()); + _rt::bool_lift(l31 as u8) + }; + Some(e) + } + _ => _rt::invalid_enum_discriminant(), + }, + valid_lifting_optimization: match l32 { + 0 => None, + 1 => { + let e = { + let l33 = i32::from(*arg0.add(51).cast::()); + _rt::bool_lift(l33 as u8) + }; + Some(e) + } + _ => _rt::invalid_enum_discriminant(), + }, + tracing: match l34 { + 0 => None, + 1 => { + let e = { + let l35 = i32::from(*arg0.add(53).cast::()); + _rt::bool_lift(l35 as u8) + }; + Some(e) + } + _ => _rt::invalid_enum_discriminant(), + }, + no_namespaced_exports: match l36 { + 0 => None, + 1 => { + let e = { + let l37 = i32::from(*arg0.add(55).cast::()); + _rt::bool_lift(l37 as u8) + }; + Some(e) + } + _ => _rt::invalid_enum_discriminant(), + }, + multi_memory: match l38 { + 0 => None, + 1 => { + let e = { + let l39 = i32::from(*arg0.add(57).cast::()); + _rt::bool_lift(l39 as u8) + }; + Some(e) + } + _ => _rt::invalid_enum_discriminant(), + }, + }, + ); + _rt::cabi_dealloc(arg0, 60, 4); + let ptr41 = _RET_AREA.0.as_mut_ptr().cast::(); + match result40 { + Ok(e) => { + *ptr41.add(0).cast::() = (0i32) as u8; + let Transpiled { files: files42, imports: imports42, exports: exports42 } = e; + let vec46 = files42; + let len46 = vec46.len(); + let layout46 = _rt::alloc::Layout::from_size_align_unchecked( + vec46.len() * 16, + 4, + ); + let result46 = if layout46.size() != 0 { + let ptr = _rt::alloc::alloc(layout46).cast::(); + if ptr.is_null() { + _rt::alloc::handle_alloc_error(layout46); + } + ptr + } else { + { ::core::ptr::null_mut() } + }; + for (i, e) in vec46.into_iter().enumerate() { + let base = result46.add(i * 16); + { + let (t43_0, t43_1) = e; + let vec44 = (t43_0.into_bytes()).into_boxed_slice(); + let ptr44 = vec44.as_ptr().cast::(); + let len44 = vec44.len(); + ::core::mem::forget(vec44); + *base.add(4).cast::() = len44; + *base.add(0).cast::<*mut u8>() = ptr44.cast_mut(); + let vec45 = (t43_1).into_boxed_slice(); + let ptr45 = vec45.as_ptr().cast::(); + let len45 = vec45.len(); + ::core::mem::forget(vec45); + *base.add(12).cast::() = len45; + *base.add(8).cast::<*mut u8>() = ptr45.cast_mut(); + } + } + *ptr41.add(8).cast::() = len46; + *ptr41.add(4).cast::<*mut u8>() = result46; + let vec48 = imports42; + let len48 = vec48.len(); + let layout48 = _rt::alloc::Layout::from_size_align_unchecked( + vec48.len() * 8, + 4, + ); + let result48 = if layout48.size() != 0 { + let ptr = _rt::alloc::alloc(layout48).cast::(); + if ptr.is_null() { + _rt::alloc::handle_alloc_error(layout48); + } + ptr + } else { + { ::core::ptr::null_mut() } + }; + for (i, e) in vec48.into_iter().enumerate() { + let base = result48.add(i * 8); + { + let vec47 = (e.into_bytes()).into_boxed_slice(); + let ptr47 = vec47.as_ptr().cast::(); + let len47 = vec47.len(); + ::core::mem::forget(vec47); + *base.add(4).cast::() = len47; + *base.add(0).cast::<*mut u8>() = ptr47.cast_mut(); + } + } + *ptr41.add(16).cast::() = len48; + *ptr41.add(12).cast::<*mut u8>() = result48; + let vec51 = exports42; + let len51 = vec51.len(); + let layout51 = _rt::alloc::Layout::from_size_align_unchecked( + vec51.len() * 12, + 4, + ); + let result51 = if layout51.size() != 0 { + let ptr = _rt::alloc::alloc(layout51).cast::(); + if ptr.is_null() { + _rt::alloc::handle_alloc_error(layout51); + } + ptr + } else { + { ::core::ptr::null_mut() } + }; + for (i, e) in vec51.into_iter().enumerate() { + let base = result51.add(i * 12); + { + let (t49_0, t49_1) = e; + let vec50 = (t49_0.into_bytes()).into_boxed_slice(); + let ptr50 = vec50.as_ptr().cast::(); + let len50 = vec50.len(); + ::core::mem::forget(vec50); + *base.add(4).cast::() = len50; + *base.add(0).cast::<*mut u8>() = ptr50.cast_mut(); + *base.add(8).cast::() = (t49_1.clone() as i32) as u8; + } + } + *ptr41.add(24).cast::() = len51; + *ptr41.add(20).cast::<*mut u8>() = result51; + } + Err(e) => { + *ptr41.add(0).cast::() = (1i32) as u8; + let vec52 = (e.into_bytes()).into_boxed_slice(); + let ptr52 = vec52.as_ptr().cast::(); + let len52 = vec52.len(); + ::core::mem::forget(vec52); + *ptr41.add(8).cast::() = len52; + *ptr41.add(4).cast::<*mut u8>() = ptr52.cast_mut(); + } + }; + ptr41 +} +#[doc(hidden)] +#[allow(non_snake_case)] +pub unsafe fn __post_return_generate(arg0: *mut u8) { + let l0 = i32::from(*arg0.add(0).cast::()); + match l0 { + 0 => { + let l1 = *arg0.add(4).cast::<*mut u8>(); + let l2 = *arg0.add(8).cast::(); + let base8 = l1; + let len8 = l2; + for i in 0..len8 { + let base = base8.add(i * 16); + { + let l3 = *base.add(0).cast::<*mut u8>(); + let l4 = *base.add(4).cast::(); + _rt::cabi_dealloc(l3, l4, 1); + let l5 = *base.add(8).cast::<*mut u8>(); + let l6 = *base.add(12).cast::(); + let base7 = l5; + let len7 = l6; + _rt::cabi_dealloc(base7, len7 * 1, 1); + } + } + _rt::cabi_dealloc(base8, len8 * 16, 4); + let l9 = *arg0.add(12).cast::<*mut u8>(); + let l10 = *arg0.add(16).cast::(); + let base13 = l9; + let len13 = l10; + for i in 0..len13 { + let base = base13.add(i * 8); + { + let l11 = *base.add(0).cast::<*mut u8>(); + let l12 = *base.add(4).cast::(); + _rt::cabi_dealloc(l11, l12, 1); + } + } + _rt::cabi_dealloc(base13, len13 * 8, 4); + let l14 = *arg0.add(20).cast::<*mut u8>(); + let l15 = *arg0.add(24).cast::(); + let base18 = l14; + let len18 = l15; + for i in 0..len18 { + let base = base18.add(i * 12); + { + let l16 = *base.add(0).cast::<*mut u8>(); + let l17 = *base.add(4).cast::(); + _rt::cabi_dealloc(l16, l17, 1); + } + } + _rt::cabi_dealloc(base18, len18 * 12, 4); + } + _ => { + let l19 = *arg0.add(4).cast::<*mut u8>(); + let l20 = *arg0.add(8).cast::(); + _rt::cabi_dealloc(l19, l20, 1); + } + } +} +#[doc(hidden)] +#[allow(non_snake_case)] +pub unsafe fn _export_generate_types_cabi(arg0: *mut u8) -> *mut u8 { + #[cfg(target_arch = "wasm32")] _rt::run_ctors_once(); + let l0 = *arg0.add(0).cast::<*mut u8>(); + let l1 = *arg0.add(4).cast::(); + let len2 = l1; + let bytes2 = _rt::Vec::from_raw_parts(l0.cast(), len2, len2); + let l3 = i32::from(*arg0.add(8).cast::()); + let v13 = match l3 { + 0 => { + let e13 = { + let l4 = *arg0.add(12).cast::<*mut u8>(); + let l5 = *arg0.add(16).cast::(); + let len6 = l5; + let bytes6 = _rt::Vec::from_raw_parts(l4.cast(), len6, len6); + _rt::string_lift(bytes6) + }; + Wit::Source(e13) + } + 1 => { + let e13 = { + let l7 = *arg0.add(12).cast::<*mut u8>(); + let l8 = *arg0.add(16).cast::(); + let len9 = l8; + _rt::Vec::from_raw_parts(l7.cast(), len9, len9) + }; + Wit::Binary(e13) + } + n => { + debug_assert_eq!(n, 2, "invalid enum discriminant"); + let e13 = { + let l10 = *arg0.add(12).cast::<*mut u8>(); + let l11 = *arg0.add(16).cast::(); + let len12 = l11; + let bytes12 = _rt::Vec::from_raw_parts(l10.cast(), len12, len12); + _rt::string_lift(bytes12) + }; + Wit::Path(e13) + } + }; + let l14 = i32::from(*arg0.add(20).cast::()); + let l18 = i32::from(*arg0.add(32).cast::()); + let l20 = i32::from(*arg0.add(34).cast::()); + let l23 = i32::from(*arg0.add(36).cast::()); + let l33 = i32::from(*arg0.add(48).cast::()); + let result42 = T::generate_types( + _rt::string_lift(bytes2), + TypeGenerationOptions { + wit: v13, + world: match l14 { + 0 => None, + 1 => { + let e = { + let l15 = *arg0.add(24).cast::<*mut u8>(); + let l16 = *arg0.add(28).cast::(); + let len17 = l16; + let bytes17 = _rt::Vec::from_raw_parts(l15.cast(), len17, len17); + _rt::string_lift(bytes17) + }; + Some(e) + } + _ => _rt::invalid_enum_discriminant(), + }, + tla_compat: match l18 { + 0 => None, + 1 => { + let e = { + let l19 = i32::from(*arg0.add(33).cast::()); + _rt::bool_lift(l19 as u8) + }; + Some(e) + } + _ => _rt::invalid_enum_discriminant(), + }, + instantiation: match l20 { + 0 => None, + 1 => { + let e = { + let l21 = i32::from(*arg0.add(35).cast::()); + let v22 = match l21 { + 0 => InstantiationMode::Async, + n => { + debug_assert_eq!(n, 1, "invalid enum discriminant"); + InstantiationMode::Sync + } + }; + v22 + }; + Some(e) + } + _ => _rt::invalid_enum_discriminant(), + }, + map: match l23 { + 0 => None, + 1 => { + let e = { + let l24 = *arg0.add(40).cast::<*mut u8>(); + let l25 = *arg0.add(44).cast::(); + let base32 = l24; + let len32 = l25; + let mut result32 = _rt::Vec::with_capacity(len32); + for i in 0..len32 { + let base = base32.add(i * 16); + let e32 = { + let l26 = *base.add(0).cast::<*mut u8>(); + let l27 = *base.add(4).cast::(); + let len28 = l27; + let bytes28 = _rt::Vec::from_raw_parts( + l26.cast(), + len28, + len28, + ); + let l29 = *base.add(8).cast::<*mut u8>(); + let l30 = *base.add(12).cast::(); + let len31 = l30; + let bytes31 = _rt::Vec::from_raw_parts( + l29.cast(), + len31, + len31, + ); + (_rt::string_lift(bytes28), _rt::string_lift(bytes31)) + }; + result32.push(e32); + } + _rt::cabi_dealloc(base32, len32 * 16, 4); + result32 + }; + Some(e) + } + _ => _rt::invalid_enum_discriminant(), + }, + features: match l33 { + 0 => None, + 1 => { + let e = { + let l34 = i32::from(*arg0.add(52).cast::()); + let v41 = match l34 { + 0 => { + let e41 = { + let l35 = *arg0.add(56).cast::<*mut u8>(); + let l36 = *arg0.add(60).cast::(); + let base40 = l35; + let len40 = l36; + let mut result40 = _rt::Vec::with_capacity(len40); + for i in 0..len40 { + let base = base40.add(i * 8); + let e40 = { + let l37 = *base.add(0).cast::<*mut u8>(); + let l38 = *base.add(4).cast::(); + let len39 = l38; + let bytes39 = _rt::Vec::from_raw_parts( + l37.cast(), + len39, + len39, + ); + _rt::string_lift(bytes39) + }; + result40.push(e40); + } + _rt::cabi_dealloc(base40, len40 * 8, 4); + result40 + }; + EnabledFeatureSet::List(e41) + } + n => { + debug_assert_eq!(n, 1, "invalid enum discriminant"); + EnabledFeatureSet::All + } + }; + v41 + }; + Some(e) + } + _ => _rt::invalid_enum_discriminant(), + }, + }, + ); + _rt::cabi_dealloc(arg0, 64, 4); + let ptr43 = _RET_AREA.0.as_mut_ptr().cast::(); + match result42 { + Ok(e) => { + *ptr43.add(0).cast::() = (0i32) as u8; + let vec47 = e; + let len47 = vec47.len(); + let layout47 = _rt::alloc::Layout::from_size_align_unchecked( + vec47.len() * 16, + 4, + ); + let result47 = if layout47.size() != 0 { + let ptr = _rt::alloc::alloc(layout47).cast::(); + if ptr.is_null() { + _rt::alloc::handle_alloc_error(layout47); + } + ptr + } else { + { ::core::ptr::null_mut() } + }; + for (i, e) in vec47.into_iter().enumerate() { + let base = result47.add(i * 16); + { + let (t44_0, t44_1) = e; + let vec45 = (t44_0.into_bytes()).into_boxed_slice(); + let ptr45 = vec45.as_ptr().cast::(); + let len45 = vec45.len(); + ::core::mem::forget(vec45); + *base.add(4).cast::() = len45; + *base.add(0).cast::<*mut u8>() = ptr45.cast_mut(); + let vec46 = (t44_1).into_boxed_slice(); + let ptr46 = vec46.as_ptr().cast::(); + let len46 = vec46.len(); + ::core::mem::forget(vec46); + *base.add(12).cast::() = len46; + *base.add(8).cast::<*mut u8>() = ptr46.cast_mut(); + } + } + *ptr43.add(8).cast::() = len47; + *ptr43.add(4).cast::<*mut u8>() = result47; + } + Err(e) => { + *ptr43.add(0).cast::() = (1i32) as u8; + let vec48 = (e.into_bytes()).into_boxed_slice(); + let ptr48 = vec48.as_ptr().cast::(); + let len48 = vec48.len(); + ::core::mem::forget(vec48); + *ptr43.add(8).cast::() = len48; + *ptr43.add(4).cast::<*mut u8>() = ptr48.cast_mut(); + } + }; + ptr43 +} +#[doc(hidden)] +#[allow(non_snake_case)] +pub unsafe fn __post_return_generate_types(arg0: *mut u8) { + let l0 = i32::from(*arg0.add(0).cast::()); + match l0 { + 0 => { + let l1 = *arg0.add(4).cast::<*mut u8>(); + let l2 = *arg0.add(8).cast::(); + let base8 = l1; + let len8 = l2; + for i in 0..len8 { + let base = base8.add(i * 16); + { + let l3 = *base.add(0).cast::<*mut u8>(); + let l4 = *base.add(4).cast::(); + _rt::cabi_dealloc(l3, l4, 1); + let l5 = *base.add(8).cast::<*mut u8>(); + let l6 = *base.add(12).cast::(); + let base7 = l5; + let len7 = l6; + _rt::cabi_dealloc(base7, len7 * 1, 1); + } + } + _rt::cabi_dealloc(base8, len8 * 16, 4); + } + _ => { + let l9 = *arg0.add(4).cast::<*mut u8>(); + let l10 = *arg0.add(8).cast::(); + _rt::cabi_dealloc(l9, l10, 1); + } + } +} +pub trait Guest { + /// Generate the file structure for the transpiled of a component + /// into a JS embedding, returns the file list and imports and exports of the + /// output JS generation component + fn generate( + component: _rt::Vec, + options: GenerateOptions, + ) -> Result; + fn generate_types( + name: _rt::String, + options: TypeGenerationOptions, + ) -> Result; +} +#[doc(hidden)] +macro_rules! __export_world_js_component_bindgen_cabi { + ($ty:ident with_types_in $($path_to_types:tt)*) => { + const _ : () = { #[export_name = "generate"] unsafe extern "C" fn + export_generate(arg0 : * mut u8,) -> * mut u8 { $($path_to_types)*:: + _export_generate_cabi::<$ty > (arg0) } #[export_name = "cabi_post_generate"] + unsafe extern "C" fn _post_return_generate(arg0 : * mut u8,) { + $($path_to_types)*:: __post_return_generate::<$ty > (arg0) } #[export_name = + "generate-types"] unsafe extern "C" fn export_generate_types(arg0 : * mut u8,) -> + * mut u8 { $($path_to_types)*:: _export_generate_types_cabi::<$ty > (arg0) } + #[export_name = "cabi_post_generate-types"] unsafe extern "C" fn + _post_return_generate_types(arg0 : * mut u8,) { $($path_to_types)*:: + __post_return_generate_types::<$ty > (arg0) } }; + }; +} +#[doc(hidden)] +pub(crate) use __export_world_js_component_bindgen_cabi; +#[repr(align(4))] +struct _RetArea([::core::mem::MaybeUninit; 28]); +static mut _RET_AREA: _RetArea = _RetArea([::core::mem::MaybeUninit::uninit(); 28]); +mod _rt { + pub use alloc_crate::vec::Vec; + pub use alloc_crate::string::String; + #[cfg(target_arch = "wasm32")] + pub fn run_ctors_once() { + wit_bindgen_rt::run_ctors_once(); + } + pub unsafe fn string_lift(bytes: Vec) -> String { + if cfg!(debug_assertions) { + String::from_utf8(bytes).unwrap() + } else { + String::from_utf8_unchecked(bytes) + } + } + pub unsafe fn bool_lift(val: u8) -> bool { + if cfg!(debug_assertions) { + match val { + 0 => false, + 1 => true, + _ => panic!("invalid bool discriminant"), + } + } else { + val != 0 + } + } + pub unsafe fn invalid_enum_discriminant() -> T { + if cfg!(debug_assertions) { + panic!("invalid enum discriminant") + } else { + core::hint::unreachable_unchecked() + } + } + pub unsafe fn cabi_dealloc(ptr: *mut u8, size: usize, align: usize) { + if size == 0 { + return; + } + let layout = alloc::Layout::from_size_align_unchecked(size, align); + alloc::dealloc(ptr, layout); + } + pub use alloc_crate::alloc; + extern crate alloc as alloc_crate; +} +/// Generates `#[no_mangle]` functions to export the specified type as the +/// root implementation of all generated traits. +/// +/// For more information see the documentation of `wit_bindgen::generate!`. +/// +/// ```rust +/// # macro_rules! export{ ($($t:tt)*) => (); } +/// # trait Guest {} +/// struct MyType; +/// +/// impl Guest for MyType { +/// // ... +/// } +/// +/// export!(MyType); +/// ``` +#[allow(unused_macros)] +#[doc(hidden)] +macro_rules! __export_js_component_bindgen_impl { + ($ty:ident) => { + self::export!($ty with_types_in self); + }; + ($ty:ident with_types_in $($path_to_types_root:tt)*) => { + $($path_to_types_root)*:: __export_world_js_component_bindgen_cabi!($ty + with_types_in $($path_to_types_root)*); + }; +} +#[doc(inline)] +pub(crate) use __export_js_component_bindgen_impl as export; +#[cfg(target_arch = "wasm32")] +#[link_section = "component-type:wit-bindgen:0.30.0:js-component-bindgen:encoded world"] +#[doc(hidden)] +pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 927] = *b"\ +\0asm\x0d\0\x01\0\0\x19\x16wit-component-encoding\x04\0\x07\x94\x06\x01A\x02\x01\ +A'\x01p}\x01o\x02s\0\x01p\x01\x03\0\x05files\x03\0\x02\x01o\x02ss\x01p\x04\x03\0\ +\x04maps\x03\0\x05\x01q\x02\x05async\0\0\x04sync\0\0\x03\0\x12instantiation-mode\ +\x03\0\x07\x01q\x04\x02js\0\0\x06hybrid\0\0\x09optimized\0\0\x10direct-optimized\ +\0\0\x03\0\x0dbindings-mode\x03\0\x09\x01k\x7f\x01k\x08\x01k\x0a\x01k\x06\x01ky\x01\ +r\x0d\x04names\x0dno-typescript\x0b\x0dinstantiation\x0c\x0fimport-bindings\x0d\x03\ +map\x0e\x06compat\x0b\x10no-nodejs-compat\x0b\x0dbase64-cutoff\x0f\x0atla-compat\ +\x0b\x1avalid-lifting-optimization\x0b\x07tracing\x0b\x15no-namespaced-exports\x0b\ +\x0cmulti-memory\x0b\x03\0\x10generate-options\x03\0\x10\x01q\x03\x06source\x01s\ +\0\x06binary\x01\0\0\x04path\x01s\0\x03\0\x03wit\x03\0\x12\x01ps\x01q\x02\x04lis\ +t\x01\x14\0\x03all\0\0\x03\0\x13enabled-feature-set\x03\0\x15\x01ks\x01k\x16\x01\ +r\x06\x03wit\x13\x05world\x17\x0atla-compat\x0b\x0dinstantiation\x0c\x03map\x0e\x08\ +features\x18\x03\0\x17type-generation-options\x03\0\x19\x01m\x02\x08function\x08\ +instance\x03\0\x0bexport-type\x03\0\x1b\x01o\x02s\x1c\x01p\x1d\x01r\x03\x05files\ +\x03\x07imports\x14\x07exports\x1e\x03\0\x0atranspiled\x03\0\x1f\x01j\x01\x20\x01\ +s\x01@\x02\x09component\0\x07options\x11\0!\x04\0\x08generate\x01\"\x01j\x01\x03\ +\x01s\x01@\x02\x04names\x07options\x1a\0#\x04\0\x0egenerate-types\x01$\x04\x01/l\ +ocal:js-component-bindgen/js-component-bindgen\x04\0\x0b\x1a\x01\0\x14js-compone\ +nt-bindgen\x03\0\0\0G\x09producers\x01\x0cprocessed-by\x02\x0dwit-component\x070\ +.215.0\x10wit-bindgen-rust\x060.30.0"; +#[inline(never)] +#[doc(hidden)] +pub fn __link_custom_section_describing_imports() { + wit_bindgen_rt::maybe_link_cabi_realloc(); +} diff --git a/crates/js-component-bindgen-component/src/lib.rs b/crates/js-component-bindgen-component/src/lib.rs index e16bd44dc..9cb2cee76 100644 --- a/crates/js-component-bindgen-component/src/lib.rs +++ b/crates/js-component-bindgen-component/src/lib.rs @@ -53,6 +53,16 @@ impl From for js_component_bindgen::BindingsMode { } } +impl From for js_component_bindgen::AsyncMode { + fn from(value: AsyncMode) -> Self { + match value { + AsyncMode::Sync => js_component_bindgen::AsyncMode::Sync, + AsyncMode::JavaScriptPromiseIntegration(AsyncImportsExports{ imports, exports }) => js_component_bindgen::AsyncMode::JavaScriptPromiseIntegration { imports, exports }, + AsyncMode::Asyncify(AsyncImportsExports{ imports, exports }) => js_component_bindgen::AsyncMode::Asyncify { imports, exports }, + } + } +} + struct JsComponentBindgenComponent; export!(JsComponentBindgenComponent); @@ -75,6 +85,7 @@ impl Guest for JsComponentBindgenComponent { no_namespaced_exports: options.no_namespaced_exports.unwrap_or(false), multi_memory: options.multi_memory.unwrap_or(false), import_bindings: options.import_bindings.map(Into::into), + async_mode: options.async_mode.map(Into::into), }; let js_component_bindgen::Transpiled { @@ -160,6 +171,7 @@ impl Guest for JsComponentBindgenComponent { no_namespaced_exports: false, multi_memory: false, import_bindings: None, + async_mode: None, }; let files = generate_types(name, resolve, world, opts).map_err(|e| e.to_string())?; diff --git a/crates/js-component-bindgen-component/wit/js-component-bindgen.wit b/crates/js-component-bindgen-component/wit/js-component-bindgen.wit index 631e5deef..80bb55caa 100644 --- a/crates/js-component-bindgen-component/wit/js-component-bindgen.wit +++ b/crates/js-component-bindgen-component/wit/js-component-bindgen.wit @@ -62,6 +62,24 @@ world js-component-bindgen { /// Whether to output core Wasm utilizing multi-memory or to polyfill /// this handling. multi-memory: option, + + /// Configure whether to use `async` imports or exports with + /// JavaScript Promise Integration (JSPI) or Asyncify. + async-mode: option, + } + + record async-imports-exports { + imports: list, + exports: list, + } + + variant async-mode { + /// the default sync mode + sync, + /// use JavaScript Promise Integration (JSPI) + java-script-promise-integration(async-imports-exports), + /// use Asyncify + asyncify(async-imports-exports), } variant wit { diff --git a/crates/js-component-bindgen/src/function_bindgen.rs b/crates/js-component-bindgen/src/function_bindgen.rs index 3d0687ad7..75c23d7ed 100644 --- a/crates/js-component-bindgen/src/function_bindgen.rs +++ b/crates/js-component-bindgen/src/function_bindgen.rs @@ -86,6 +86,8 @@ pub struct FunctionBindgen<'a> { pub callee: &'a str, pub callee_resource_dynamic: bool, pub resolve: &'a Resolve, + pub is_async: bool, + pub use_asyncify: bool, } impl FunctionBindgen<'_> { @@ -1048,7 +1050,14 @@ impl Bindgen for FunctionBindgen<'_> { Instruction::CallWasm { sig, .. } => { let sig_results_length = sig.results.len(); self.bind_results(sig_results_length, results); - uwriteln!(self.src, "{}({});", self.callee, operands.join(", ")); + if self.is_async { + if self.use_asyncify { + } else { + uwriteln!(self.src, "await WebAssembly.promising({})({});", self.callee, operands.join(", ")); + } + } else { + uwriteln!(self.src, "{}({});", self.callee, operands.join(", ")); + } if let Some(prefix) = self.tracing_prefix { let to_result_string = self.intrinsic(Intrinsic::ToResultString); @@ -1066,15 +1075,16 @@ impl Bindgen for FunctionBindgen<'_> { Instruction::CallInterface { func } => { let results_length = func.results.len(); + let if_async_await = if self.is_async { "await " } else { "" }; let call = if self.callee_resource_dynamic { format!( - "{}.{}({})", + "{if_async_await}{}.{}({})", operands[0], self.callee, operands[1..].join(", ") ) } else { - format!("{}({})", self.callee, operands.join(", ")) + format!("{if_async_await}{}({})", self.callee, operands.join(", ")) }; if self.err == ErrHandling::ResultCatchHandler { // result<_, string> allows JS error coercion only, while diff --git a/crates/js-component-bindgen/src/lib.rs b/crates/js-component-bindgen/src/lib.rs index 587babb79..a932ec391 100644 --- a/crates/js-component-bindgen/src/lib.rs +++ b/crates/js-component-bindgen/src/lib.rs @@ -8,7 +8,7 @@ pub mod function_bindgen; pub mod intrinsics; pub mod names; pub mod source; -pub use transpile_bindgen::{BindingsMode, InstantiationMode, TranspileOpts}; +pub use transpile_bindgen::{AsyncMode, BindingsMode, InstantiationMode, TranspileOpts}; use anyhow::Result; use transpile_bindgen::transpile_bindgen; diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index 085a42f0c..14211e703 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -11,7 +11,7 @@ use crate::{uwrite, uwriteln}; use base64::{engine::general_purpose, Engine as _}; use heck::*; use std::cell::RefCell; -use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::fmt::Write; use std::mem; use wasmtime_environ::component::{ExportIndex, NameMap, NameMapNoIntern, Transcode}; @@ -68,6 +68,23 @@ pub struct TranspileOpts { /// Whether to output core Wasm utilizing multi-memory or to polyfill /// this handling. pub multi_memory: bool, + /// Configure whether to use `async` imports or exports with + /// JavaScript Promise Integration (JSPI) or Asyncify. + pub async_mode: Option, +} + +#[derive(Default, Clone, Debug)] +pub enum AsyncMode { + #[default] + Sync, + JavaScriptPromiseIntegration { + imports: Vec, + exports: Vec, + }, + Asyncify { + imports: Vec, + exports: Vec, + }, } #[derive(Default, Clone, Debug)] @@ -144,6 +161,20 @@ pub fn transpile_bindgen( // bindings is the actual `instantiate` method itself, created by this // structure. + let (use_asyncify, async_imports, async_exports) = match opts.async_mode.clone() { + None | Some(AsyncMode::Sync) => (false, Default::default(), Default::default()), + Some(AsyncMode::JavaScriptPromiseIntegration { imports, exports }) => ( + false, + imports.into_iter().collect(), + exports.into_iter().collect(), + ), + Some(AsyncMode::Asyncify { imports, exports }) => ( + true, + imports.into_iter().collect(), + exports.into_iter().collect(), + ), + }; + let mut instantiator = Instantiator { src: Source::default(), sizes: SizeAlign::default(), @@ -155,6 +186,9 @@ pub fn transpile_bindgen( translation: component, component: &component.component, types, + use_asyncify, + async_imports, + async_exports, imports: Default::default(), exports: Default::default(), lowering_options: Default::default(), @@ -441,6 +475,9 @@ struct Instantiator<'a, 'b> { /// Instance flags which references have been emitted externally at least once. used_instance_flags: RefCell>, defined_resource_classes: BTreeSet, + use_asyncify: bool, + async_imports: HashSet, + async_exports: HashSet, lowering_options: PrimaryMap, } @@ -1079,6 +1116,14 @@ impl<'a> Instantiator<'a, '_> { // nested interfaces only currently possible through mapping let (import_specifier, maybe_iface_member) = map_import(&self.gen.opts.map, import_name); + uwrite!(self.src.js, "\n// import_name = {import_name:?}"); + uwrite!(self.src.js, "\n// world_key = {world_key:?}"); + uwrite!(self.src.js, "\n// import_specifier = {import_specifier:?}"); + uwrite!( + self.src.js, + "\n// maybe_iface_member = {maybe_iface_member:?}" + ); + let (func, func_name, iface_name) = match &self.resolve.worlds[self.world].imports[world_key] { WorldItem::Function(func) => { @@ -1098,6 +1143,18 @@ impl<'a> Instantiator<'a, '_> { WorldItem::Type(_) => unreachable!(), }; + uwrite!(self.src.js, "\n// func = {func:?}"); + uwrite!(self.src.js, "\n// func_name = {func_name:?}"); + uwrite!(self.src.js, "\n// iface_name = {iface_name:?}"); + + let is_async = self + .async_imports + .contains(&format!("{import_name}#{func_name}")) + || self + .async_imports + .contains(&format!("{import_specifier}#{func_name}")); + uwrite!(self.src.js, "\n// is_async = {is_async:?}\n"); + let mut resource_map = ResourceMap::new(); self.create_resource_fn_map(func, func_ty, &mut resource_map); @@ -1156,7 +1213,18 @@ impl<'a> Instantiator<'a, '_> { .len(); match self.gen.opts.import_bindings { None | Some(BindingsMode::Js) | Some(BindingsMode::Hybrid) => { - uwrite!(self.src.js, "\nfunction trampoline{}", trampoline.as_u32()); + if is_async { + // TODO + if !self.use_asyncify { + uwrite!( + self.src.js, + "\nconst trampoline{} = new WebAssembly.Suspending(async function", + trampoline.as_u32() + ); + } + } else { + uwrite!(self.src.js, "\nfunction trampoline{}", trampoline.as_u32()); + } self.bindgen( nparams, call_type, @@ -1170,8 +1238,13 @@ impl<'a> Instantiator<'a, '_> { func, &resource_map, AbiVariant::GuestImport, + is_async, ); - uwriteln!(self.src.js, ""); + if is_async { + uwriteln!(self.src.js, ");"); + } else { + uwriteln!(self.src.js, ""); + } } Some(BindingsMode::Optimized) | Some(BindingsMode::DirectOptimized) => { uwriteln!(self.src.js, "let trampoline{};", trampoline.as_u32()); @@ -1521,7 +1594,9 @@ impl<'a> Instantiator<'a, '_> { func: &Function, resource_map: &ResourceMap, abi: AbiVariant, + is_async: bool, ) { + let use_asyncify = self.use_asyncify; let memory = opts.memory.map(|idx| format!("memory{}", idx.as_u32())); let realloc = opts.realloc.map(|idx| format!("realloc{}", idx.as_u32())); let post_return = opts @@ -1615,6 +1690,8 @@ impl<'a> Instantiator<'a, '_> { }, src: source::Source::default(), resolve: self.resolve, + is_async, + use_asyncify, }; abi::call( self.resolve, @@ -1895,14 +1972,27 @@ impl<'a> Instantiator<'a, '_> { export_name: &String, resource_map: &ResourceMap, ) { + let is_async = self.async_exports.contains(local_name) + || self + .async_exports + .contains(&format!("{export_name}#{local_name}")) + || export_name + .find('@') + .map(|i| { + self.async_exports + .contains(&format!("{}#{local_name}", export_name.get(0..i).unwrap())) + }) + .unwrap_or(false); + let if_async = if is_async { "async " } else { "" }; + match func.kind { - FunctionKind::Freestanding => uwrite!(self.src.js, "\nfunction {local_name}"), + FunctionKind::Freestanding => uwrite!(self.src.js, "\n{if_async}function {local_name}"), FunctionKind::Method(_) => { self.ensure_local_resource_class(local_name.to_string()); let method_name = func.item_name().to_lower_camel_case(); uwrite!( self.src.js, - "\n{local_name}.prototype.{method_name} = function {}", + "\n{local_name}.prototype.{method_name} = {if_async}function {}", if !is_js_reserved_word(&method_name) { method_name.to_string() } else { @@ -1915,7 +2005,7 @@ impl<'a> Instantiator<'a, '_> { let method_name = func.item_name().to_lower_camel_case(); uwrite!( self.src.js, - "\n{local_name}.{method_name} = function {}", + "\n{local_name}.{method_name} = {if_async}function {}", if !is_js_reserved_word(&method_name) { method_name.to_string() } else { @@ -1953,6 +2043,7 @@ impl<'a> Instantiator<'a, '_> { func, resource_map, AbiVariant::GuestExport, + is_async, ); match func.kind { FunctionKind::Freestanding => self.src.js("\n"), From 0152f750f5f78fb5e09123b39578b4f8a12358b3 Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Fri, 25 Oct 2024 20:54:47 -0500 Subject: [PATCH 02/35] clean up --- crates/js-component-bindgen-component/src/lib.rs | 2 +- .../wit/js-component-bindgen.wit | 4 ++-- .../src/transpile_bindgen.rs | 16 ++-------------- 3 files changed, 5 insertions(+), 17 deletions(-) diff --git a/crates/js-component-bindgen-component/src/lib.rs b/crates/js-component-bindgen-component/src/lib.rs index 9cb2cee76..c282020fb 100644 --- a/crates/js-component-bindgen-component/src/lib.rs +++ b/crates/js-component-bindgen-component/src/lib.rs @@ -57,7 +57,7 @@ impl From for js_component_bindgen::AsyncMode { fn from(value: AsyncMode) -> Self { match value { AsyncMode::Sync => js_component_bindgen::AsyncMode::Sync, - AsyncMode::JavaScriptPromiseIntegration(AsyncImportsExports{ imports, exports }) => js_component_bindgen::AsyncMode::JavaScriptPromiseIntegration { imports, exports }, + AsyncMode::Jspi(AsyncImportsExports{ imports, exports }) => js_component_bindgen::AsyncMode::JavaScriptPromiseIntegration { imports, exports }, AsyncMode::Asyncify(AsyncImportsExports{ imports, exports }) => js_component_bindgen::AsyncMode::Asyncify { imports, exports }, } } diff --git a/crates/js-component-bindgen-component/wit/js-component-bindgen.wit b/crates/js-component-bindgen-component/wit/js-component-bindgen.wit index 80bb55caa..fa3c8e3ee 100644 --- a/crates/js-component-bindgen-component/wit/js-component-bindgen.wit +++ b/crates/js-component-bindgen-component/wit/js-component-bindgen.wit @@ -74,10 +74,10 @@ world js-component-bindgen { } variant async-mode { - /// the default sync mode + /// default to sync mode sync, /// use JavaScript Promise Integration (JSPI) - java-script-promise-integration(async-imports-exports), + jspi(async-imports-exports), /// use Asyncify asyncify(async-imports-exports), } diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index 14211e703..470edcd11 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -1116,14 +1116,6 @@ impl<'a> Instantiator<'a, '_> { // nested interfaces only currently possible through mapping let (import_specifier, maybe_iface_member) = map_import(&self.gen.opts.map, import_name); - uwrite!(self.src.js, "\n// import_name = {import_name:?}"); - uwrite!(self.src.js, "\n// world_key = {world_key:?}"); - uwrite!(self.src.js, "\n// import_specifier = {import_specifier:?}"); - uwrite!( - self.src.js, - "\n// maybe_iface_member = {maybe_iface_member:?}" - ); - let (func, func_name, iface_name) = match &self.resolve.worlds[self.world].imports[world_key] { WorldItem::Function(func) => { @@ -1143,17 +1135,12 @@ impl<'a> Instantiator<'a, '_> { WorldItem::Type(_) => unreachable!(), }; - uwrite!(self.src.js, "\n// func = {func:?}"); - uwrite!(self.src.js, "\n// func_name = {func_name:?}"); - uwrite!(self.src.js, "\n// iface_name = {iface_name:?}"); - let is_async = self .async_imports .contains(&format!("{import_name}#{func_name}")) || self .async_imports .contains(&format!("{import_specifier}#{func_name}")); - uwrite!(self.src.js, "\n// is_async = {is_async:?}\n"); let mut resource_map = ResourceMap::new(); self.create_resource_fn_map(func, func_ty, &mut resource_map); @@ -1215,7 +1202,8 @@ impl<'a> Instantiator<'a, '_> { None | Some(BindingsMode::Js) | Some(BindingsMode::Hybrid) => { if is_async { // TODO - if !self.use_asyncify { + if self.use_asyncify { + } else { uwrite!( self.src.js, "\nconst trampoline{} = new WebAssembly.Suspending(async function", From d9c0796e08f2c344546ffbc51d7f8956a2db9d17 Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Fri, 25 Oct 2024 21:54:12 -0500 Subject: [PATCH 03/35] clippy fixes --- crates/js-component-bindgen-component/src/lib.rs | 8 ++++++-- crates/js-component-bindgen/src/function_bindgen.rs | 7 ++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/js-component-bindgen-component/src/lib.rs b/crates/js-component-bindgen-component/src/lib.rs index c282020fb..b5af05f5c 100644 --- a/crates/js-component-bindgen-component/src/lib.rs +++ b/crates/js-component-bindgen-component/src/lib.rs @@ -57,8 +57,12 @@ impl From for js_component_bindgen::AsyncMode { fn from(value: AsyncMode) -> Self { match value { AsyncMode::Sync => js_component_bindgen::AsyncMode::Sync, - AsyncMode::Jspi(AsyncImportsExports{ imports, exports }) => js_component_bindgen::AsyncMode::JavaScriptPromiseIntegration { imports, exports }, - AsyncMode::Asyncify(AsyncImportsExports{ imports, exports }) => js_component_bindgen::AsyncMode::Asyncify { imports, exports }, + AsyncMode::Jspi(AsyncImportsExports { imports, exports }) => { + js_component_bindgen::AsyncMode::JavaScriptPromiseIntegration { imports, exports } + } + AsyncMode::Asyncify(AsyncImportsExports { imports, exports }) => { + js_component_bindgen::AsyncMode::Asyncify { imports, exports } + } } } } diff --git a/crates/js-component-bindgen/src/function_bindgen.rs b/crates/js-component-bindgen/src/function_bindgen.rs index 75c23d7ed..94f2cafe6 100644 --- a/crates/js-component-bindgen/src/function_bindgen.rs +++ b/crates/js-component-bindgen/src/function_bindgen.rs @@ -1053,7 +1053,12 @@ impl Bindgen for FunctionBindgen<'_> { if self.is_async { if self.use_asyncify { } else { - uwriteln!(self.src, "await WebAssembly.promising({})({});", self.callee, operands.join(", ")); + uwriteln!( + self.src, + "await WebAssembly.promising({})({});", + self.callee, + operands.join(", ") + ); } } else { uwriteln!(self.src, "{}({});", self.callee, operands.join(", ")); From 4fc199d009c69d8345946db9b619b858f2cea60c Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Sun, 27 Oct 2024 07:26:16 -0500 Subject: [PATCH 04/35] wip on asyncify --- crates/js-component-bindgen/src/function_bindgen.rs | 6 ++++++ crates/js-component-bindgen/src/transpile_bindgen.rs | 7 ++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/js-component-bindgen/src/function_bindgen.rs b/crates/js-component-bindgen/src/function_bindgen.rs index 94f2cafe6..a74141c70 100644 --- a/crates/js-component-bindgen/src/function_bindgen.rs +++ b/crates/js-component-bindgen/src/function_bindgen.rs @@ -1052,6 +1052,12 @@ impl Bindgen for FunctionBindgen<'_> { self.bind_results(sig_results_length, results); if self.is_async { if self.use_asyncify { + uwriteln!( + self.src, + "await {}({});", + self.callee, + operands.join(", ") + ); } else { uwriteln!( self.src, diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index 470edcd11..f9ad5f16a 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -1203,6 +1203,11 @@ impl<'a> Instantiator<'a, '_> { if is_async { // TODO if self.use_asyncify { + uwrite!( + self.src.js, + "\nasync function trampoline{}", + trampoline.as_u32() + ); } else { uwrite!( self.src.js, @@ -1228,7 +1233,7 @@ impl<'a> Instantiator<'a, '_> { AbiVariant::GuestImport, is_async, ); - if is_async { + if is_async && !self.use_asyncify { uwriteln!(self.src.js, ");"); } else { uwriteln!(self.src.js, ""); From 449ffef39900ec4dec6d8b237751dd2fb55d2690 Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Mon, 28 Oct 2024 09:55:45 -0500 Subject: [PATCH 05/35] prototyping --- .../js-component-bindgen/src/function_bindgen.rs | 2 +- .../js-component-bindgen/src/transpile_bindgen.rs | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/crates/js-component-bindgen/src/function_bindgen.rs b/crates/js-component-bindgen/src/function_bindgen.rs index a74141c70..42067072a 100644 --- a/crates/js-component-bindgen/src/function_bindgen.rs +++ b/crates/js-component-bindgen/src/function_bindgen.rs @@ -1054,7 +1054,7 @@ impl Bindgen for FunctionBindgen<'_> { if self.use_asyncify { uwriteln!( self.src, - "await {}({});", + "await asyncifyWrapExport({})({});", self.callee, operands.join(", ") ); diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index f9ad5f16a..81f1f00f7 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -583,6 +583,10 @@ impl<'a> Instantiator<'a, '_> { } } fn instantiate(&mut self) { + if self.use_asyncify { + uwriteln!(self.src.js, "// HERE"); + } + for (i, trampoline) in self.translation.trampolines.iter() { let Trampoline::LowerImport { index, @@ -1201,11 +1205,16 @@ impl<'a> Instantiator<'a, '_> { match self.gen.opts.import_bindings { None | Some(BindingsMode::Js) | Some(BindingsMode::Hybrid) => { if is_async { - // TODO if self.use_asyncify { + uwriteln!(self.src.js, "// import_name = {import_name:?}"); + uwriteln!(self.src.js, "// callee_name = {callee_name:?}"); + uwriteln!(self.src.js, "// options = {options:?}"); + uwriteln!(self.src.js, "// func = {func:?}"); + uwriteln!(self.src.js, "// resource_map = {resource_map:?}"); + uwrite!( self.src.js, - "\nasync function trampoline{}", + "\nconst trampoline{} = asyncifyWrapImport(async function", trampoline.as_u32() ); } else { @@ -1233,7 +1242,7 @@ impl<'a> Instantiator<'a, '_> { AbiVariant::GuestImport, is_async, ); - if is_async && !self.use_asyncify { + if is_async { uwriteln!(self.src.js, ");"); } else { uwriteln!(self.src.js, ""); From 94d160e849f2ffc3c34103dfa4c9ba006162f6b4 Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Mon, 28 Oct 2024 17:37:59 -0500 Subject: [PATCH 06/35] added asyncify logic --- .../src/transpile_bindgen.rs | 101 ++++++++++++++++-- 1 file changed, 93 insertions(+), 8 deletions(-) diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index 81f1f00f7..5180b08e2 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -584,7 +584,93 @@ impl<'a> Instantiator<'a, '_> { } fn instantiate(&mut self) { if self.use_asyncify { - uwriteln!(self.src.js, "// HERE"); + let (if_async, if_async_await) = + if let Some(InstantiationMode::Sync) = self.gen.opts.instantiation { + ("", "") + } else { + ("async ", "await ") + }; + uwrite!(self.src.js, " + let asyncifyPromise; + let asyncifyResolved; + const asyncifyModules = []; + + {if_async}function asyncifyInstantiate(module, imports) {{ + const instance = {if_async_await}instantiateCore(module, imports); + const memory = instance.exports.memory || (imports && imports.env && imports.env.memory); + const realloc = instance.exports.cabi_realloc || instance.exports.cabi_export_realloc; + + if (instance.exports.asyncify_get_state && memory && realloc) {{ + const address = realloc(0, 0, 4, 4096); + new Int32Array(memory.buffer, address).set([address + 8, address + 4096]); + asyncifyModules.push({{ instance, memory, address }}); + }} + + return instance; + }} + + function asyncifyState() {{ + return asyncifyModules[0]?.instance.exports.asyncify_get_state(); + }} + function asyncifyAssertNoneState() {{ + let state = asyncifyState(); + if (state !== 0) {{ + throw new Error(`reentrancy not supported, expected asyncify state '0' but found '${{state}}'`); + }} + }} + function asyncifyWrapImport(fn) {{ + return (...args) => {{ + if (asyncifyState() === 2) {{ + asyncifyModules.forEach(({{ instance }}) => {{ + instance.exports.asyncify_stop_rewind(); + }}); + + const ret = asyncifyResolved; + asyncifyResolved = undefined; + return ret; + }} + asyncifyAssertNoneState(); + let value = fn(...args); + + asyncifyModules.forEach(({{ instance, address }}) => {{ + instance.exports.asyncify_start_unwind(address); + }}); + + asyncifyPromise = value; + }}; + }} + + function asyncifyWrapExport(fn) {{ + return async (...args) => {{ + if (asyncifyModules.length === 0) {{ + throw new Error(`none of the Wasm modules were processed with wasm-opt asyncify`); + }} + asyncifyAssertNoneState(); + + let result = fn(...args); + + while (asyncifyState() === 1) {{ // unwinding + asyncifyModules.forEach(({{ instance }}) => {{ + instance.exports.asyncify_stop_unwind(); + }}); + + asyncifyResolved = await asyncifyPromise; + asyncifyPromise = undefined; + asyncifyAssertNoneState(); + + asyncifyModules.forEach(({{ instance, address }}) => {{ + instance.exports.asyncify_start_rewind(address); + }}); + + result = fn(...args); + }} + + asyncifyAssertNoneState(); + + return result; + }}; + }} + "); } for (i, trampoline) in self.translation.trampolines.iter() { @@ -1049,7 +1135,12 @@ impl<'a> Instantiator<'a, '_> { let i = self.instances.push(idx); let iu32 = i.as_u32(); - let instantiate = self.gen.intrinsic(Intrinsic::InstantiateCore); + let instantiate = if self.use_asyncify { + self.gen.all_intrinsics.insert(Intrinsic::InstantiateCore); + "asyncifyInstantiate" + } else { + &self.gen.intrinsic(Intrinsic::InstantiateCore) + }; uwriteln!(self.src.js, "let exports{iu32};"); match self.gen.opts.instantiation { @@ -1206,12 +1297,6 @@ impl<'a> Instantiator<'a, '_> { None | Some(BindingsMode::Js) | Some(BindingsMode::Hybrid) => { if is_async { if self.use_asyncify { - uwriteln!(self.src.js, "// import_name = {import_name:?}"); - uwriteln!(self.src.js, "// callee_name = {callee_name:?}"); - uwriteln!(self.src.js, "// options = {options:?}"); - uwriteln!(self.src.js, "// func = {func:?}"); - uwriteln!(self.src.js, "// resource_map = {resource_map:?}"); - uwrite!( self.src.js, "\nconst trampoline{} = asyncifyWrapImport(async function", From ed0dd1a9e8d2680f489b995009d8d74e219a6dbe Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Mon, 28 Oct 2024 18:25:16 -0500 Subject: [PATCH 07/35] clean up --- .../src/bindings.rs | 964 ------------------ 1 file changed, 964 deletions(-) delete mode 100644 crates/js-component-bindgen-component/src/bindings.rs diff --git a/crates/js-component-bindgen-component/src/bindings.rs b/crates/js-component-bindgen-component/src/bindings.rs deleted file mode 100644 index 1889827ff..000000000 --- a/crates/js-component-bindgen-component/src/bindings.rs +++ /dev/null @@ -1,964 +0,0 @@ -pub type Files = _rt::Vec<(_rt::String, _rt::Vec)>; -pub type Maps = _rt::Vec<(_rt::String, _rt::String)>; -#[derive(Clone, Copy)] -pub enum InstantiationMode { - Async, - Sync, -} -impl ::core::fmt::Debug for InstantiationMode { - fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - match self { - InstantiationMode::Async => { - f.debug_tuple("InstantiationMode::Async").finish() - } - InstantiationMode::Sync => f.debug_tuple("InstantiationMode::Sync").finish(), - } - } -} -#[derive(Clone, Copy)] -pub enum BindingsMode { - Js, - Hybrid, - Optimized, - DirectOptimized, -} -impl ::core::fmt::Debug for BindingsMode { - fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - match self { - BindingsMode::Js => f.debug_tuple("BindingsMode::Js").finish(), - BindingsMode::Hybrid => f.debug_tuple("BindingsMode::Hybrid").finish(), - BindingsMode::Optimized => f.debug_tuple("BindingsMode::Optimized").finish(), - BindingsMode::DirectOptimized => { - f.debug_tuple("BindingsMode::DirectOptimized").finish() - } - } - } -} -#[derive(Clone)] -pub struct GenerateOptions { - /// Name to use for the generated component - pub name: _rt::String, - /// Disables generation of `*.d.ts` files and instead only generates `*.js` - /// source files. - pub no_typescript: Option, - /// Provide a custom JS instantiation API for the component instead - /// of the direct importable native ESM output. - pub instantiation: Option, - /// Import bindings generation mode - pub import_bindings: Option, - /// Mappings of component import specifiers to JS import specifiers. - pub map: Option, - /// Enables all compat flags: --tla-compat. - pub compat: Option, - /// Disables compatibility in Node.js without a fetch global. - pub no_nodejs_compat: Option, - /// Set the cutoff byte size for base64 inlining core Wasm in instantiation mode - /// (set to 0 to disable all base64 inlining) - pub base64_cutoff: Option, - /// Enables compatibility for JS environments without top-level await support - /// via an async $init promise export to wait for instead. - pub tla_compat: Option, - /// Disable verification of component Wasm data structures when - /// lifting as a production optimization - pub valid_lifting_optimization: Option, - /// Whether or not to emit `tracing` calls on function entry/exit. - pub tracing: Option, - /// Whether to generate namespaced exports like `foo as "local:package/foo"`. - /// These exports can break typescript builds. - pub no_namespaced_exports: Option, - /// Whether to output core Wasm utilizing multi-memory or to polyfill - /// this handling. - pub multi_memory: Option, -} -impl ::core::fmt::Debug for GenerateOptions { - fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - f.debug_struct("GenerateOptions") - .field("name", &self.name) - .field("no-typescript", &self.no_typescript) - .field("instantiation", &self.instantiation) - .field("import-bindings", &self.import_bindings) - .field("map", &self.map) - .field("compat", &self.compat) - .field("no-nodejs-compat", &self.no_nodejs_compat) - .field("base64-cutoff", &self.base64_cutoff) - .field("tla-compat", &self.tla_compat) - .field("valid-lifting-optimization", &self.valid_lifting_optimization) - .field("tracing", &self.tracing) - .field("no-namespaced-exports", &self.no_namespaced_exports) - .field("multi-memory", &self.multi_memory) - .finish() - } -} -#[derive(Clone)] -pub enum Wit { - /// wit is provided as an inline WIT string - Source(_rt::String), - /// wit is provided from a component binary - Binary(_rt::Vec), - /// wit is provided from a filesystem path - Path(_rt::String), -} -impl ::core::fmt::Debug for Wit { - fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - match self { - Wit::Source(e) => f.debug_tuple("Wit::Source").field(e).finish(), - Wit::Binary(e) => f.debug_tuple("Wit::Binary").field(e).finish(), - Wit::Path(e) => f.debug_tuple("Wit::Path").field(e).finish(), - } - } -} -/// Enumerate enabled features -#[derive(Clone)] -pub enum EnabledFeatureSet { - /// Enable only the given list of features - List(_rt::Vec<_rt::String>), - /// Enable all features - All, -} -impl ::core::fmt::Debug for EnabledFeatureSet { - fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - match self { - EnabledFeatureSet::List(e) => { - f.debug_tuple("EnabledFeatureSet::List").field(e).finish() - } - EnabledFeatureSet::All => f.debug_tuple("EnabledFeatureSet::All").finish(), - } - } -} -#[derive(Clone)] -pub struct TypeGenerationOptions { - /// wit to generate typing from - pub wit: Wit, - /// world to generate typing for - pub world: Option<_rt::String>, - pub tla_compat: Option, - pub instantiation: Option, - pub map: Option, - /// Features that should be enabled as part of feature gating - pub features: Option, -} -impl ::core::fmt::Debug for TypeGenerationOptions { - fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - f.debug_struct("TypeGenerationOptions") - .field("wit", &self.wit) - .field("world", &self.world) - .field("tla-compat", &self.tla_compat) - .field("instantiation", &self.instantiation) - .field("map", &self.map) - .field("features", &self.features) - .finish() - } -} -#[repr(u8)] -#[derive(Clone, Copy, Eq, PartialEq)] -pub enum ExportType { - Function, - Instance, -} -impl ::core::fmt::Debug for ExportType { - fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - match self { - ExportType::Function => f.debug_tuple("ExportType::Function").finish(), - ExportType::Instance => f.debug_tuple("ExportType::Instance").finish(), - } - } -} -impl ExportType { - #[doc(hidden)] - pub unsafe fn _lift(val: u8) -> ExportType { - if !cfg!(debug_assertions) { - return ::core::mem::transmute(val); - } - match val { - 0 => ExportType::Function, - 1 => ExportType::Instance, - _ => panic!("invalid enum discriminant"), - } - } -} -#[derive(Clone)] -pub struct Transpiled { - pub files: Files, - pub imports: _rt::Vec<_rt::String>, - pub exports: _rt::Vec<(_rt::String, ExportType)>, -} -impl ::core::fmt::Debug for Transpiled { - fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - f.debug_struct("Transpiled") - .field("files", &self.files) - .field("imports", &self.imports) - .field("exports", &self.exports) - .finish() - } -} -#[doc(hidden)] -#[allow(non_snake_case)] -pub unsafe fn _export_generate_cabi(arg0: *mut u8) -> *mut u8 { - #[cfg(target_arch = "wasm32")] _rt::run_ctors_once(); - let l0 = *arg0.add(0).cast::<*mut u8>(); - let l1 = *arg0.add(4).cast::(); - let len2 = l1; - let l3 = *arg0.add(8).cast::<*mut u8>(); - let l4 = *arg0.add(12).cast::(); - let len5 = l4; - let bytes5 = _rt::Vec::from_raw_parts(l3.cast(), len5, len5); - let l6 = i32::from(*arg0.add(16).cast::()); - let l8 = i32::from(*arg0.add(18).cast::()); - let l11 = i32::from(*arg0.add(20).cast::()); - let l14 = i32::from(*arg0.add(24).cast::()); - let l24 = i32::from(*arg0.add(36).cast::()); - let l26 = i32::from(*arg0.add(38).cast::()); - let l28 = i32::from(*arg0.add(40).cast::()); - let l30 = i32::from(*arg0.add(48).cast::()); - let l32 = i32::from(*arg0.add(50).cast::()); - let l34 = i32::from(*arg0.add(52).cast::()); - let l36 = i32::from(*arg0.add(54).cast::()); - let l38 = i32::from(*arg0.add(56).cast::()); - let result40 = T::generate( - _rt::Vec::from_raw_parts(l0.cast(), len2, len2), - GenerateOptions { - name: _rt::string_lift(bytes5), - no_typescript: match l6 { - 0 => None, - 1 => { - let e = { - let l7 = i32::from(*arg0.add(17).cast::()); - _rt::bool_lift(l7 as u8) - }; - Some(e) - } - _ => _rt::invalid_enum_discriminant(), - }, - instantiation: match l8 { - 0 => None, - 1 => { - let e = { - let l9 = i32::from(*arg0.add(19).cast::()); - let v10 = match l9 { - 0 => InstantiationMode::Async, - n => { - debug_assert_eq!(n, 1, "invalid enum discriminant"); - InstantiationMode::Sync - } - }; - v10 - }; - Some(e) - } - _ => _rt::invalid_enum_discriminant(), - }, - import_bindings: match l11 { - 0 => None, - 1 => { - let e = { - let l12 = i32::from(*arg0.add(21).cast::()); - let v13 = match l12 { - 0 => BindingsMode::Js, - 1 => BindingsMode::Hybrid, - 2 => BindingsMode::Optimized, - n => { - debug_assert_eq!(n, 3, "invalid enum discriminant"); - BindingsMode::DirectOptimized - } - }; - v13 - }; - Some(e) - } - _ => _rt::invalid_enum_discriminant(), - }, - map: match l14 { - 0 => None, - 1 => { - let e = { - let l15 = *arg0.add(28).cast::<*mut u8>(); - let l16 = *arg0.add(32).cast::(); - let base23 = l15; - let len23 = l16; - let mut result23 = _rt::Vec::with_capacity(len23); - for i in 0..len23 { - let base = base23.add(i * 16); - let e23 = { - let l17 = *base.add(0).cast::<*mut u8>(); - let l18 = *base.add(4).cast::(); - let len19 = l18; - let bytes19 = _rt::Vec::from_raw_parts( - l17.cast(), - len19, - len19, - ); - let l20 = *base.add(8).cast::<*mut u8>(); - let l21 = *base.add(12).cast::(); - let len22 = l21; - let bytes22 = _rt::Vec::from_raw_parts( - l20.cast(), - len22, - len22, - ); - (_rt::string_lift(bytes19), _rt::string_lift(bytes22)) - }; - result23.push(e23); - } - _rt::cabi_dealloc(base23, len23 * 16, 4); - result23 - }; - Some(e) - } - _ => _rt::invalid_enum_discriminant(), - }, - compat: match l24 { - 0 => None, - 1 => { - let e = { - let l25 = i32::from(*arg0.add(37).cast::()); - _rt::bool_lift(l25 as u8) - }; - Some(e) - } - _ => _rt::invalid_enum_discriminant(), - }, - no_nodejs_compat: match l26 { - 0 => None, - 1 => { - let e = { - let l27 = i32::from(*arg0.add(39).cast::()); - _rt::bool_lift(l27 as u8) - }; - Some(e) - } - _ => _rt::invalid_enum_discriminant(), - }, - base64_cutoff: match l28 { - 0 => None, - 1 => { - let e = { - let l29 = *arg0.add(44).cast::(); - l29 as u32 - }; - Some(e) - } - _ => _rt::invalid_enum_discriminant(), - }, - tla_compat: match l30 { - 0 => None, - 1 => { - let e = { - let l31 = i32::from(*arg0.add(49).cast::()); - _rt::bool_lift(l31 as u8) - }; - Some(e) - } - _ => _rt::invalid_enum_discriminant(), - }, - valid_lifting_optimization: match l32 { - 0 => None, - 1 => { - let e = { - let l33 = i32::from(*arg0.add(51).cast::()); - _rt::bool_lift(l33 as u8) - }; - Some(e) - } - _ => _rt::invalid_enum_discriminant(), - }, - tracing: match l34 { - 0 => None, - 1 => { - let e = { - let l35 = i32::from(*arg0.add(53).cast::()); - _rt::bool_lift(l35 as u8) - }; - Some(e) - } - _ => _rt::invalid_enum_discriminant(), - }, - no_namespaced_exports: match l36 { - 0 => None, - 1 => { - let e = { - let l37 = i32::from(*arg0.add(55).cast::()); - _rt::bool_lift(l37 as u8) - }; - Some(e) - } - _ => _rt::invalid_enum_discriminant(), - }, - multi_memory: match l38 { - 0 => None, - 1 => { - let e = { - let l39 = i32::from(*arg0.add(57).cast::()); - _rt::bool_lift(l39 as u8) - }; - Some(e) - } - _ => _rt::invalid_enum_discriminant(), - }, - }, - ); - _rt::cabi_dealloc(arg0, 60, 4); - let ptr41 = _RET_AREA.0.as_mut_ptr().cast::(); - match result40 { - Ok(e) => { - *ptr41.add(0).cast::() = (0i32) as u8; - let Transpiled { files: files42, imports: imports42, exports: exports42 } = e; - let vec46 = files42; - let len46 = vec46.len(); - let layout46 = _rt::alloc::Layout::from_size_align_unchecked( - vec46.len() * 16, - 4, - ); - let result46 = if layout46.size() != 0 { - let ptr = _rt::alloc::alloc(layout46).cast::(); - if ptr.is_null() { - _rt::alloc::handle_alloc_error(layout46); - } - ptr - } else { - { ::core::ptr::null_mut() } - }; - for (i, e) in vec46.into_iter().enumerate() { - let base = result46.add(i * 16); - { - let (t43_0, t43_1) = e; - let vec44 = (t43_0.into_bytes()).into_boxed_slice(); - let ptr44 = vec44.as_ptr().cast::(); - let len44 = vec44.len(); - ::core::mem::forget(vec44); - *base.add(4).cast::() = len44; - *base.add(0).cast::<*mut u8>() = ptr44.cast_mut(); - let vec45 = (t43_1).into_boxed_slice(); - let ptr45 = vec45.as_ptr().cast::(); - let len45 = vec45.len(); - ::core::mem::forget(vec45); - *base.add(12).cast::() = len45; - *base.add(8).cast::<*mut u8>() = ptr45.cast_mut(); - } - } - *ptr41.add(8).cast::() = len46; - *ptr41.add(4).cast::<*mut u8>() = result46; - let vec48 = imports42; - let len48 = vec48.len(); - let layout48 = _rt::alloc::Layout::from_size_align_unchecked( - vec48.len() * 8, - 4, - ); - let result48 = if layout48.size() != 0 { - let ptr = _rt::alloc::alloc(layout48).cast::(); - if ptr.is_null() { - _rt::alloc::handle_alloc_error(layout48); - } - ptr - } else { - { ::core::ptr::null_mut() } - }; - for (i, e) in vec48.into_iter().enumerate() { - let base = result48.add(i * 8); - { - let vec47 = (e.into_bytes()).into_boxed_slice(); - let ptr47 = vec47.as_ptr().cast::(); - let len47 = vec47.len(); - ::core::mem::forget(vec47); - *base.add(4).cast::() = len47; - *base.add(0).cast::<*mut u8>() = ptr47.cast_mut(); - } - } - *ptr41.add(16).cast::() = len48; - *ptr41.add(12).cast::<*mut u8>() = result48; - let vec51 = exports42; - let len51 = vec51.len(); - let layout51 = _rt::alloc::Layout::from_size_align_unchecked( - vec51.len() * 12, - 4, - ); - let result51 = if layout51.size() != 0 { - let ptr = _rt::alloc::alloc(layout51).cast::(); - if ptr.is_null() { - _rt::alloc::handle_alloc_error(layout51); - } - ptr - } else { - { ::core::ptr::null_mut() } - }; - for (i, e) in vec51.into_iter().enumerate() { - let base = result51.add(i * 12); - { - let (t49_0, t49_1) = e; - let vec50 = (t49_0.into_bytes()).into_boxed_slice(); - let ptr50 = vec50.as_ptr().cast::(); - let len50 = vec50.len(); - ::core::mem::forget(vec50); - *base.add(4).cast::() = len50; - *base.add(0).cast::<*mut u8>() = ptr50.cast_mut(); - *base.add(8).cast::() = (t49_1.clone() as i32) as u8; - } - } - *ptr41.add(24).cast::() = len51; - *ptr41.add(20).cast::<*mut u8>() = result51; - } - Err(e) => { - *ptr41.add(0).cast::() = (1i32) as u8; - let vec52 = (e.into_bytes()).into_boxed_slice(); - let ptr52 = vec52.as_ptr().cast::(); - let len52 = vec52.len(); - ::core::mem::forget(vec52); - *ptr41.add(8).cast::() = len52; - *ptr41.add(4).cast::<*mut u8>() = ptr52.cast_mut(); - } - }; - ptr41 -} -#[doc(hidden)] -#[allow(non_snake_case)] -pub unsafe fn __post_return_generate(arg0: *mut u8) { - let l0 = i32::from(*arg0.add(0).cast::()); - match l0 { - 0 => { - let l1 = *arg0.add(4).cast::<*mut u8>(); - let l2 = *arg0.add(8).cast::(); - let base8 = l1; - let len8 = l2; - for i in 0..len8 { - let base = base8.add(i * 16); - { - let l3 = *base.add(0).cast::<*mut u8>(); - let l4 = *base.add(4).cast::(); - _rt::cabi_dealloc(l3, l4, 1); - let l5 = *base.add(8).cast::<*mut u8>(); - let l6 = *base.add(12).cast::(); - let base7 = l5; - let len7 = l6; - _rt::cabi_dealloc(base7, len7 * 1, 1); - } - } - _rt::cabi_dealloc(base8, len8 * 16, 4); - let l9 = *arg0.add(12).cast::<*mut u8>(); - let l10 = *arg0.add(16).cast::(); - let base13 = l9; - let len13 = l10; - for i in 0..len13 { - let base = base13.add(i * 8); - { - let l11 = *base.add(0).cast::<*mut u8>(); - let l12 = *base.add(4).cast::(); - _rt::cabi_dealloc(l11, l12, 1); - } - } - _rt::cabi_dealloc(base13, len13 * 8, 4); - let l14 = *arg0.add(20).cast::<*mut u8>(); - let l15 = *arg0.add(24).cast::(); - let base18 = l14; - let len18 = l15; - for i in 0..len18 { - let base = base18.add(i * 12); - { - let l16 = *base.add(0).cast::<*mut u8>(); - let l17 = *base.add(4).cast::(); - _rt::cabi_dealloc(l16, l17, 1); - } - } - _rt::cabi_dealloc(base18, len18 * 12, 4); - } - _ => { - let l19 = *arg0.add(4).cast::<*mut u8>(); - let l20 = *arg0.add(8).cast::(); - _rt::cabi_dealloc(l19, l20, 1); - } - } -} -#[doc(hidden)] -#[allow(non_snake_case)] -pub unsafe fn _export_generate_types_cabi(arg0: *mut u8) -> *mut u8 { - #[cfg(target_arch = "wasm32")] _rt::run_ctors_once(); - let l0 = *arg0.add(0).cast::<*mut u8>(); - let l1 = *arg0.add(4).cast::(); - let len2 = l1; - let bytes2 = _rt::Vec::from_raw_parts(l0.cast(), len2, len2); - let l3 = i32::from(*arg0.add(8).cast::()); - let v13 = match l3 { - 0 => { - let e13 = { - let l4 = *arg0.add(12).cast::<*mut u8>(); - let l5 = *arg0.add(16).cast::(); - let len6 = l5; - let bytes6 = _rt::Vec::from_raw_parts(l4.cast(), len6, len6); - _rt::string_lift(bytes6) - }; - Wit::Source(e13) - } - 1 => { - let e13 = { - let l7 = *arg0.add(12).cast::<*mut u8>(); - let l8 = *arg0.add(16).cast::(); - let len9 = l8; - _rt::Vec::from_raw_parts(l7.cast(), len9, len9) - }; - Wit::Binary(e13) - } - n => { - debug_assert_eq!(n, 2, "invalid enum discriminant"); - let e13 = { - let l10 = *arg0.add(12).cast::<*mut u8>(); - let l11 = *arg0.add(16).cast::(); - let len12 = l11; - let bytes12 = _rt::Vec::from_raw_parts(l10.cast(), len12, len12); - _rt::string_lift(bytes12) - }; - Wit::Path(e13) - } - }; - let l14 = i32::from(*arg0.add(20).cast::()); - let l18 = i32::from(*arg0.add(32).cast::()); - let l20 = i32::from(*arg0.add(34).cast::()); - let l23 = i32::from(*arg0.add(36).cast::()); - let l33 = i32::from(*arg0.add(48).cast::()); - let result42 = T::generate_types( - _rt::string_lift(bytes2), - TypeGenerationOptions { - wit: v13, - world: match l14 { - 0 => None, - 1 => { - let e = { - let l15 = *arg0.add(24).cast::<*mut u8>(); - let l16 = *arg0.add(28).cast::(); - let len17 = l16; - let bytes17 = _rt::Vec::from_raw_parts(l15.cast(), len17, len17); - _rt::string_lift(bytes17) - }; - Some(e) - } - _ => _rt::invalid_enum_discriminant(), - }, - tla_compat: match l18 { - 0 => None, - 1 => { - let e = { - let l19 = i32::from(*arg0.add(33).cast::()); - _rt::bool_lift(l19 as u8) - }; - Some(e) - } - _ => _rt::invalid_enum_discriminant(), - }, - instantiation: match l20 { - 0 => None, - 1 => { - let e = { - let l21 = i32::from(*arg0.add(35).cast::()); - let v22 = match l21 { - 0 => InstantiationMode::Async, - n => { - debug_assert_eq!(n, 1, "invalid enum discriminant"); - InstantiationMode::Sync - } - }; - v22 - }; - Some(e) - } - _ => _rt::invalid_enum_discriminant(), - }, - map: match l23 { - 0 => None, - 1 => { - let e = { - let l24 = *arg0.add(40).cast::<*mut u8>(); - let l25 = *arg0.add(44).cast::(); - let base32 = l24; - let len32 = l25; - let mut result32 = _rt::Vec::with_capacity(len32); - for i in 0..len32 { - let base = base32.add(i * 16); - let e32 = { - let l26 = *base.add(0).cast::<*mut u8>(); - let l27 = *base.add(4).cast::(); - let len28 = l27; - let bytes28 = _rt::Vec::from_raw_parts( - l26.cast(), - len28, - len28, - ); - let l29 = *base.add(8).cast::<*mut u8>(); - let l30 = *base.add(12).cast::(); - let len31 = l30; - let bytes31 = _rt::Vec::from_raw_parts( - l29.cast(), - len31, - len31, - ); - (_rt::string_lift(bytes28), _rt::string_lift(bytes31)) - }; - result32.push(e32); - } - _rt::cabi_dealloc(base32, len32 * 16, 4); - result32 - }; - Some(e) - } - _ => _rt::invalid_enum_discriminant(), - }, - features: match l33 { - 0 => None, - 1 => { - let e = { - let l34 = i32::from(*arg0.add(52).cast::()); - let v41 = match l34 { - 0 => { - let e41 = { - let l35 = *arg0.add(56).cast::<*mut u8>(); - let l36 = *arg0.add(60).cast::(); - let base40 = l35; - let len40 = l36; - let mut result40 = _rt::Vec::with_capacity(len40); - for i in 0..len40 { - let base = base40.add(i * 8); - let e40 = { - let l37 = *base.add(0).cast::<*mut u8>(); - let l38 = *base.add(4).cast::(); - let len39 = l38; - let bytes39 = _rt::Vec::from_raw_parts( - l37.cast(), - len39, - len39, - ); - _rt::string_lift(bytes39) - }; - result40.push(e40); - } - _rt::cabi_dealloc(base40, len40 * 8, 4); - result40 - }; - EnabledFeatureSet::List(e41) - } - n => { - debug_assert_eq!(n, 1, "invalid enum discriminant"); - EnabledFeatureSet::All - } - }; - v41 - }; - Some(e) - } - _ => _rt::invalid_enum_discriminant(), - }, - }, - ); - _rt::cabi_dealloc(arg0, 64, 4); - let ptr43 = _RET_AREA.0.as_mut_ptr().cast::(); - match result42 { - Ok(e) => { - *ptr43.add(0).cast::() = (0i32) as u8; - let vec47 = e; - let len47 = vec47.len(); - let layout47 = _rt::alloc::Layout::from_size_align_unchecked( - vec47.len() * 16, - 4, - ); - let result47 = if layout47.size() != 0 { - let ptr = _rt::alloc::alloc(layout47).cast::(); - if ptr.is_null() { - _rt::alloc::handle_alloc_error(layout47); - } - ptr - } else { - { ::core::ptr::null_mut() } - }; - for (i, e) in vec47.into_iter().enumerate() { - let base = result47.add(i * 16); - { - let (t44_0, t44_1) = e; - let vec45 = (t44_0.into_bytes()).into_boxed_slice(); - let ptr45 = vec45.as_ptr().cast::(); - let len45 = vec45.len(); - ::core::mem::forget(vec45); - *base.add(4).cast::() = len45; - *base.add(0).cast::<*mut u8>() = ptr45.cast_mut(); - let vec46 = (t44_1).into_boxed_slice(); - let ptr46 = vec46.as_ptr().cast::(); - let len46 = vec46.len(); - ::core::mem::forget(vec46); - *base.add(12).cast::() = len46; - *base.add(8).cast::<*mut u8>() = ptr46.cast_mut(); - } - } - *ptr43.add(8).cast::() = len47; - *ptr43.add(4).cast::<*mut u8>() = result47; - } - Err(e) => { - *ptr43.add(0).cast::() = (1i32) as u8; - let vec48 = (e.into_bytes()).into_boxed_slice(); - let ptr48 = vec48.as_ptr().cast::(); - let len48 = vec48.len(); - ::core::mem::forget(vec48); - *ptr43.add(8).cast::() = len48; - *ptr43.add(4).cast::<*mut u8>() = ptr48.cast_mut(); - } - }; - ptr43 -} -#[doc(hidden)] -#[allow(non_snake_case)] -pub unsafe fn __post_return_generate_types(arg0: *mut u8) { - let l0 = i32::from(*arg0.add(0).cast::()); - match l0 { - 0 => { - let l1 = *arg0.add(4).cast::<*mut u8>(); - let l2 = *arg0.add(8).cast::(); - let base8 = l1; - let len8 = l2; - for i in 0..len8 { - let base = base8.add(i * 16); - { - let l3 = *base.add(0).cast::<*mut u8>(); - let l4 = *base.add(4).cast::(); - _rt::cabi_dealloc(l3, l4, 1); - let l5 = *base.add(8).cast::<*mut u8>(); - let l6 = *base.add(12).cast::(); - let base7 = l5; - let len7 = l6; - _rt::cabi_dealloc(base7, len7 * 1, 1); - } - } - _rt::cabi_dealloc(base8, len8 * 16, 4); - } - _ => { - let l9 = *arg0.add(4).cast::<*mut u8>(); - let l10 = *arg0.add(8).cast::(); - _rt::cabi_dealloc(l9, l10, 1); - } - } -} -pub trait Guest { - /// Generate the file structure for the transpiled of a component - /// into a JS embedding, returns the file list and imports and exports of the - /// output JS generation component - fn generate( - component: _rt::Vec, - options: GenerateOptions, - ) -> Result; - fn generate_types( - name: _rt::String, - options: TypeGenerationOptions, - ) -> Result; -} -#[doc(hidden)] -macro_rules! __export_world_js_component_bindgen_cabi { - ($ty:ident with_types_in $($path_to_types:tt)*) => { - const _ : () = { #[export_name = "generate"] unsafe extern "C" fn - export_generate(arg0 : * mut u8,) -> * mut u8 { $($path_to_types)*:: - _export_generate_cabi::<$ty > (arg0) } #[export_name = "cabi_post_generate"] - unsafe extern "C" fn _post_return_generate(arg0 : * mut u8,) { - $($path_to_types)*:: __post_return_generate::<$ty > (arg0) } #[export_name = - "generate-types"] unsafe extern "C" fn export_generate_types(arg0 : * mut u8,) -> - * mut u8 { $($path_to_types)*:: _export_generate_types_cabi::<$ty > (arg0) } - #[export_name = "cabi_post_generate-types"] unsafe extern "C" fn - _post_return_generate_types(arg0 : * mut u8,) { $($path_to_types)*:: - __post_return_generate_types::<$ty > (arg0) } }; - }; -} -#[doc(hidden)] -pub(crate) use __export_world_js_component_bindgen_cabi; -#[repr(align(4))] -struct _RetArea([::core::mem::MaybeUninit; 28]); -static mut _RET_AREA: _RetArea = _RetArea([::core::mem::MaybeUninit::uninit(); 28]); -mod _rt { - pub use alloc_crate::vec::Vec; - pub use alloc_crate::string::String; - #[cfg(target_arch = "wasm32")] - pub fn run_ctors_once() { - wit_bindgen_rt::run_ctors_once(); - } - pub unsafe fn string_lift(bytes: Vec) -> String { - if cfg!(debug_assertions) { - String::from_utf8(bytes).unwrap() - } else { - String::from_utf8_unchecked(bytes) - } - } - pub unsafe fn bool_lift(val: u8) -> bool { - if cfg!(debug_assertions) { - match val { - 0 => false, - 1 => true, - _ => panic!("invalid bool discriminant"), - } - } else { - val != 0 - } - } - pub unsafe fn invalid_enum_discriminant() -> T { - if cfg!(debug_assertions) { - panic!("invalid enum discriminant") - } else { - core::hint::unreachable_unchecked() - } - } - pub unsafe fn cabi_dealloc(ptr: *mut u8, size: usize, align: usize) { - if size == 0 { - return; - } - let layout = alloc::Layout::from_size_align_unchecked(size, align); - alloc::dealloc(ptr, layout); - } - pub use alloc_crate::alloc; - extern crate alloc as alloc_crate; -} -/// Generates `#[no_mangle]` functions to export the specified type as the -/// root implementation of all generated traits. -/// -/// For more information see the documentation of `wit_bindgen::generate!`. -/// -/// ```rust -/// # macro_rules! export{ ($($t:tt)*) => (); } -/// # trait Guest {} -/// struct MyType; -/// -/// impl Guest for MyType { -/// // ... -/// } -/// -/// export!(MyType); -/// ``` -#[allow(unused_macros)] -#[doc(hidden)] -macro_rules! __export_js_component_bindgen_impl { - ($ty:ident) => { - self::export!($ty with_types_in self); - }; - ($ty:ident with_types_in $($path_to_types_root:tt)*) => { - $($path_to_types_root)*:: __export_world_js_component_bindgen_cabi!($ty - with_types_in $($path_to_types_root)*); - }; -} -#[doc(inline)] -pub(crate) use __export_js_component_bindgen_impl as export; -#[cfg(target_arch = "wasm32")] -#[link_section = "component-type:wit-bindgen:0.30.0:js-component-bindgen:encoded world"] -#[doc(hidden)] -pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 927] = *b"\ -\0asm\x0d\0\x01\0\0\x19\x16wit-component-encoding\x04\0\x07\x94\x06\x01A\x02\x01\ -A'\x01p}\x01o\x02s\0\x01p\x01\x03\0\x05files\x03\0\x02\x01o\x02ss\x01p\x04\x03\0\ -\x04maps\x03\0\x05\x01q\x02\x05async\0\0\x04sync\0\0\x03\0\x12instantiation-mode\ -\x03\0\x07\x01q\x04\x02js\0\0\x06hybrid\0\0\x09optimized\0\0\x10direct-optimized\ -\0\0\x03\0\x0dbindings-mode\x03\0\x09\x01k\x7f\x01k\x08\x01k\x0a\x01k\x06\x01ky\x01\ -r\x0d\x04names\x0dno-typescript\x0b\x0dinstantiation\x0c\x0fimport-bindings\x0d\x03\ -map\x0e\x06compat\x0b\x10no-nodejs-compat\x0b\x0dbase64-cutoff\x0f\x0atla-compat\ -\x0b\x1avalid-lifting-optimization\x0b\x07tracing\x0b\x15no-namespaced-exports\x0b\ -\x0cmulti-memory\x0b\x03\0\x10generate-options\x03\0\x10\x01q\x03\x06source\x01s\ -\0\x06binary\x01\0\0\x04path\x01s\0\x03\0\x03wit\x03\0\x12\x01ps\x01q\x02\x04lis\ -t\x01\x14\0\x03all\0\0\x03\0\x13enabled-feature-set\x03\0\x15\x01ks\x01k\x16\x01\ -r\x06\x03wit\x13\x05world\x17\x0atla-compat\x0b\x0dinstantiation\x0c\x03map\x0e\x08\ -features\x18\x03\0\x17type-generation-options\x03\0\x19\x01m\x02\x08function\x08\ -instance\x03\0\x0bexport-type\x03\0\x1b\x01o\x02s\x1c\x01p\x1d\x01r\x03\x05files\ -\x03\x07imports\x14\x07exports\x1e\x03\0\x0atranspiled\x03\0\x1f\x01j\x01\x20\x01\ -s\x01@\x02\x09component\0\x07options\x11\0!\x04\0\x08generate\x01\"\x01j\x01\x03\ -\x01s\x01@\x02\x04names\x07options\x1a\0#\x04\0\x0egenerate-types\x01$\x04\x01/l\ -ocal:js-component-bindgen/js-component-bindgen\x04\0\x0b\x1a\x01\0\x14js-compone\ -nt-bindgen\x03\0\0\0G\x09producers\x01\x0cprocessed-by\x02\x0dwit-component\x070\ -.215.0\x10wit-bindgen-rust\x060.30.0"; -#[inline(never)] -#[doc(hidden)] -pub fn __link_custom_section_describing_imports() { - wit_bindgen_rt::maybe_link_cabi_realloc(); -} From f4b37b08f111c162fc1b2b0c8fe892841f6ef565 Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Tue, 29 Oct 2024 09:06:33 -0500 Subject: [PATCH 08/35] fix --- xtask/src/build/jco.rs | 1 + xtask/src/generate/wasi_types.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/xtask/src/build/jco.rs b/xtask/src/build/jco.rs index 8f2577eae..85024f2f7 100644 --- a/xtask/src/build/jco.rs +++ b/xtask/src/build/jco.rs @@ -84,6 +84,7 @@ fn transpile(component_path: &str, name: String, optimize: bool) -> Result<()> { no_namespaced_exports: true, multi_memory: true, import_bindings: Some(BindingsMode::Js), + async_mode: None, }; let transpiled = js_component_bindgen::transpile(&adapted_component, opts)?; diff --git a/xtask/src/generate/wasi_types.rs b/xtask/src/generate/wasi_types.rs index f4942d06c..0eb82429e 100644 --- a/xtask/src/generate/wasi_types.rs +++ b/xtask/src/generate/wasi_types.rs @@ -38,6 +38,7 @@ pub(crate) fn run() -> Result<()> { no_namespaced_exports: true, multi_memory: false, import_bindings: Some(BindingsMode::Js), + async_mode: None, }; let files = generate_types(name, resolve, world, opts)?; From 807c47bab8a8157fdd95cb4a811b50cf2e9651bd Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Tue, 29 Oct 2024 12:16:56 -0500 Subject: [PATCH 09/35] added jco CLI args for async mode --- src/cmd/opt.js | 17 +++++++++++++---- src/cmd/transpile.js | 20 +++++++++++++++++++- src/jco.js | 3 +++ 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/cmd/opt.js b/src/cmd/opt.js index 3a1ceeb6e..8cdfe9b3b 100644 --- a/src/cmd/opt.js +++ b/src/cmd/opt.js @@ -47,7 +47,7 @@ ${table([...compressionInfo.map(({ beforeBytes, afterBytes }, i) => { /** * * @param {Uint8Array} componentBytes - * @param {{ quiet: boolean, optArgs?: string[] }} options? + * @param {{ quiet: boolean, asyncMode?: string, optArgs?: string[] }} opts? * @returns {Promise<{ component: Uint8Array, compressionInfo: { beforeBytes: number, afterBytes: number }[] >} */ export async function optimizeComponent (componentBytes, opts) { @@ -67,8 +67,11 @@ export async function optimizeComponent (componentBytes, opts) { spinner.text = spinnerText(); } + const args = opts?.optArgs ? [...opts.optArgs] : ['-Os', '--low-memory-unused', '--enable-bulk-memory']; + if (opts.asyncMode === 'asyncify') args.push('--asyncify'); + const optimizedCoreModules = await Promise.all(coreModules.map(async ([coreModuleStart, coreModuleEnd]) => { - const optimized = wasmOpt(componentBytes.subarray(coreModuleStart, coreModuleEnd), opts?.optArgs); + const optimized = wasmOpt(componentBytes.subarray(coreModuleStart, coreModuleEnd), args); if (spinner) { completed++; spinner.text = spinnerText(); @@ -76,7 +79,12 @@ export async function optimizeComponent (componentBytes, opts) { return optimized; })); - let outComponentBytes = new Uint8Array(componentBytes.byteLength); + // With the optional asyncify pass, the size may increase rather than shrink + const previousModulesTotalSize = coreModules.reduce((total, [coreModuleStart, coreModuleEnd]) => total + (coreModuleEnd - coreModuleStart), 0); + const optimizedModulesTotalSize = optimizedCoreModules.reduce((total, buf) => total + buf.byteLength, 0); + const sizeChange = optimizedModulesTotalSize - previousModulesTotalSize; + + let outComponentBytes = new Uint8Array(componentBytes.byteLength + sizeChange); let nextReadPos = 0, nextWritePos = 0; for (let i = 0; i < coreModules.length; i++) { const [coreModuleStart, coreModuleEnd] = coreModules[i]; @@ -130,9 +138,10 @@ export async function optimizeComponent (componentBytes, opts) { /** * @param {Uint8Array} source + * @param {Array} args * @returns {Promise} */ -async function wasmOpt(source, args = ['-O1', '--low-memory-unused', '--enable-bulk-memory']) { +async function wasmOpt(source, args) { const wasmOptPath = fileURLToPath(import.meta.resolve('binaryen/bin/wasm-opt')); try { diff --git a/src/cmd/transpile.js b/src/cmd/transpile.js index 6a58dda17..3d4e1e9e6 100644 --- a/src/cmd/transpile.js +++ b/src/cmd/transpile.js @@ -26,6 +26,9 @@ export async function types (witPath, opts) { * worldName?: string, * instantiation?: 'async' | 'sync', * tlaCompat?: bool, + * asyncMode?: string, + * asyncImports?: string[], + * asyncExports?: string[], * outDir?: string, * features?: string[] | 'all', * }} opts @@ -98,6 +101,7 @@ export async function transpile (componentPath, opts, program) { opts.name = basename(componentPath.slice(0, -extname(componentPath).length || Infinity)); if (opts.map) opts.map = Object.fromEntries(opts.map.map(mapping => mapping.split('='))); + const { files } = await transpileComponent(component, opts); await writeFiles(files, opts.quiet ? false : 'Transpiled JS Component Files'); } @@ -126,6 +130,9 @@ async function wasm2Js (source) { * instantiation?: 'async' | 'sync', * importBindings?: 'js' | 'optimized' | 'hybrid' | 'direct-optimized', * map?: Record, + * asyncMode?: string, + * asyncImports?: string[], + * asyncExports?: string[], * validLiftingOptimization?: bool, * tracing?: bool, * nodejsCompat?: bool, @@ -148,7 +155,7 @@ export async function transpileComponent (component, opts = {}) { let spinner; const showSpinner = getShowSpinner(); - if (opts.optimize) { + if (opts.optimize || opts.asyncMode === 'asyncify') { if (showSpinner) setShowSpinner(true); ({ component } = await optimizeComponent(component, opts)); } @@ -176,10 +183,21 @@ export async function transpileComponent (component, opts = {}) { instantiation = { tag: 'async' }; } + const asyncMode = !opts.asyncMode || opts.asyncMode === 'sync' ? + null : + { + tag: opts.asyncMode, + val: { + imports: opts.asyncImports || [], + exports: opts.asyncExports || [], + }, + }; + let { files, imports, exports } = generate(component, { name: opts.name ?? 'component', map: Object.entries(opts.map ?? {}), instantiation, + asyncMode, importBindings: opts.importBindings ? { tag: opts.importBindings } : null, validLiftingOptimization: opts.validLiftingOptimization ?? false, tracing: opts.tracing ?? false, diff --git a/src/jco.js b/src/jco.js index 9237c5435..8d0f35f8e 100755 --- a/src/jco.js +++ b/src/jco.js @@ -51,6 +51,9 @@ program.command('transpile') .option('--no-typescript', 'do not output TypeScript .d.ts types') .option('--valid-lifting-optimization', 'optimize component binary validations assuming all lifted values are valid') .addOption(new Option('--import-bindings [mode]', 'bindings mode for imports').choices(['js', 'optimized', 'hybrid', 'direct-optimized']).preset('js')) + .addOption(new Option('--async-mode [mode]', 'use async imports and exports').choices(['sync', 'jspi', 'asyncify']).preset('sync')) + .option('--async-imports ', 'async component imports') + .option('--async-exports ', 'async component exports') .option('--tracing', 'emit `tracing` calls on function entry/exit') .option('-b, --base64-cutoff ', 'set the byte size under which core Wasm binaries will be inlined as base64', myParseInt) .option('--tla-compat', 'enables compatibility for JS environments without top-level await support via an async $init promise export') From 1652b89b65123be51135cbc5d0802ec7a9b5be53 Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Tue, 29 Oct 2024 12:20:55 -0500 Subject: [PATCH 10/35] fix --- src/cmd/opt.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cmd/opt.js b/src/cmd/opt.js index 8cdfe9b3b..5b0e954ca 100644 --- a/src/cmd/opt.js +++ b/src/cmd/opt.js @@ -84,7 +84,7 @@ export async function optimizeComponent (componentBytes, opts) { const optimizedModulesTotalSize = optimizedCoreModules.reduce((total, buf) => total + buf.byteLength, 0); const sizeChange = optimizedModulesTotalSize - previousModulesTotalSize; - let outComponentBytes = new Uint8Array(componentBytes.byteLength + sizeChange); + let outComponentBytes = new Uint8Array(sizeChange > 0 ? componentBytes.byteLength + sizeChange : componentBytes.byteLength); let nextReadPos = 0, nextWritePos = 0; for (let i = 0; i < coreModules.length; i++) { const [coreModuleStart, coreModuleEnd] = coreModules[i]; From cfb8af6b1b636d7e5bb9bde19a2ed7762a696bab Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Tue, 29 Oct 2024 12:29:11 -0500 Subject: [PATCH 11/35] updated docs --- src/jco.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/jco.js b/src/jco.js index 8d0f35f8e..63ce1a854 100755 --- a/src/jco.js +++ b/src/jco.js @@ -52,8 +52,8 @@ program.command('transpile') .option('--valid-lifting-optimization', 'optimize component binary validations assuming all lifted values are valid') .addOption(new Option('--import-bindings [mode]', 'bindings mode for imports').choices(['js', 'optimized', 'hybrid', 'direct-optimized']).preset('js')) .addOption(new Option('--async-mode [mode]', 'use async imports and exports').choices(['sync', 'jspi', 'asyncify']).preset('sync')) - .option('--async-imports ', 'async component imports') - .option('--async-exports ', 'async component exports') + .option('--async-imports ', 'async component imports (examples: "wasi:io/poll@0.2.0#poll", "wasi:io/poll@0.2.0#[method]pollable.block")') + .option('--async-exports ', 'async component exports (examples: "wasi:cli/run@0.2.0#run", "handle")') .option('--tracing', 'emit `tracing` calls on function entry/exit') .option('-b, --base64-cutoff ', 'set the byte size under which core Wasm binaries will be inlined as base64', myParseInt) .option('--tla-compat', 'enables compatibility for JS environments without top-level await support via an async $init promise export') From 11f2b0ec33710512b0c65798a9e906636027bda7 Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Tue, 29 Oct 2024 15:09:58 -0500 Subject: [PATCH 12/35] fixes and added default async imports and exports --- .../src/transpile_bindgen.rs | 10 ++++++--- src/cmd/opt.js | 2 +- src/cmd/transpile.js | 21 +++++++++++++++++++ src/jco.js | 6 ++++-- 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index 5180b08e2..b45c70558 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -1233,9 +1233,13 @@ impl<'a> Instantiator<'a, '_> { let is_async = self .async_imports .contains(&format!("{import_name}#{func_name}")) - || self - .async_imports - .contains(&format!("{import_specifier}#{func_name}")); + || import_name + .find('@') + .map(|i| { + self.async_imports + .contains(&format!("{}#{func_name}", import_name.get(0..i).unwrap())) + }) + .unwrap_or(false); let mut resource_map = ResourceMap::new(); self.create_resource_fn_map(func, func_ty, &mut resource_map); diff --git a/src/cmd/opt.js b/src/cmd/opt.js index 5b0e954ca..8aef4e157 100644 --- a/src/cmd/opt.js +++ b/src/cmd/opt.js @@ -68,7 +68,7 @@ export async function optimizeComponent (componentBytes, opts) { } const args = opts?.optArgs ? [...opts.optArgs] : ['-Os', '--low-memory-unused', '--enable-bulk-memory']; - if (opts.asyncMode === 'asyncify') args.push('--asyncify'); + if (opts?.asyncMode === 'asyncify') args.push('--asyncify'); const optimizedCoreModules = await Promise.all(coreModules.map(async ([coreModuleStart, coreModuleEnd]) => { const optimized = wasmOpt(componentBytes.subarray(coreModuleStart, coreModuleEnd), args); diff --git a/src/cmd/transpile.js b/src/cmd/transpile.js index 3d4e1e9e6..31785da34 100644 --- a/src/cmd/transpile.js +++ b/src/cmd/transpile.js @@ -14,6 +14,22 @@ import { platform } from 'node:process'; const isWindows = platform === 'win32'; +const DEFAULT_ASYNC_IMPORTS = [ + "wasi:io/poll#poll", + "wasi:io/poll#[method]pollable.block", + "wasi:io/streams#[method]input-stream.blocking-read", + "wasi:io/streams#[method]input-stream.blocking-skip", + "wasi:io/streams#[method]output-stream.blocking-flush", + "wasi:io/streams#[method]output-stream.blocking-write-and-flush", + "wasi:io/streams#[method]output-stream.blocking-write-zeroes-and-flush", + "wasi:io/streams#[method]output-stream.blocking-splice", +]; + +const DEFAULT_ASYNC_EXPORTS = [ + "wasi:cli/run#run", + "wasi:http/incoming-handler#handle", +]; + export async function types (witPath, opts) { const files = await typesComponent(witPath, opts); await writeFiles(files, opts.quiet ? false : 'Generated Type Files'); @@ -102,6 +118,11 @@ export async function transpile (componentPath, opts, program) { if (opts.map) opts.map = Object.fromEntries(opts.map.map(mapping => mapping.split('='))); + if (opts.defaultAsyncImports) + opts.asyncImports = DEFAULT_ASYNC_IMPORTS.concat(opts.asyncImports || []); + if (opts.defaultAsyncExports) + opts.asyncExports = DEFAULT_ASYNC_EXPORTS.concat(opts.asyncExports || []); + const { files } = await transpileComponent(component, opts); await writeFiles(files, opts.quiet ? false : 'Transpiled JS Component Files'); } diff --git a/src/jco.js b/src/jco.js index 63ce1a854..3df851b9b 100755 --- a/src/jco.js +++ b/src/jco.js @@ -52,8 +52,10 @@ program.command('transpile') .option('--valid-lifting-optimization', 'optimize component binary validations assuming all lifted values are valid') .addOption(new Option('--import-bindings [mode]', 'bindings mode for imports').choices(['js', 'optimized', 'hybrid', 'direct-optimized']).preset('js')) .addOption(new Option('--async-mode [mode]', 'use async imports and exports').choices(['sync', 'jspi', 'asyncify']).preset('sync')) - .option('--async-imports ', 'async component imports (examples: "wasi:io/poll@0.2.0#poll", "wasi:io/poll@0.2.0#[method]pollable.block")') - .option('--async-exports ', 'async component exports (examples: "wasi:cli/run@0.2.0#run", "handle")') + .option('--default-async-imports', 'default async component imports from WASI interfaces') + .option('--default-async-exports', 'default async component exports from WASI interfaces') + .option('--async-imports ', 'async component imports (examples: "wasi:io/poll@0.2.0#poll", "wasi:io/poll#[method]pollable.block")') + .option('--async-exports ', 'async component exports (examples: "wasi:cli/run@#run", "handle")') .option('--tracing', 'emit `tracing` calls on function entry/exit') .option('-b, --base64-cutoff ', 'set the byte size under which core Wasm binaries will be inlined as base64', myParseInt) .option('--tla-compat', 'enables compatibility for JS environments without top-level await support via an async $init promise export') From 0c16d11d6e2a43433c3cf2976a8de4f75996514f Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Wed, 30 Oct 2024 15:50:53 -0500 Subject: [PATCH 13/35] fix for the optimizeComponent byte range --- src/cmd/opt.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/cmd/opt.js b/src/cmd/opt.js index 8aef4e157..7c75e783e 100644 --- a/src/cmd/opt.js +++ b/src/cmd/opt.js @@ -67,7 +67,7 @@ export async function optimizeComponent (componentBytes, opts) { spinner.text = spinnerText(); } - const args = opts?.optArgs ? [...opts.optArgs] : ['-Os', '--low-memory-unused', '--enable-bulk-memory']; + const args = opts?.optArgs ? [...opts.optArgs] : ['-Oz', '--low-memory-unused', '--enable-bulk-memory']; if (opts?.asyncMode === 'asyncify') args.push('--asyncify'); const optimizedCoreModules = await Promise.all(coreModules.map(async ([coreModuleStart, coreModuleEnd]) => { @@ -84,7 +84,8 @@ export async function optimizeComponent (componentBytes, opts) { const optimizedModulesTotalSize = optimizedCoreModules.reduce((total, buf) => total + buf.byteLength, 0); const sizeChange = optimizedModulesTotalSize - previousModulesTotalSize; - let outComponentBytes = new Uint8Array(sizeChange > 0 ? componentBytes.byteLength + sizeChange : componentBytes.byteLength); + // Adds an extra 100 bytes to be safe. Sometimes an extra byte appears to be required. + let outComponentBytes = new Uint8Array(componentBytes.byteLength + sizeChange + 100); let nextReadPos = 0, nextWritePos = 0; for (let i = 0; i < coreModules.length; i++) { const [coreModuleStart, coreModuleEnd] = coreModules[i]; @@ -112,11 +113,11 @@ export async function optimizeComponent (componentBytes, opts) { nextReadPos = coreModuleEnd; } - outComponentBytes.set(componentBytes.subarray(nextReadPos, componentBytes.byteLength), nextWritePos); + outComponentBytes.set(componentBytes.subarray(nextReadPos), nextWritePos); nextWritePos += componentBytes.byteLength - nextReadPos; - nextReadPos += componentBytes.byteLength - nextReadPos; - outComponentBytes = outComponentBytes.subarray(0, outComponentBytes.length + nextWritePos - nextReadPos); + // truncate to the bytes written + outComponentBytes = outComponentBytes.subarray(0, nextWritePos); // verify it still parses ok try { From 65c533f200ea6f8f5bab479c9ca19dc99fb1ca54 Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Fri, 1 Nov 2024 07:35:36 -0500 Subject: [PATCH 14/35] added ts for async --- crates/js-component-bindgen/src/ts_bindgen.rs | 75 +++++++++++++++---- 1 file changed, 60 insertions(+), 15 deletions(-) diff --git a/crates/js-component-bindgen/src/ts_bindgen.rs b/crates/js-component-bindgen/src/ts_bindgen.rs index 8bd22994e..9c1c0a244 100644 --- a/crates/js-component-bindgen/src/ts_bindgen.rs +++ b/crates/js-component-bindgen/src/ts_bindgen.rs @@ -2,7 +2,7 @@ use crate::files::Files; use crate::function_bindgen::{array_ty, as_nullable, maybe_null}; use crate::names::{is_js_identifier, maybe_quote_id, LocalNames, RESERVED_KEYWORDS}; use crate::source::Source; -use crate::transpile_bindgen::{parse_world_key, InstantiationMode, TranspileOpts}; +use crate::transpile_bindgen::{parse_world_key, AsyncMode, InstantiationMode, TranspileOpts}; use crate::{dealias, feature_gate_allowed, uwrite, uwriteln}; use anyhow::{Context as _, Result}; use heck::*; @@ -29,6 +29,9 @@ struct TsBindgen { import_object: Source, /// TypeScript definitions which will become the export object export_object: Source, + + async_imports: HashSet, + async_exports: HashSet, } /// Used to generate a `*.d.ts` file for each imported and exported interface for @@ -53,12 +56,23 @@ pub fn ts_bindgen( opts: &TranspileOpts, files: &mut Files, ) -> Result<()> { + let (async_imports, async_exports) = match opts.async_mode.clone() { + None | Some(AsyncMode::Sync) => (Default::default(), Default::default()), + Some(AsyncMode::JavaScriptPromiseIntegration { imports, exports }) => { + (imports.into_iter().collect(), exports.into_iter().collect()) + } + Some(AsyncMode::Asyncify { imports, exports }) => { + (imports.into_iter().collect(), exports.into_iter().collect()) + } + }; let mut bindgen = TsBindgen { src: Source::default(), interface_names: LocalNames::default(), local_names: LocalNames::default(), import_object: Source::default(), export_object: Source::default(), + async_imports, + async_exports, }; let world = &resolve.worlds[id]; @@ -368,7 +382,7 @@ impl TsBindgen { files: &mut Files, ) -> String { // in case an imported type is used as an exported type - let local_name = self.generate_interface(name, resolve, id, files); + let local_name = self.generate_interface(name, resolve, id, files, false); uwriteln!( self.import_object, "{}: typeof {local_name},", @@ -388,7 +402,7 @@ impl TsBindgen { if iface_name == "*" { uwrite!(self.import_object, "{}: ", maybe_quote_id(import_name)); let name = resolve.interfaces[id].name.as_ref().unwrap(); - let local_name = self.generate_interface(name, resolve, id, files); + let local_name = self.generate_interface(name, resolve, id, files, false); uwriteln!(self.import_object, "typeof {local_name},",); return; } @@ -396,7 +410,7 @@ impl TsBindgen { uwriteln!(self.import_object, "{}: {{", maybe_quote_id(import_name)); for (iface_name, &id) in ifaces { let name = resolve.interfaces[id].name.as_ref().unwrap(); - let local_name = self.generate_interface(name, resolve, id, files); + let local_name = self.generate_interface(name, resolve, id, files, false); uwriteln!( self.import_object, "{}: typeof {local_name},", @@ -415,7 +429,7 @@ impl TsBindgen { ) { uwriteln!(self.import_object, "{}: {{", maybe_quote_id(import_name)); let mut gen = self.ts_interface(resolve, false); - gen.ts_func(func, true, false); + gen.ts_func(func, true, false, false); let src = gen.finish(); self.import_object.push_str(&src); uwriteln!(self.import_object, "}},"); @@ -429,7 +443,7 @@ impl TsBindgen { files: &mut Files, instantiation: bool, ) -> String { - let local_name = self.generate_interface(export_name, resolve, id, files); + let local_name = self.generate_interface(export_name, resolve, id, files, true); if instantiation { uwriteln!( self.export_object, @@ -460,7 +474,7 @@ impl TsBindgen { ) { let mut gen = self.ts_interface(resolve, false); for (_, func) in funcs { - gen.ts_func(func, false, declaration); + gen.ts_func(func, false, declaration, false); } let src = gen.finish(); self.export_object.push_str(&src); @@ -472,6 +486,7 @@ impl TsBindgen { resolve: &Resolve, id: InterfaceId, files: &mut Files, + is_world_export: bool, ) -> String { let iface = resolve .interfaces @@ -520,6 +535,11 @@ impl TsBindgen { return local_name; } + let async_funcs = if is_world_export { + self.async_exports.clone() + } else { + self.async_imports.clone() + }; let mut gen = self.ts_interface(resolve, false); uwriteln!(gen.src, "export namespace {camel} {{"); @@ -530,7 +550,16 @@ impl TsBindgen { { continue; } - gen.ts_func(func, false, true); + let func_name = &func.name; + let is_async = is_world_export && async_funcs.contains(func_name) + || async_funcs.contains(&format!("{id_name}#{func_name}")) + || id_name + .find('@') + .map(|i| { + async_funcs.contains(&format!("{}#{func_name}", id_name.get(0..i).unwrap())) + }) + .unwrap_or(false); + gen.ts_func(func, false, true, is_async); } // Export resources for the interface for (_, ty) in resolve.interfaces[id].types.iter() { @@ -729,7 +758,7 @@ impl<'a> TsInterface<'a> { self.src.push_str("]"); } - fn ts_func(&mut self, func: &Function, default: bool, declaration: bool) { + fn ts_func(&mut self, func: &Function, default: bool, declaration: bool, is_async: bool) { let iface = if let FunctionKind::Method(ty) | FunctionKind::Static(ty) | FunctionKind::Constructor(ty) = func.kind @@ -754,11 +783,15 @@ impl<'a> TsInterface<'a> { func.item_name().to_lower_camel_case() }; + let if_async = if is_async { "async " } else { "" }; + if declaration { match func.kind { FunctionKind::Freestanding => { if is_js_identifier(&out_name) { - iface.src.push_str(&format!("export function {out_name}")); + iface + .src + .push_str(&format!("export {if_async}function {out_name}")); } else { let (local_name, _) = iface.local_names.get_or_create(&out_name, &out_name); iface @@ -766,21 +799,23 @@ impl<'a> TsInterface<'a> { .push_str(&format!("export {{ {local_name} as {out_name} }};\n")); iface .src - .push_str(&format!("declare function {local_name}")); + .push_str(&format!("declare {if_async}function {local_name}")); }; } FunctionKind::Method(_) => { if is_js_identifier(&out_name) { - iface.src.push_str(&out_name); + iface.src.push_str(&format!("{if_async}{out_name}")); } else { - iface.src.push_str(&format!("'{out_name}'")); + iface.src.push_str(&format!("{if_async}'{out_name}'")); } } FunctionKind::Static(_) => { if is_js_identifier(&out_name) { - iface.src.push_str(&format!("static {out_name}")) + iface.src.push_str(&format!("static {if_async}{out_name}")) } else { - iface.src.push_str(&format!("static '{out_name}'")) + iface + .src + .push_str(&format!("static {if_async}'{out_name}'")) } } FunctionKind::Constructor(_) => iface.src.push_str("constructor"), @@ -825,6 +860,10 @@ impl<'a> TsInterface<'a> { } iface.src.push_str(": "); + if is_async { + iface.src.push_str("Promise<"); + } + if let Some((ok_ty, _)) = func.results.throws(iface.resolve) { iface.print_optional_ty(ok_ty); } else { @@ -843,6 +882,12 @@ impl<'a> TsInterface<'a> { } } } + + if is_async { + // closes `Promise<>` + iface.src.push_str(">"); + } + iface.src.push_str(format!("{}\n", end_character).as_str()); } From 9953b63d4fa6018bc4d41022bdc8cf6e356e2d4c Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Fri, 1 Nov 2024 08:48:27 -0500 Subject: [PATCH 15/35] fixed cli types subcommand --- crates/js-component-bindgen-component/src/lib.rs | 2 +- .../wit/js-component-bindgen.wit | 3 +++ src/cmd/transpile.js | 16 ++++++++++++++++ src/jco.js | 5 +++++ 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/crates/js-component-bindgen-component/src/lib.rs b/crates/js-component-bindgen-component/src/lib.rs index b5af05f5c..f6f6f6da9 100644 --- a/crates/js-component-bindgen-component/src/lib.rs +++ b/crates/js-component-bindgen-component/src/lib.rs @@ -175,7 +175,7 @@ impl Guest for JsComponentBindgenComponent { no_namespaced_exports: false, multi_memory: false, import_bindings: None, - async_mode: None, + async_mode: opts.async_mode.map(Into::into), }; let files = generate_types(name, resolve, world, opts).map_err(|e| e.to_string())?; diff --git a/crates/js-component-bindgen-component/wit/js-component-bindgen.wit b/crates/js-component-bindgen-component/wit/js-component-bindgen.wit index fa3c8e3ee..44879628c 100644 --- a/crates/js-component-bindgen-component/wit/js-component-bindgen.wit +++ b/crates/js-component-bindgen-component/wit/js-component-bindgen.wit @@ -109,6 +109,9 @@ world js-component-bindgen { map: option, /// Features that should be enabled as part of feature gating features: option, + /// Configure whether to use `async` imports or exports with + /// JavaScript Promise Integration (JSPI) or Asyncify. + async-mode: option, } enum export-type { diff --git a/src/cmd/transpile.js b/src/cmd/transpile.js index 31785da34..6e6389983 100644 --- a/src/cmd/transpile.js +++ b/src/cmd/transpile.js @@ -70,12 +70,28 @@ export async function typesComponent (witPath, opts) { features = { tag: 'list', val: opts.feature }; } + if (opts.defaultAsyncImports) + opts.asyncImports = DEFAULT_ASYNC_IMPORTS.concat(opts.asyncImports || []); + if (opts.defaultAsyncExports) + opts.asyncExports = DEFAULT_ASYNC_EXPORTS.concat(opts.asyncExports || []); + + const asyncMode = !opts.asyncMode || opts.asyncMode === 'sync' ? + null : + { + tag: opts.asyncMode, + val: { + imports: opts.asyncImports || [], + exports: opts.asyncExports || [], + }, + }; + return Object.fromEntries(generateTypes(name, { wit: { tag: 'path', val: (isWindows ? '//?/' : '') + resolve(witPath) }, instantiation, tlaCompat: opts.tlaCompat ?? false, world: opts.worldName, features, + asyncMode, }).map(([name, file]) => [`${outDir}${name}`, file])); } diff --git a/src/jco.js b/src/jco.js index 3df851b9b..188d5df2d 100755 --- a/src/jco.js +++ b/src/jco.js @@ -80,6 +80,11 @@ program.command('types') .requiredOption('-o, --out-dir ', 'output directory') .option('--tla-compat', 'generates types for the TLA compat output with an async $init promise export') .addOption(new Option('-I, --instantiation [mode]', 'type output for custom module instantiation').choices(['async', 'sync']).preset('async')) + .addOption(new Option('--async-mode [mode]', 'use async imports and exports').choices(['sync', 'jspi', 'asyncify']).preset('sync')) + .option('--default-async-imports', 'default async component imports from WASI interfaces') + .option('--default-async-exports', 'default async component exports from WASI interfaces') + .option('--async-imports ', 'async component imports (examples: "wasi:io/poll@0.2.0#poll", "wasi:io/poll#[method]pollable.block")') + .option('--async-exports ', 'async component exports (examples: "wasi:cli/run@#run", "handle")') .option('-q, --quiet', 'disable output summary') .option('--feature ', 'enable one specific WIT feature (repeatable)', collectOptions, []) .option('--all-features', 'enable all features') From d783d85aa44632bb56eb0929c9291e6a4bbd9700 Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Fri, 1 Nov 2024 09:08:47 -0500 Subject: [PATCH 16/35] updated asyncify instatiate --- .../js-component-bindgen/src/transpile_bindgen.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index b45c70558..15fd67813 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -600,9 +600,16 @@ impl<'a> Instantiator<'a, '_> { const memory = instance.exports.memory || (imports && imports.env && imports.env.memory); const realloc = instance.exports.cabi_realloc || instance.exports.cabi_export_realloc; - if (instance.exports.asyncify_get_state && memory && realloc) {{ - const address = realloc(0, 0, 4, 4096); - new Int32Array(memory.buffer, address).set([address + 8, address + 4096]); + if (instance.exports.asyncify_get_state && memory) {{ + let address; + if (realloc) {{ + address = realloc(0, 0, 4, 1024); + new Int32Array(memory.buffer, address).set([address + 8, address + 1024]); + }} else {{ + address = 16; + new Int32Array(memory.buffer, address).set([address + 8, address + 1024]); + }} + asyncifyModules.push({{ instance, memory, address }}); }} From c9d9daf2086a81343375a9898d29d2c54dd75cc9 Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Fri, 1 Nov 2024 10:12:59 -0500 Subject: [PATCH 17/35] updated docs --- docs/src/transpiling.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/src/transpiling.md b/docs/src/transpiling.md index fd88b0b26..9047b1d71 100644 --- a/docs/src/transpiling.md +++ b/docs/src/transpiling.md @@ -52,6 +52,9 @@ Options include: * `--valid-lifting-optimization`: Internal validations are removed assuming that core Wasm binaries are valid components, providing a minor output size saving. * `--tracing`: Emit tracing calls for all function entry and exits. * `--no-namespaced-exports`: Removes exports of the type `test as "test:flavorful/test"` which are not compatible with typescript +* `--async-mode [mode]`: For the component imports and exports, functions and methods on resources can be specified as `async`. The two options are `jspi` (JavaScript Promise Integration) and `asyncify` (Binaryen's `wasm-opt --asyncify`). +* `--async-imports `: Specify the component imports as `async`. Used with `--async-mode`. +* `--async-exports `: Specify the component exports as `async`. Used with `--async-mode`. ## Browser Support From 60ce5c02227c98bcdb4ec05fcb795a452a2c74bd Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Fri, 1 Nov 2024 11:25:41 -0500 Subject: [PATCH 18/35] fix --- crates/js-component-bindgen/src/transpile_bindgen.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index 15fd67813..1f3964f17 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -2070,15 +2070,18 @@ impl<'a> Instantiator<'a, '_> { export_name: &String, resource_map: &ResourceMap, ) { - let is_async = self.async_exports.contains(local_name) + let is_async = self.async_exports.contains(&func.name) || self .async_exports - .contains(&format!("{export_name}#{local_name}")) + .contains(&format!("{export_name}#{}", func.name)) || export_name .find('@') .map(|i| { - self.async_exports - .contains(&format!("{}#{local_name}", export_name.get(0..i).unwrap())) + self.async_exports.contains(&format!( + "{}#{}", + export_name.get(0..i).unwrap(), + func.name + )) }) .unwrap_or(false); let if_async = if is_async { "async " } else { "" }; From 09f702226bf166b05a31baefdeb20ba5590834a6 Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Fri, 1 Nov 2024 12:04:15 -0500 Subject: [PATCH 19/35] fixes --- crates/js-component-bindgen/src/ts_bindgen.rs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/crates/js-component-bindgen/src/ts_bindgen.rs b/crates/js-component-bindgen/src/ts_bindgen.rs index 9c1c0a244..99afb5726 100644 --- a/crates/js-component-bindgen/src/ts_bindgen.rs +++ b/crates/js-component-bindgen/src/ts_bindgen.rs @@ -467,14 +467,26 @@ impl TsBindgen { fn export_funcs( &mut self, resolve: &Resolve, - _world: WorldId, + world: WorldId, funcs: &[(String, &Function)], _files: &mut Files, declaration: bool, ) { + let async_exports = self.async_exports.clone(); + let id_name = &resolve.worlds[world].name; let mut gen = self.ts_interface(resolve, false); for (_, func) in funcs { - gen.ts_func(func, false, declaration, false); + let func_name = &func.name; + let is_async = async_exports.contains(func_name) + || async_exports.contains(&format!("{id_name}#{func_name}")) + || id_name + .find('@') + .map(|i| { + async_exports + .contains(&format!("{}#{func_name}", id_name.get(0..i).unwrap())) + }) + .unwrap_or(false); + gen.ts_func(func, false, declaration, is_async); } let src = gen.finish(); self.export_object.push_str(&src); @@ -821,9 +833,9 @@ impl<'a> TsInterface<'a> { FunctionKind::Constructor(_) => iface.src.push_str("constructor"), } } else if is_js_identifier(&out_name) { - iface.src.push_str(&out_name); + iface.src.push_str(&format!("{if_async}{out_name}")); } else { - iface.src.push_str(&format!("'{out_name}'")); + iface.src.push_str(&format!("{if_async}'{out_name}'")); } let end_character = if declaration { ';' } else { ',' }; From b75cc480b679fe39e7fb856e1c61d31bc43bd1d4 Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Fri, 1 Nov 2024 12:39:53 -0500 Subject: [PATCH 20/35] renamed if_async to maybe_async --- .../src/function_bindgen.rs | 10 +++++++--- .../src/transpile_bindgen.rs | 16 ++++++++------- crates/js-component-bindgen/src/ts_bindgen.rs | 20 ++++++++++--------- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/crates/js-component-bindgen/src/function_bindgen.rs b/crates/js-component-bindgen/src/function_bindgen.rs index 42067072a..f7c42e42f 100644 --- a/crates/js-component-bindgen/src/function_bindgen.rs +++ b/crates/js-component-bindgen/src/function_bindgen.rs @@ -1086,16 +1086,20 @@ impl Bindgen for FunctionBindgen<'_> { Instruction::CallInterface { func } => { let results_length = func.results.len(); - let if_async_await = if self.is_async { "await " } else { "" }; + let maybe_async_await = if self.is_async { "await " } else { "" }; let call = if self.callee_resource_dynamic { format!( - "{if_async_await}{}.{}({})", + "{maybe_async_await}{}.{}({})", operands[0], self.callee, operands[1..].join(", ") ) } else { - format!("{if_async_await}{}({})", self.callee, operands.join(", ")) + format!( + "{maybe_async_await}{}({})", + self.callee, + operands.join(", ") + ) }; if self.err == ErrHandling::ResultCatchHandler { // result<_, string> allows JS error coercion only, while diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index 1f3964f17..3aea69e9f 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -584,7 +584,7 @@ impl<'a> Instantiator<'a, '_> { } fn instantiate(&mut self) { if self.use_asyncify { - let (if_async, if_async_await) = + let (maybe_async, maybe_async_await) = if let Some(InstantiationMode::Sync) = self.gen.opts.instantiation { ("", "") } else { @@ -595,8 +595,8 @@ impl<'a> Instantiator<'a, '_> { let asyncifyResolved; const asyncifyModules = []; - {if_async}function asyncifyInstantiate(module, imports) {{ - const instance = {if_async_await}instantiateCore(module, imports); + {maybe_async}function asyncifyInstantiate(module, imports) {{ + const instance = {maybe_async_await}instantiateCore(module, imports); const memory = instance.exports.memory || (imports && imports.env && imports.env.memory); const realloc = instance.exports.cabi_realloc || instance.exports.cabi_export_realloc; @@ -2084,16 +2084,18 @@ impl<'a> Instantiator<'a, '_> { )) }) .unwrap_or(false); - let if_async = if is_async { "async " } else { "" }; + let maybe_async = if is_async { "async " } else { "" }; match func.kind { - FunctionKind::Freestanding => uwrite!(self.src.js, "\n{if_async}function {local_name}"), + FunctionKind::Freestanding => { + uwrite!(self.src.js, "\n{maybe_async}function {local_name}") + } FunctionKind::Method(_) => { self.ensure_local_resource_class(local_name.to_string()); let method_name = func.item_name().to_lower_camel_case(); uwrite!( self.src.js, - "\n{local_name}.prototype.{method_name} = {if_async}function {}", + "\n{local_name}.prototype.{method_name} = {maybe_async}function {}", if !is_js_reserved_word(&method_name) { method_name.to_string() } else { @@ -2106,7 +2108,7 @@ impl<'a> Instantiator<'a, '_> { let method_name = func.item_name().to_lower_camel_case(); uwrite!( self.src.js, - "\n{local_name}.{method_name} = {if_async}function {}", + "\n{local_name}.{method_name} = {maybe_async}function {}", if !is_js_reserved_word(&method_name) { method_name.to_string() } else { diff --git a/crates/js-component-bindgen/src/ts_bindgen.rs b/crates/js-component-bindgen/src/ts_bindgen.rs index 99afb5726..6b22a4c26 100644 --- a/crates/js-component-bindgen/src/ts_bindgen.rs +++ b/crates/js-component-bindgen/src/ts_bindgen.rs @@ -795,7 +795,7 @@ impl<'a> TsInterface<'a> { func.item_name().to_lower_camel_case() }; - let if_async = if is_async { "async " } else { "" }; + let maybe_async = if is_async { "async " } else { "" }; if declaration { match func.kind { @@ -803,7 +803,7 @@ impl<'a> TsInterface<'a> { if is_js_identifier(&out_name) { iface .src - .push_str(&format!("export {if_async}function {out_name}")); + .push_str(&format!("export {maybe_async}function {out_name}")); } else { let (local_name, _) = iface.local_names.get_or_create(&out_name, &out_name); iface @@ -811,31 +811,33 @@ impl<'a> TsInterface<'a> { .push_str(&format!("export {{ {local_name} as {out_name} }};\n")); iface .src - .push_str(&format!("declare {if_async}function {local_name}")); + .push_str(&format!("declare {maybe_async}function {local_name}")); }; } FunctionKind::Method(_) => { if is_js_identifier(&out_name) { - iface.src.push_str(&format!("{if_async}{out_name}")); + iface.src.push_str(&format!("{maybe_async}{out_name}")); } else { - iface.src.push_str(&format!("{if_async}'{out_name}'")); + iface.src.push_str(&format!("{maybe_async}'{out_name}'")); } } FunctionKind::Static(_) => { if is_js_identifier(&out_name) { - iface.src.push_str(&format!("static {if_async}{out_name}")) + iface + .src + .push_str(&format!("static {maybe_async}{out_name}")) } else { iface .src - .push_str(&format!("static {if_async}'{out_name}'")) + .push_str(&format!("static {maybe_async}'{out_name}'")) } } FunctionKind::Constructor(_) => iface.src.push_str("constructor"), } } else if is_js_identifier(&out_name) { - iface.src.push_str(&format!("{if_async}{out_name}")); + iface.src.push_str(&format!("{maybe_async}{out_name}")); } else { - iface.src.push_str(&format!("{if_async}'{out_name}'")); + iface.src.push_str(&format!("{maybe_async}'{out_name}'")); } let end_character = if declaration { ';' } else { ',' }; From a55c0d619fe7480f4a4196222bc04c5feaec76ab Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Fri, 1 Nov 2024 15:23:47 -0500 Subject: [PATCH 21/35] added first test --- test/cli.js | 41 ++++++++++++++++++ .../components/async_call.component.wasm | Bin 0 -> 21690 bytes 2 files changed, 41 insertions(+) create mode 100644 test/fixtures/components/async_call.component.wasm diff --git a/test/cli.js b/test/cli.js index 806aed85b..d6ca48625 100644 --- a/test/cli.js +++ b/test/cli.js @@ -16,6 +16,8 @@ const multiMemory = execArgv.includes("--experimental-wasm-multi-memory") ? ["--multi-memory"] : []; +const AsyncFunction = (async () => {}).constructor; + export async function cliTest(_fixtures) { suite("CLI", () => { var tmpDir; @@ -119,6 +121,45 @@ export async function cliTest(_fixtures) { ok(source.toString().includes("export { test")); }); + test("Transpile & Asyncify", async () => { + const name = "async_call"; + const { stderr } = await exec( + jcoPath, + "transpile", + `test/fixtures/components/${name}.component.wasm`, + `--name=${name}`, + "--valid-lifting-optimization", + "--tla-compat", + "--instantiation=async", + "--base64-cutoff=0", + "--async-mode=asyncify", + "--async-imports=something:test/test-interface#call-async", + "--async-exports=run-async", + "-o", + outDir + ); + strictEqual(stderr, ""); + await writeFile( + `${outDir}/package.json`, + JSON.stringify({ type: "module" }) + ); + const m = await import(`${outDir}/${name}.js`); + const inst = await m.instantiate( + (fileName) => readFile(`${outDir}/${fileName}`).then((file) => WebAssembly.compile(file)), + { + 'something:test/test-interface': { + callAsync: async () => "called async", + callSync: () => "called sync", + }, + }, + ); + strictEqual(inst.runSync instanceof AsyncFunction, false); + strictEqual(inst.runAsync instanceof AsyncFunction, true); + + strictEqual(inst.runSync(), "called sync"); + strictEqual(await inst.runAsync(), "called async"); + }); + test("Transpile & Optimize & Minify", async () => { const name = "flavorful"; const { stderr } = await exec( diff --git a/test/fixtures/components/async_call.component.wasm b/test/fixtures/components/async_call.component.wasm new file mode 100644 index 0000000000000000000000000000000000000000..a52483904a23a4cb7b3ca0507f60ee81b5e8cb7a GIT binary patch literal 21690 zcmch9eT*FEdEYxT`+0juE~#@uiC2wiYHlim{vgjhl41s(z>YtyMSf8t&%v2QZuPqxPVjp4*^jM7Znf{O#!!1 z3kMKdA?)w>yzk8J9*>da6c%+i^L{<=^Zh*U$BpWaFD)rcrQd6vb*wW=jT34i=x%JR zbjMdW14|`TR*Jf`P_(SO##@)d$;GwJ3lB`f@#KE~T3Oqigrh+>2$RbCr;n5ZC|^lKJ8q)cFnrRcl@?cq3}OmS+72wz;8Ar`Had6{ZN;Zx@jv1Jy?)o?Pd@-xG{?t;40PNreM z`P2=z`}v_B;7di$1u*-Jt4`U8qSanPk5jgCE#JytC&1j>p6v;C_upc+J20CI@cDnn z-UZvW59bbJjBB}e{t^;7dq9<6iavC~d{i?3Yx%QG$_?;^N28Vr~!N|>-tOo_Ba3PTi1lOXMg>_?7i}*|NFVGzrW$AT#-Hg$L~M?JAd-Im;dt5HS^ED z@#Js(;jjJAfB)}(4@}8I_8d|(!aiU1MsZ; z?-K5eZmL)}6SKOx5-Tq{XsSl~aZYRUT<|3fz3se5yB%r^BfD)*{oB_c*c z@vVr_f<6@S8c3K|QH+ODL%~2y5oEXtXtv{LKwU=O)X(o90{mgoP+&v>=!|Pqdo95f z+Ur{d@IY#hfuhXNw}iAvdokKF`U}m2;__EMR$JD*QMFdWziq-aGQx%LEXj4VV zKoD_T1_Gm5?%Cv?uX2B22|!?F`|4-4eWKt%rZ8vymMJFW6;!)c!C6eeVMNZ40!L@% z0!z%GeV(ILC+cs{^cVFo-qN8ez?T|2p-I|GwbdM^Ck6Sew!j5(t+Wt02l!0d)sqgy zO*VjK_8I;ItM+W#5-N`s98(E?ithT$ex5DMMf>;=NEZ>sMUEf>)tMqlpasHoFt46) zd5GEaL)SiDNH}NQL;)TPnuKak<8vF!E|*+{NRk(l}b@y+WP?LRKi9^LVD+3;HvkswUqqbY$G z%^ZdRIQ9iI*7BWQj+HX_jK~LHt5M8r*C#9hIRtY|xC1gNDrs0Y4Tp2NFD2mHD59a3 z0dYvcf~*~(%-(6kPoqx3$k4~)EC>ZfLFgqSMA3ka3E$cA9e8!bAF%&LHeohtc^t?l zIEinK_$Ek*vjMF9Gvbl#Gy0#500K42V@kFnnzAE&fkw)JdoNQt0R2ZKO&kKWchDcJN{Qc52OswV%d;yXm@reMe>i4 z*d;n5DA$HhhQ|R%GH|27AI6Y@BqVBpRtg-m8-cT0DA%&TA^aqPo~_@}HKKK%LaME> zpCdiZFscwC3imWbEg=n2zt2F7xCagSt_>@tnN9iO-whvU`8RLgOd}`2TM9p!-3vu~ zeHWSoL0OY^|3R<^s)rPlmO=VXpe42-nl};G76DD26ceI!#1qJhQsaRV`GN-=UOd1} zfUh$2Fa*Va;>U^!YT}h07x@LO$oP1SdpD8VUB~ac&mEr>9Qpz%&|SNJVrK|XP(%`< zim+O$m4yzjO{XveEvH4tV5&`%7*V^J=fvWJCBj~j{-ByfNES3lMDE|DtRzg%zXD<6 z@tC+Wo4O2z5}!w0wfbxOqpv}}hGqpi!79YY8JUAs^Y(|iwL~4=qIH~;Y2y$QM3>?7 z=glwTWCBi7Owi#(u?b2kHbI6gplx6fG2jT-pl88+aK2>2EVA?u5q@A4LF`8_9>_?+ zcmQQX<{}0NMFa%m^nF?c^4%h!$-AqT680H3JxAj#DoJPymIIa{JtzPMg9K9d9IXNR zd$;_0__zTeMgse~#q3B_WOjg}>7yiOg+Q!sH9V4lX+0qE@oYciU@IguOK}qd{;LZwZ5pt@S(vGH>bseTZc&y`HGQ%vmlq#g% ztY)3Or$7c&0Hc=Cjs3B9nmRMV;i2PfIF7lo+L~aQm4b7lx@duDDW{lGff@cA&JJJB zGkbP+i}o(e*g^0YuA=4ISRDJXnDHGX+$R1NN!SZiSM9`TUuotqSaNE&jpZ9NF<-eU zWIdTUZGkgIat-9@ZygEQcpJjIiZ2rnvY3WyQ9h!>upKR3*f|6lNexz%a@ECo0i=!J zrg+?RAthW!WuDgW;$>N0z`tXZH~*2Di=ZdPz>$(x+B zkW;GTg^Y8?%@ne38eEJrP++`VOrs6lWU85t0KuYI32_0116>q zOKInYutd}yH-$?23+gJ!2PGzjB3`1G;!oV9&cMOzd@)T7W#WT^xt z%M?JWAe@2hrUuOP%ZLydikZSWh|NM(5F52dX`2;W2P-2s0cpV(ED$Cxrr?0cossyn zG*l}YtCpKU5`?h0-=G;}P;cd6SK?fZy9AfSPo6I(xrRgWJnRdO(?vPUc_3;v^HsREZVM9 zQ-?v;7Qrd)5vfsBJPd(y?{gTb{SG5Fbr`7#@VvvYK_+IHbr?`KptR3nq-Pz5tW5l0S7p88_+Q^cVQ=rTo%}a+jAq17 zL=kdvLGqZz0@m7u{2jf@A-}Scb4i76`<|pw6b62nAV`JAjAM0Dg$!Pq1WG|!Kq7-7 zl$x_vwePE2h?^i%4 zT21jy(<)ETzHVAg@hMZ3r)Pg*zWB5$%G0x-nJ<3P6y*u|%om?EMR`Jy^Tp>(QJ$Xt zjrrnNO;MhZuPMI3C*(WdDz%{ZMcw-C`OZ>{)|Yha_e`tlEVXF;j&6O$w3^ORi`JKQ z>#L^Kbe3AQeqXo#g=saNr53HP>DJdxtLZGYX#Ih1eZ#bx&QgokAL`aU+RU`G)S~#N zZoO_=O=qb^>rz7Qc=`6KqeO9-A#k88vQj6B- zbn7=vtLZGYv~>UQkSkePlp|t^FPV<|svbqxu#Q~qlWVB3ROy+)*{cAs^Fm4w=zf%}PoZ38h-+##S6HWgC^Hmx$vz zS$lBH3)o^aSGLfgv^0#A-X?;u9qdt2l>rkynNqP!w2SJ*nLAFDkOh)gj^P#31H&r} za%Q(4kizWe6<48HXXUc0u6_#Np@=8|dcwaA~R&-LE zmuN1;4u*iWQXzLmC*u2FvT>oNI48UCd4?|jW>Y6lPE*Ro9H%mY9>R* z#F9oqgC(Ix_8>w4ha*?Qk+S+>4~-J&);Nh8Ak(Gr=ZG?n40{_>;xGqhStk|RGLc)^ z;=nMgu1Q0NTE)@xKnF(OL?%bBF$TtUN(2UsnI&?TGbU}6WsRYoe2ix``og@JYb_*a z*d76MqrEZk;G@_Z6L$R#1xn8rE!bdlSRm*H8@PdWHHpN64GqgP4a-pjRh52pge+7al*tzazX#D3Cp3 z*P_m1QliWq@AX0gu;`k!xNxE*Mzu5Y$sZmt!JFgKLu_g;uP1P0A7NH^qWa*-W3Cfw z^1ng6jb8!X1f?OeJKUJ8SpgUO?s|GqhmCLOZ+4@b_ zLE9R3O7+Z8NNPP5BbNgk^8b|hsNA z1TZdu#8<;_<2^bP!3=v>7z8b?JeL2;lQ;IHKh86-1T5l|_=JmJcjdpT(Lq-zm>d<$ zK>x+J8u5gM^J#OHr3AA*2|#lweb#E=CJC7D+4rIS zvmEvOS@-jJzi;^bQw_&OqwTwpO&(7i(?3mA^1scF2%)xL8tS=ncVjxpCQF00z}nwr z7jAxQWzB8l0MtH1HkzXtIaW;g3Ash!;DqrwbO58#MNYdxzkkp0VM&nk|5!uw{{ye0 z_^bXKeCA)!X!zGj5Nf~4XH@&Ow3U(>IjfnM=T-{){|vPV!j3D4Ll!&_p6n=5=< z_n+9yU>WQRrx;AIHHPF<&naWkOVU}oXbB3)AK<)EGT8)j4q6QM#&?#I*t!I5*nfl7 zyV#Xa!g*#*Qh^Fu32)8N5zv5VEf!~_w57<5a9`+lW+d1Wdq6{Qh8aW7xHfbvM1Zhl zCnAbJVOm#z{s@h9qWr-mVBl)lA`oXCujX+OM_q#cS4 z0Tj@D2WIq6`iffJA`1sP2-%urbj@~?CX($sF;uxBbD$}|0ei2+8E)=;~!{~nbf1-WFcWoP2PowL8H`I)iq=G7aLG2+LD!o3_ z_Y;!4A@38_ISV*@`Gy>>pW;!r1SK<#?W2lU@TM3+meP1iL(+HX*iT=hyA#z|YhjGI zq)0taBOW9h4z834I2|{VScxH&16%<%93eu7j>RzpA|9kc2Ma!?H;fT!#;{-U4I zH;Q-;2z^=1|7*<}a_zJu%RN1-0fn{xlklfHK*Llx;SCk}`#-a089EzuZpY8X1Nn)S zmw)V5exmANejY=AK#KTzY=)n=GINp4G}eGR2V|yBx+oXXSJ}CqDy`H5$r-(cU5l2$`63bKFVcr=!gAM=t5h4&SBF?+ zjGu&!%*YnqarZ*_E!RA77j;g=nOPC*j6%48tMxBmqiaD7j>9b zlbic(d(LQ+8d1GcdWLxi7vxADbN%APQa{fjst<{9yEHq&kC+${iBM=D*HPcdFbe zs2iEfIj#$-w*M5{aGh>$oP(UJ#m?{@yJrvsIfjWG9;oB~KU~nYbGW!CR*PE1`w76n zdUFrWX|+13c_R>ziTbc`tZDq~pS_`zLv=PGH;Smt&#^Mf`i?6a0&ZsiOX3I`L1Cn5 zCi(mOy5*yDGdgBQhd47)!i=~a#A&#*fYJ%Q&^m^P1E)Zj14kUq`^X$LW0hnDEh>%- zv>ue>$YA@{2Q1u(B7E3Z92xBVa~jDb5JsJu5U6;ORk?2^t%aFvdWtvH=RRQO0#jrk zyx0UvPHEgvYTR92iHnMCTcpnGe_Rygi%BpDFF4TRu_E4Zw*=Ta6P^b1Ryi(v-Nd~& zT-r$JWrT770Bs35`j^BMt5TO%C@bSr_DR4y@&qx*4@^p5TA}E985qWTH-IqW4l zZj$K{L%Kv7O5H-VT~V+j@E3(+y5$sJ7x{|qSXEfSPtGutfeQ79981}9$OjHCvK_~b zi^Jj~s0M%Wz|fp;6+j%)5K%ifM@2^xt^r2?_d_Ej++N9PNO%}XI7$;pRMG|B!_NZN zGGRfAIu0Mqd8i(cE^tpZ1)pBz4G5qmV#f7#;g&0`#fEMSit;;vk36IWSxkUIH8|kJ zD>2u~eK%#L^78T|M__!kH(Eh#f7B4BOlot2 z7`EP``@VTtlk!VcOtqq1$OpQL8iYibK$n4zV1>Q;DOPe*FSSm}Ec*;eCUr1Bl3Q{Y z;Av)T9yCB#h^)IPWT1VPjEF|K6PcB#tq6*@qq>lzEJ5o!{~Sg{P1K9lp@gkqj<|s! zBA06)2>-hf0MO zB@S>*rEMzV$9#Ji2?`7v#9c*P3Q6G_nJ~%5-0%~^I863> zdIt^?jI-TD!6Hi-Dq*@~KfMboQ@bPoi6@`RU^5hv^L7o1+cl^I8|)$eGfPbR0PRvh z2@i2#_23X#m)Z4iKJjLnm#*bG#Li7(18JBGpN{vi^?bZ9{XlQKkb`& zIa2?D4n$vYP?8$3UJl0KR3Zfe_!(`q{26V;X6;lPnabJJM_V&ed^1P|vH~>*DCq@h znDxz>+<8T#k?2I~F_OaG=g*8glfVPYpvT}nm1QOm%Do4p;Xz$!zWC66%|Eam8#@B= z#xItz&Pn+EJ>}oSEpVWr&VJ!Ps!tJ%LXvihdeGqbS*+R-z_F2usio6iG0DD<|Gh6G zn%=yLW4+hZCam0=_%ASb9^S9i2NaCPe;kKRu)){V#=7pfzYC0z$)7rV^N;@Y{zqtU z1&pJ;3Qozhf36+@O35$li**_sfGsA5sKf=R)D(f#?dQ?ZQvL#sZw?5Y^*q)(c9Up^ z8BCM%OZcNYC_Fc25Z)t}`1iPp_53#_H(+)gt*G5Z?vL!qgCb$)h9*`ogbUS#AB9^0 zT)7j>Kd54(YcacoU{fp13&}bt3yvu1zK}u!ZDdba%VA3J91a z#w^g`Iz_p$IfF3=_Xe(MIzPD&2S|*@yhKH70pUWaWhga)X`WJ*%Y!eX%ufCvkj1&& z=aUvUu5PH@n+Z!PW#cD_pGEv6@RPz%8b2BQ+&{XEKThw5+vEHD8<*tIqE||*l~wQl z@hFI^R!8IRcpQ!eg$-~Ws! z$BZX^p+W|Mgf~&2M|}XM5T{w5u+0C+6Y^+*1^gG4b9g8J2=@?v4&#Tg*q6^E_&JK7 zJMa^9x4XgGt4N4Z`NC0$t!ymujPa<4w9b_exZquOd)zx2-Gw*)*t z7qAa{70js(f_@3C>-U2}+dk6ikdls!2qs&jvGcbMn6OsrbT-yD*E<1hZR=8J90t_j zPCpz6qcwbS;DRAx3>uofN~7Lub!&r0Ehv|Rc4GR~gXA5-tsgx|#os(gFLv-P1bh%d zlq_?|l=}K>2id4>zJ9Q9=WOpzI2vt@Iv2Z}{f%&xs|y*kA4C9yD>Azo4pb}kpfPAx zy+Ih3YR#b9UThpRtcGtgbc8#dt<50Bz*4E(9P|f+a=%^=@zP#uV_?3CiTBPTAPHeNBO&hGDfuOm2R&a1fJLH zdO_D~ANfc;VtX>GRr;f?ZCFERdjsDQAu59C>gsqhf+nr5{-QjI9-v|NpkJ-kOSK>j zyN!DL=+-Q>M&lM}aGI;D+uhBzU|qoV!V8>?6_IwVuAbW(UFuHsOds!F-Pr2(;|crq zX06_>2VU4}l)LRas1rhPU4WD;A15p`%Il6hKpdEsdeuR%RBAOUgHp9oskN6o7+7zL zP8>|)EqVeDuC5A81)GxQYjCm43Hn`VTQw}hpVs>DtnGJwm=ny2x!lrX)_VA1E#>Bg z(bl6-U21Nx+VA%Ijd~5}!s9jDKk>_dLoAK(H4`e$-k?=&)VnbMYQ0%*|77J>9Ls~X z&9(8x4$-2B2Llg|snKuM%B5;IY#;kMjA=&EJbo!Re#Qj&Sbxiv%bVIUUA(-x4p@U; zSZ(y`P`jWDXI^W+`{B0@tJ+^1<8v%wCosTTfmdnOLj2 zjqd2e6k^zHfiu-!(5tkX-Coc>?!R4#wZSN)04sw=IcQYEpcIr*QE#817Q{@7Uk>1w zr_&<<@w`^QG4RU0X0KbSLQHqj@x)`}mji$_k;rs6)-G%UOshKR*UAX+tro&bSZ}{a z{1+^+I=(t?!3(TUMlkYy!Tfv%#FNA9Kw(6rEc*V!lKpHS*Jwj(VTSG!%lDO3MujGtrq0n8kD?R zty*cn@2MXvF4YSmBZKGFTCHji78QCuWLWKc9s@^O_5aTd5hwaHu;q=e1YE``>O!wy zgMKw@y%G{d+-TxP1F<~(;ep%~c=c4utVLd3pj|Yt^^5|AIY_BX#d#+~S z7A|zQwl3X&)HuEIqzi{9a7s+)y|QRr6yK2eTpc6pxjLSNmu6GPX$PMUr;GHQhOIpp z6dJ1gb zo7@WVt%2oY6~Uz5uQ$s5M!Oy-oYUU=%_N-tpxLkVTjg4SrCuqlwY^>hpQc~kynLyO z6;@p<7Tz{yGV?hFp*au0Wj-%gE9ACu9$ZZZ)kdk_EComfy6wjMX6L9&q6Q{aoAsd9 zt8}s49Mqaf#ha8#oG!O?-idGMl`GrbzN`r^jJg{bSqED^#2ODP;$ErhwOd|161i19 z*t)!lr6lrnc|(%Y9dWtkHN$SX(duED+z+bdR{Mc-@i-87+Zii5x7;3ugEhv8pf_l= z`pB6Y^+9FO3fdp29SmnK--ED%a?lFvSiFbbpx*xJSX#5ZZvd0dWv_;LASZ0El)H79 z9aa|g_6PZOykPt4gL9v@OCPn=;SN%3dLo zqq}+Ga`!@LC4K?RN41EiAjE!8f2DWTNzyulyUMH0%4*3yHSofE8L3m?V*8=CQZB8O z>!+7s;48rv*5RAs<|K^`q;hSwlxMB^pxz3hq|ne@EvfT|Vbcg|ok7^0An6zk!-UxGl8+18c6!;dN=jK}Wo1t4U4pTt7T#rNvA^VVKPD${7T?tUQy4AVxj(*x5qX%8ZDJt0`Kuco@09Z zL%0XDFr^N=Zc>m>F=7wyExwU?)JioI3Lwn)A2Gn5Q*vclGxTB4ZaP(F0d3F)g~P|x zA-+$imNrYb*(YqvnevFJJK!6qRY#q4(mI1}#MC_`%bxDTuyoK?OeTkkl7Os|DJyr_ rn)zU*1w7s>VB+l>LOD9Wp+n9HkhB2FSh+(WG5V&Zh0Vrys-*u9$+ffF literal 0 HcmV?d00001 From fc026fab50f727adc7e170a7cc1b56622fccb869 Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Sat, 2 Nov 2024 08:45:03 -0500 Subject: [PATCH 22/35] moved asyncify functions to intrinsics; added an option to still use ESM imports when using `--instantion` --- .../js-component-bindgen-component/src/lib.rs | 2 + .../wit/js-component-bindgen.wit | 7 + .../src/function_bindgen.rs | 4 +- crates/js-component-bindgen/src/intrinsics.rs | 154 +++++++++++++++++ .../src/transpile_bindgen.rs | 156 +++++------------- crates/js-component-bindgen/src/ts_bindgen.rs | 16 +- docs/src/transpiling.md | 1 + src/cmd/transpile.js | 4 + src/jco.js | 2 + xtask/src/build/jco.rs | 1 + xtask/src/generate/wasi_types.rs | 1 + 11 files changed, 225 insertions(+), 123 deletions(-) diff --git a/crates/js-component-bindgen-component/src/lib.rs b/crates/js-component-bindgen-component/src/lib.rs index f6f6f6da9..b2ab87624 100644 --- a/crates/js-component-bindgen-component/src/lib.rs +++ b/crates/js-component-bindgen-component/src/lib.rs @@ -78,6 +78,7 @@ impl Guest for JsComponentBindgenComponent { name: options.name, no_typescript: options.no_typescript.unwrap_or(false), instantiation: options.instantiation.map(Into::into), + esm_imports: options.esm_imports.unwrap_or(false), map: options.map.map(|map| map.into_iter().collect()), no_nodejs_compat: options.no_nodejs_compat.unwrap_or(false), base64_cutoff: options.base64_cutoff.unwrap_or(5000) as usize, @@ -167,6 +168,7 @@ impl Guest for JsComponentBindgenComponent { no_typescript: false, no_nodejs_compat: false, instantiation: opts.instantiation.map(Into::into), + esm_imports: opts.esm_imports.unwrap_or(false), map: opts.map.map(|map| map.into_iter().collect()), tla_compat: opts.tla_compat.unwrap_or(false), valid_lifting_optimization: false, diff --git a/crates/js-component-bindgen-component/wit/js-component-bindgen.wit b/crates/js-component-bindgen-component/wit/js-component-bindgen.wit index 44879628c..8ef9ed0f0 100644 --- a/crates/js-component-bindgen-component/wit/js-component-bindgen.wit +++ b/crates/js-component-bindgen-component/wit/js-component-bindgen.wit @@ -28,6 +28,10 @@ world js-component-bindgen { /// of the direct importable native ESM output. instantiation: option, + /// If `instantiation` is set, instead of providing an import object, use + /// ESM imports. + esm-imports: option, + /// Import bindings generation mode import-bindings: option, @@ -106,6 +110,9 @@ world js-component-bindgen { %world: option, tla-compat: option, instantiation: option, + /// If `instantiation` is set, instead of providing an import object, use + /// ESM imports. + esm-imports: option, map: option, /// Features that should be enabled as part of feature gating features: option, diff --git a/crates/js-component-bindgen/src/function_bindgen.rs b/crates/js-component-bindgen/src/function_bindgen.rs index f7c42e42f..13b743fbc 100644 --- a/crates/js-component-bindgen/src/function_bindgen.rs +++ b/crates/js-component-bindgen/src/function_bindgen.rs @@ -1052,9 +1052,11 @@ impl Bindgen for FunctionBindgen<'_> { self.bind_results(sig_results_length, results); if self.is_async { if self.use_asyncify { + let asyncify_wrap_export = self.intrinsic(Intrinsic::AsyncifyWrapExport); uwriteln!( self.src, - "await asyncifyWrapExport({})({});", + "await {}({})({});", + asyncify_wrap_export, self.callee, operands.join(", ") ); diff --git a/crates/js-component-bindgen/src/intrinsics.rs b/crates/js-component-bindgen/src/intrinsics.rs index 413c6da9d..4dde0e36c 100644 --- a/crates/js-component-bindgen/src/intrinsics.rs +++ b/crates/js-component-bindgen/src/intrinsics.rs @@ -5,6 +5,10 @@ use std::fmt::Write; #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] pub enum Intrinsic { + AsyncifyAsyncInstantiate, + AsyncifySyncInstantiate, + AsyncifyWrapExport, + AsyncifyWrapImport, Base64Compile, ClampGuest, ComponentError, @@ -23,6 +27,7 @@ pub enum Intrinsic { HasOwnProperty, I32ToF32, I64ToF64, + Imports, InstantiateCore, IsLE, ResourceTableFlag, @@ -114,6 +119,139 @@ pub fn render_intrinsics( for i in intrinsics.iter() { match i { + Intrinsic::AsyncifyAsyncInstantiate => output.push_str(" + const asyncifyModules = []; + let asyncifyPromise; + let asyncifyResolved; + + async function asyncifyInstantiate(module, imports) { + const instance = await instantiateCore(module, imports); + const memory = instance.exports.memory || (imports && imports.env && imports.env.memory); + const realloc = instance.exports.cabi_realloc || instance.exports.cabi_export_realloc; + + if (instance.exports.asyncify_get_state && memory) { + let address; + if (realloc) { + address = realloc(0, 0, 4, 1024); + new Int32Array(memory.buffer, address).set([address + 8, address + 1024]); + } else { + address = 16; + new Int32Array(memory.buffer, address).set([address + 8, address + 1024]); + } + + asyncifyModules.push({ instance, memory, address }); + } + + return instance; + } + + function asyncifyState() { + return asyncifyModules[0]?.instance.exports.asyncify_get_state(); + } + + function asyncifyAssertNoneState() { + let state = asyncifyState(); + if (state !== 0) { + throw new Error(`reentrancy not supported, expected asyncify state '0' but found '${state}'`); + } + } + "), + + Intrinsic::AsyncifySyncInstantiate => output.push_str(" + const asyncifyModules = []; + let asyncifyPromise; + let asyncifyResolved; + + function asyncifyInstantiate(module, imports) { + const instance = instantiateCore(module, imports); + const memory = instance.exports.memory || (imports && imports.env && imports.env.memory); + const realloc = instance.exports.cabi_realloc || instance.exports.cabi_export_realloc; + + if (instance.exports.asyncify_get_state && memory) { + let address; + if (realloc) { + address = realloc(0, 0, 4, 1024); + new Int32Array(memory.buffer, address).set([address + 8, address + 1024]); + } else { + address = 16; + new Int32Array(memory.buffer, address).set([address + 8, address + 1024]); + } + + asyncifyModules.push({ instance, memory, address }); + } + + return instance; + } + + function asyncifyState() { + return asyncifyModules[0]?.instance.exports.asyncify_get_state(); + } + + function asyncifyAssertNoneState() { + let state = asyncifyState(); + if (state !== 0) { + throw new Error(`reentrancy not supported, expected asyncify state '0' but found '${state}'`); + } + } + "), + + Intrinsic::AsyncifyWrapExport => output.push_str(" + function asyncifyWrapExport(fn) { + return async (...args) => { + if (asyncifyModules.length === 0) { + throw new Error(`none of the Wasm modules were processed with wasm-opt asyncify`); + } + asyncifyAssertNoneState(); + + let result = fn(...args); + + while (asyncifyState() === 1) { + asyncifyModules.forEach(({ instance }) => { + instance.exports.asyncify_stop_unwind(); + }); + + asyncifyResolved = await asyncifyPromise; + asyncifyPromise = undefined; + asyncifyAssertNoneState(); + + asyncifyModules.forEach(({ instance, address }) => { + instance.exports.asyncify_start_rewind(address); + }); + + result = fn(...args); + } + + asyncifyAssertNoneState(); + + return result; + }; + } + "), + + Intrinsic::AsyncifyWrapImport => output.push_str(" + function asyncifyWrapImport(fn) { + return (...args) => { + if (asyncifyState() === 2) { + asyncifyModules.forEach(({ instance }) => { + instance.exports.asyncify_stop_rewind(); + }); + + const ret = asyncifyResolved; + asyncifyResolved = undefined; + return ret; + } + asyncifyAssertNoneState(); + let value = fn(...args); + + asyncifyModules.forEach(({ instance, address }) => { + instance.exports.asyncify_start_unwind(address); + }); + + asyncifyPromise = value; + }; + } + "), + Intrinsic::Base64Compile => if !no_nodejs_compat { output.push_str(" const base64Compile = str => WebAssembly.compile(typeof Buffer !== 'undefined' ? Buffer.from(str, 'base64') : Uint8Array.from(atob(str), b => b.charCodeAt(0))); @@ -285,6 +423,8 @@ pub fn render_intrinsics( const i64ToF64 = i => (i64ToF64I[0] = i, i64ToF64F[0]); "), + Intrinsic::Imports => {}, + Intrinsic::InstantiateCore => if !instantiation { output.push_str(" const instantiateCore = WebAssembly.instantiate; @@ -654,6 +794,14 @@ impl Intrinsic { pub fn get_global_names() -> &'static [&'static str] { &[ // Intrinsic list exactly as below + "asyncifyAssertNoneState", + "asyncifyInstantiate", + "asyncifyModules", + "asyncifyPromise", + "asyncifyResolved", + "asyncifyState", + "asyncifyWrapExport", + "asyncifyWrapImport", "base64Compile", "clampGuest", "ComponentError", @@ -671,6 +819,7 @@ impl Intrinsic { "hasOwnProperty", "i32ToF32", "i64ToF64", + "imports", "instantiateCore", "isLE", "resourceCallBorrows", @@ -733,6 +882,10 @@ impl Intrinsic { pub fn name(&self) -> &'static str { match self { + Intrinsic::AsyncifyAsyncInstantiate => "asyncifyInstantiate", + Intrinsic::AsyncifySyncInstantiate => "asyncifyInstantiate", + Intrinsic::AsyncifyWrapExport => "asyncifyWrapExport", + Intrinsic::AsyncifyWrapImport => "asyncifyWrapImport", Intrinsic::Base64Compile => "base64Compile", Intrinsic::ClampGuest => "clampGuest", Intrinsic::ComponentError => "ComponentError", @@ -751,6 +904,7 @@ impl Intrinsic { Intrinsic::HasOwnProperty => "hasOwnProperty", Intrinsic::I32ToF32 => "i32ToF32", Intrinsic::I64ToF64 => "i64ToF64", + Intrinsic::Imports => "imports", Intrinsic::InstantiateCore => "instantiateCore", Intrinsic::IsLE => "isLE", Intrinsic::ResourceCallBorrows => "resourceCallBorrows", diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index 3aea69e9f..dd4101212 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -43,6 +43,9 @@ pub struct TranspileOpts { /// Provide a custom JS instantiation API for the component instead /// of the direct importable native ESM output. pub instantiation: Option, + /// If `instantiation` is set, instead of providing an import object, use + /// ESM imports. + pub esm_imports: bool, /// Configure how import bindings are provided, as high-level JS bindings, /// or as hybrid optimized bindings. pub import_bindings: Option, @@ -302,14 +305,24 @@ impl<'a> JsBindgen<'a> { ); if let Some(instantiation) = &self.opts.instantiation { + if self.opts.esm_imports { + self.esm_bindgen + .render_imports(&mut output, None, &mut self.local_names); + } + uwrite!( output, "\ - export function instantiate(getCoreModule, imports, instantiateCore = {}) {{ + export function instantiate(getCoreModule{}, instantiateCore = {}) {{ {} {} {} ", + if self.opts.esm_imports { + "" + } else { + ", imports" + }, match instantiation { InstantiationMode::Async => "WebAssembly.instantiate", InstantiationMode::Sync => @@ -319,15 +332,19 @@ impl<'a> JsBindgen<'a> { &intrinsic_definitions as &str, &compilation_promises as &str, ); - } - let imports_object = if self.opts.instantiation.is_some() { - Some("imports") + if !self.opts.esm_imports { + let imports_obj = &self.intrinsic(Intrinsic::Imports); + self.esm_bindgen.render_imports( + &mut output, + Some(imports_obj), + &mut self.local_names, + ); + } } else { - None - }; - self.esm_bindgen - .render_imports(&mut output, imports_object, &mut self.local_names); + self.esm_bindgen + .render_imports(&mut output, None, &mut self.local_names); + } if self.opts.instantiation.is_some() { self.esm_bindgen.render_exports( @@ -583,103 +600,6 @@ impl<'a> Instantiator<'a, '_> { } } fn instantiate(&mut self) { - if self.use_asyncify { - let (maybe_async, maybe_async_await) = - if let Some(InstantiationMode::Sync) = self.gen.opts.instantiation { - ("", "") - } else { - ("async ", "await ") - }; - uwrite!(self.src.js, " - let asyncifyPromise; - let asyncifyResolved; - const asyncifyModules = []; - - {maybe_async}function asyncifyInstantiate(module, imports) {{ - const instance = {maybe_async_await}instantiateCore(module, imports); - const memory = instance.exports.memory || (imports && imports.env && imports.env.memory); - const realloc = instance.exports.cabi_realloc || instance.exports.cabi_export_realloc; - - if (instance.exports.asyncify_get_state && memory) {{ - let address; - if (realloc) {{ - address = realloc(0, 0, 4, 1024); - new Int32Array(memory.buffer, address).set([address + 8, address + 1024]); - }} else {{ - address = 16; - new Int32Array(memory.buffer, address).set([address + 8, address + 1024]); - }} - - asyncifyModules.push({{ instance, memory, address }}); - }} - - return instance; - }} - - function asyncifyState() {{ - return asyncifyModules[0]?.instance.exports.asyncify_get_state(); - }} - function asyncifyAssertNoneState() {{ - let state = asyncifyState(); - if (state !== 0) {{ - throw new Error(`reentrancy not supported, expected asyncify state '0' but found '${{state}}'`); - }} - }} - function asyncifyWrapImport(fn) {{ - return (...args) => {{ - if (asyncifyState() === 2) {{ - asyncifyModules.forEach(({{ instance }}) => {{ - instance.exports.asyncify_stop_rewind(); - }}); - - const ret = asyncifyResolved; - asyncifyResolved = undefined; - return ret; - }} - asyncifyAssertNoneState(); - let value = fn(...args); - - asyncifyModules.forEach(({{ instance, address }}) => {{ - instance.exports.asyncify_start_unwind(address); - }}); - - asyncifyPromise = value; - }}; - }} - - function asyncifyWrapExport(fn) {{ - return async (...args) => {{ - if (asyncifyModules.length === 0) {{ - throw new Error(`none of the Wasm modules were processed with wasm-opt asyncify`); - }} - asyncifyAssertNoneState(); - - let result = fn(...args); - - while (asyncifyState() === 1) {{ // unwinding - asyncifyModules.forEach(({{ instance }}) => {{ - instance.exports.asyncify_stop_unwind(); - }}); - - asyncifyResolved = await asyncifyPromise; - asyncifyPromise = undefined; - asyncifyAssertNoneState(); - - asyncifyModules.forEach(({{ instance, address }}) => {{ - instance.exports.asyncify_start_rewind(address); - }}); - - result = fn(...args); - }} - - asyncifyAssertNoneState(); - - return result; - }}; - }} - "); - } - for (i, trampoline) in self.translation.trampolines.iter() { let Trampoline::LowerImport { index, @@ -1142,12 +1062,7 @@ impl<'a> Instantiator<'a, '_> { let i = self.instances.push(idx); let iu32 = i.as_u32(); - let instantiate = if self.use_asyncify { - self.gen.all_intrinsics.insert(Intrinsic::InstantiateCore); - "asyncifyInstantiate" - } else { - &self.gen.intrinsic(Intrinsic::InstantiateCore) - }; + let instantiate = self.gen.intrinsic(Intrinsic::InstantiateCore); uwriteln!(self.src.js, "let exports{iu32};"); match self.gen.opts.instantiation { @@ -1155,7 +1070,12 @@ impl<'a> Instantiator<'a, '_> { uwriteln!( self.src.js_init, "({{ exports: exports{iu32} }} = yield {instantiate}(yield module{}{imports}));", - idx.as_u32() + idx.as_u32(), + instantiate = if self.use_asyncify { + self.gen.intrinsic(Intrinsic::AsyncifyAsyncInstantiate) + } else { + instantiate + }, ) } @@ -1163,7 +1083,12 @@ impl<'a> Instantiator<'a, '_> { uwriteln!( self.src.js_init, "({{ exports: exports{iu32} }} = {instantiate}(module{}{imports}));", - idx.as_u32() + idx.as_u32(), + instantiate = if self.use_asyncify { + self.gen.intrinsic(Intrinsic::AsyncifySyncInstantiate) + } else { + instantiate + }, ) } } @@ -1310,8 +1235,9 @@ impl<'a> Instantiator<'a, '_> { if self.use_asyncify { uwrite!( self.src.js, - "\nconst trampoline{} = asyncifyWrapImport(async function", - trampoline.as_u32() + "\nconst trampoline{} = {}(async function", + trampoline.as_u32(), + self.gen.intrinsic(Intrinsic::AsyncifyWrapImport), ); } else { uwrite!( diff --git a/crates/js-component-bindgen/src/ts_bindgen.rs b/crates/js-component-bindgen/src/ts_bindgen.rs index 6b22a4c26..b4799b66a 100644 --- a/crates/js-component-bindgen/src/ts_bindgen.rs +++ b/crates/js-component-bindgen/src/ts_bindgen.rs @@ -270,7 +270,7 @@ pub fn ts_bindgen( // With the current representation of a "world" this is an import object // per-imported-interface where the type of that field is defined by the // interface itbindgen. - if opts.instantiation.is_some() { + if opts.instantiation.is_some() && !opts.esm_imports { uwriteln!(bindgen.src, "export interface ImportObject {{"); bindgen.src.push_str(&bindgen.import_object); uwriteln!(bindgen.src, "}}"); @@ -296,6 +296,11 @@ pub fn ts_bindgen( // Generate the TypeScript definition of the `instantiate` function // which is the main workhorse of the generated bindings. + let maybe_imports_obj = if opts.esm_imports { + "" + } else { + "\nimports: ImportObject," + }; match opts.instantiation { Some(InstantiationMode::Async) => { uwriteln!( @@ -321,13 +326,11 @@ pub fn ts_bindgen( * on the web, for example. */ export function instantiate( - getCoreModule: (path: string) => WebAssembly.Module, - imports: ImportObject, + getCoreModule: (path: string) => WebAssembly.Module,{maybe_imports_obj} instantiateCore?: (module: WebAssembly.Module, imports: Record) => WebAssembly.Instance ): {camel}; export function instantiate( - getCoreModule: (path: string) => WebAssembly.Module | Promise, - imports: ImportObject, + getCoreModule: (path: string) => WebAssembly.Module | Promise,{maybe_imports_obj} instantiateCore?: (module: WebAssembly.Module, imports: Record) => WebAssembly.Instance | Promise ): {camel} | Promise<{camel}>; ", @@ -358,8 +361,7 @@ pub fn ts_bindgen( * `WebAssembly.Module` constructor on the web, for example. */ export function instantiate( - getCoreModule: (path: string) => WebAssembly.Module, - imports: ImportObject, + getCoreModule: (path: string) => WebAssembly.Module,{maybe_imports_obj} instantiateCore?: (module: WebAssembly.Module, imports: Record) => WebAssembly.Instance ): {camel}; ", diff --git a/docs/src/transpiling.md b/docs/src/transpiling.md index 9047b1d71..f35b15e2f 100644 --- a/docs/src/transpiling.md +++ b/docs/src/transpiling.md @@ -49,6 +49,7 @@ Options include: * `--map`: Provide custom mappings for world imports. Supports both wildcard mappings (`*` similarly as in the package.json "exports" field) as well as `#` mappings for targetting exported interfaces. For example, the WASI mappings are internally defined with mappings like `--map wasi:filesystem/*=@bytecodealliance/preview2-shim/filesystem#*` to map `import as * filesystem from 'wasi:filesystem/types'` to `import { types } from '@bytecodealliance/preview2-shim/filesystem`. * `--no-nodejs-compat`: Disables Node.js compat in the output to load core Wasm with FS methods. * `--instantiation [mode]`: Instead of a direct ES module, export an `instantiate` function which can take the imports as an argument instead of implicit imports. The `instantiate` function can be async (with `--instantiation` or `--instantiation async`), or sync (with `--instantiation sync`). +* `--esm-imports`: If `--instantiation` is set, use ESM imports instead of providing an import object. * `--valid-lifting-optimization`: Internal validations are removed assuming that core Wasm binaries are valid components, providing a minor output size saving. * `--tracing`: Emit tracing calls for all function entry and exits. * `--no-namespaced-exports`: Removes exports of the type `test as "test:flavorful/test"` which are not compatible with typescript diff --git a/src/cmd/transpile.js b/src/cmd/transpile.js index 6e6389983..ce4e0b294 100644 --- a/src/cmd/transpile.js +++ b/src/cmd/transpile.js @@ -41,6 +41,7 @@ export async function types (witPath, opts) { * name?: string, * worldName?: string, * instantiation?: 'async' | 'sync', + * esmImports?: bool, * tlaCompat?: bool, * asyncMode?: string, * asyncImports?: string[], @@ -88,6 +89,7 @@ export async function typesComponent (witPath, opts) { return Object.fromEntries(generateTypes(name, { wit: { tag: 'path', val: (isWindows ? '//?/' : '') + resolve(witPath) }, instantiation, + esmImports: opts.esmImports, tlaCompat: opts.tlaCompat ?? false, world: opts.worldName, features, @@ -165,6 +167,7 @@ async function wasm2Js (source) { * @param {{ * name: string, * instantiation?: 'async' | 'sync', + * esmImports?: bool, * importBindings?: 'js' | 'optimized' | 'hybrid' | 'direct-optimized', * map?: Record, * asyncMode?: string, @@ -234,6 +237,7 @@ export async function transpileComponent (component, opts = {}) { name: opts.name ?? 'component', map: Object.entries(opts.map ?? {}), instantiation, + esmImports: opts.esmImports, asyncMode, importBindings: opts.importBindings ? { tag: opts.importBindings } : null, validLiftingOptimization: opts.validLiftingOptimization ?? false, diff --git a/src/jco.js b/src/jco.js index 188d5df2d..453d05175 100755 --- a/src/jco.js +++ b/src/jco.js @@ -65,6 +65,7 @@ program.command('transpile') .option('--stub', 'generate a stub implementation from a WIT file directly') .option('--js', 'output JS instead of core WebAssembly') .addOption(new Option('-I, --instantiation [mode]', 'output for custom module instantiation').choices(['async', 'sync']).preset('async')) + .option('--esm-imports', 'if `--instantiation` is set, use ESM imports instead of providing an import object') .option('-q, --quiet', 'disable output summary') .option('--no-namespaced-exports', 'disable namespaced exports for typescript compatibility') .option('--multi-memory', 'optimized output for Wasm multi-memory') @@ -80,6 +81,7 @@ program.command('types') .requiredOption('-o, --out-dir ', 'output directory') .option('--tla-compat', 'generates types for the TLA compat output with an async $init promise export') .addOption(new Option('-I, --instantiation [mode]', 'type output for custom module instantiation').choices(['async', 'sync']).preset('async')) + .option('--esm-imports', 'if `--instantiation` is set, use ESM imports instead of providing an import object') .addOption(new Option('--async-mode [mode]', 'use async imports and exports').choices(['sync', 'jspi', 'asyncify']).preset('sync')) .option('--default-async-imports', 'default async component imports from WASI interfaces') .option('--default-async-exports', 'default async component exports from WASI interfaces') diff --git a/xtask/src/build/jco.rs b/xtask/src/build/jco.rs index 85024f2f7..3fbbefecf 100644 --- a/xtask/src/build/jco.rs +++ b/xtask/src/build/jco.rs @@ -75,6 +75,7 @@ fn transpile(component_path: &str, name: String, optimize: bool) -> Result<()> { name, no_typescript: false, instantiation: None, + esm_imports: false, map: Some(import_map), no_nodejs_compat: false, base64_cutoff: 5000_usize, diff --git a/xtask/src/generate/wasi_types.rs b/xtask/src/generate/wasi_types.rs index 0eb82429e..dec888f1c 100644 --- a/xtask/src/generate/wasi_types.rs +++ b/xtask/src/generate/wasi_types.rs @@ -30,6 +30,7 @@ pub(crate) fn run() -> Result<()> { no_typescript: false, no_nodejs_compat: false, instantiation: None, + esm_imports: false, map: None, tla_compat: false, valid_lifting_optimization: false, From 88fafd3bb946ec17036baa15c6ba7ae5e87104d7 Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Sat, 2 Nov 2024 16:28:23 -0500 Subject: [PATCH 23/35] calls the async wrapper func at initialization time --- .../src/function_bindgen.rs | 29 ++----- .../src/transpile_bindgen.rs | 78 +++++++++++++++---- 2 files changed, 68 insertions(+), 39 deletions(-) diff --git a/crates/js-component-bindgen/src/function_bindgen.rs b/crates/js-component-bindgen/src/function_bindgen.rs index 13b743fbc..e2f92976b 100644 --- a/crates/js-component-bindgen/src/function_bindgen.rs +++ b/crates/js-component-bindgen/src/function_bindgen.rs @@ -87,7 +87,6 @@ pub struct FunctionBindgen<'a> { pub callee_resource_dynamic: bool, pub resolve: &'a Resolve, pub is_async: bool, - pub use_asyncify: bool, } impl FunctionBindgen<'_> { @@ -1050,27 +1049,13 @@ impl Bindgen for FunctionBindgen<'_> { Instruction::CallWasm { sig, .. } => { let sig_results_length = sig.results.len(); self.bind_results(sig_results_length, results); - if self.is_async { - if self.use_asyncify { - let asyncify_wrap_export = self.intrinsic(Intrinsic::AsyncifyWrapExport); - uwriteln!( - self.src, - "await {}({})({});", - asyncify_wrap_export, - self.callee, - operands.join(", ") - ); - } else { - uwriteln!( - self.src, - "await WebAssembly.promising({})({});", - self.callee, - operands.join(", ") - ); - } - } else { - uwriteln!(self.src, "{}({});", self.callee, operands.join(", ")); - } + let maybe_async_await = if self.is_async { "await " } else { "" }; + uwriteln!( + self.src, + "{maybe_async_await}{}({});", + self.callee, + operands.join(", ") + ); if let Some(prefix) = self.tracing_prefix { let to_result_string = self.intrinsic(Intrinsic::ToResultString); diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index dd4101212..34c4bba0f 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -135,6 +135,12 @@ struct JsBindgen<'a> { /// List of all intrinsics emitted to `src` so far. all_intrinsics: BTreeSet, + + /// List of all core Wasm exported functions (and if is async) referenced in + /// `src` so far. + all_core_exported_funcs: Vec<(String, bool)>, + + use_asyncify: bool, } #[allow(clippy::too_many_arguments)] @@ -148,6 +154,20 @@ pub fn transpile_bindgen( opts: TranspileOpts, files: &mut Files, ) -> (Vec, Vec<(String, Export)>) { + let (use_asyncify, async_imports, async_exports) = match opts.async_mode.clone() { + None | Some(AsyncMode::Sync) => (false, Default::default(), Default::default()), + Some(AsyncMode::JavaScriptPromiseIntegration { imports, exports }) => ( + false, + imports.into_iter().collect(), + exports.into_iter().collect(), + ), + Some(AsyncMode::Asyncify { imports, exports }) => ( + true, + imports.into_iter().collect(), + exports.into_iter().collect(), + ), + }; + let mut bindgen = JsBindgen { local_names: LocalNames::default(), src: Source::default(), @@ -155,6 +175,8 @@ pub fn transpile_bindgen( core_module_cnt: 0, opts: &opts, all_intrinsics: BTreeSet::new(), + all_core_exported_funcs: Vec::new(), + use_asyncify, }; bindgen .local_names @@ -164,20 +186,6 @@ pub fn transpile_bindgen( // bindings is the actual `instantiate` method itself, created by this // structure. - let (use_asyncify, async_imports, async_exports) = match opts.async_mode.clone() { - None | Some(AsyncMode::Sync) => (false, Default::default(), Default::default()), - Some(AsyncMode::JavaScriptPromiseIntegration { imports, exports }) => ( - false, - imports.into_iter().collect(), - exports.into_iter().collect(), - ), - Some(AsyncMode::Asyncify { imports, exports }) => ( - true, - imports.into_iter().collect(), - exports.into_iter().collect(), - ), - }; - let mut instantiator = Instantiator { src: Source::default(), sizes: SizeAlign::default(), @@ -261,6 +269,24 @@ impl<'a> JsBindgen<'a> { ) { let mut output = source::Source::default(); let mut compilation_promises = source::Source::default(); + let mut core_exported_funcs = source::Source::default(); + + let async_wrap_fn = if self.use_asyncify { + &self.intrinsic(Intrinsic::AsyncifyWrapExport) + } else { + "WebAssembly.promising" + }; + for (core_export_fn, is_async) in self.all_core_exported_funcs.iter() { + let local_name = self.local_names.get(core_export_fn); + if *is_async { + uwriteln!( + core_exported_funcs, + "{local_name} = {async_wrap_fn}({core_export_fn});", + ); + } else { + uwriteln!(core_exported_funcs, "{local_name} = {core_export_fn};",); + } + } // Setup the compilation data and compilation promises let mut removed = BTreeSet::new(); @@ -347,6 +373,7 @@ impl<'a> JsBindgen<'a> { } if self.opts.instantiation.is_some() { + uwrite!(&mut self.src.js, "{}", &core_exported_funcs as &str); self.esm_bindgen.render_exports( &mut self.src.js, self.opts.instantiation.is_some(), @@ -415,6 +442,7 @@ impl<'a> JsBindgen<'a> { let gen = (function* init () {{ {}\ {}\ + {}\ }})(); let promise, resolve, reject; function runNext (value) {{ @@ -445,6 +473,7 @@ impl<'a> JsBindgen<'a> { &self.src.js as &str, &compilation_promises as &str, &self.src.js_init as &str, + &core_exported_funcs as &str, ); self.esm_bindgen.render_exports( @@ -1620,7 +1649,6 @@ impl<'a> Instantiator<'a, '_> { abi: AbiVariant, is_async: bool, ) { - let use_asyncify = self.use_asyncify; let memory = opts.memory.map(|idx| format!("memory{}", idx.as_u32())); let realloc = opts.realloc.map(|idx| format!("realloc{}", idx.as_u32())); let post_return = opts @@ -1715,7 +1743,6 @@ impl<'a> Instantiator<'a, '_> { src: source::Source::default(), resolve: self.resolve, is_async, - use_asyncify, }; abi::call( self.resolve, @@ -2010,8 +2037,26 @@ impl<'a> Instantiator<'a, '_> { )) }) .unwrap_or(false); + let maybe_async = if is_async { "async " } else { "" }; + let core_export_fn = self.core_def(def); + let callee = match self + .gen + .local_names + .get_or_create(&core_export_fn, &core_export_fn) + { + (local_name, true) => local_name.to_string(), + (local_name, false) => { + let local_name = local_name.to_string(); + uwriteln!(self.src.js, "let {local_name};"); + self.gen + .all_core_exported_funcs + .push((core_export_fn.clone(), is_async)); + local_name + } + }; + match func.kind { FunctionKind::Freestanding => { uwrite!(self.src.js, "\n{maybe_async}function {local_name}") @@ -2055,7 +2100,6 @@ impl<'a> Instantiator<'a, '_> { self.defined_resource_classes.insert(local_name.to_string()); } } - let callee = self.core_def(def); self.bindgen( func.params.len(), match func.kind { From cfe3251ad7c3ab5da825f1dfeff747b70aca5142 Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Sat, 2 Nov 2024 21:05:46 -0500 Subject: [PATCH 24/35] removed duplicate type def --- crates/js-component-bindgen/src/ts_bindgen.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/js-component-bindgen/src/ts_bindgen.rs b/crates/js-component-bindgen/src/ts_bindgen.rs index b4799b66a..69da965c9 100644 --- a/crates/js-component-bindgen/src/ts_bindgen.rs +++ b/crates/js-component-bindgen/src/ts_bindgen.rs @@ -325,10 +325,6 @@ pub fn ts_bindgen( * `WebAssembly.Module` object. This would use `compileStreaming` * on the web, for example. */ - export function instantiate( - getCoreModule: (path: string) => WebAssembly.Module,{maybe_imports_obj} - instantiateCore?: (module: WebAssembly.Module, imports: Record) => WebAssembly.Instance - ): {camel}; export function instantiate( getCoreModule: (path: string) => WebAssembly.Module | Promise,{maybe_imports_obj} instantiateCore?: (module: WebAssembly.Module, imports: Record) => WebAssembly.Instance | Promise From 62f989647d38c22210085bfb2dee5a3c559cb782 Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Sun, 3 Nov 2024 07:40:51 -0600 Subject: [PATCH 25/35] added more transpile options --- .../js-component-bindgen-component/src/lib.rs | 17 +++++ .../wit/js-component-bindgen.wit | 13 ++++ crates/js-component-bindgen/src/lib.rs | 4 +- .../src/transpile_bindgen.rs | 67 ++++++++++++++++--- crates/js-component-bindgen/src/ts_bindgen.rs | 4 +- src/cmd/transpile.js | 9 +++ src/jco.js | 2 + xtask/src/build/jco.rs | 2 + xtask/src/generate/wasi_types.rs | 2 + 9 files changed, 109 insertions(+), 11 deletions(-) diff --git a/crates/js-component-bindgen-component/src/lib.rs b/crates/js-component-bindgen-component/src/lib.rs index b2ab87624..e3b0d3677 100644 --- a/crates/js-component-bindgen-component/src/lib.rs +++ b/crates/js-component-bindgen-component/src/lib.rs @@ -42,6 +42,19 @@ impl From for js_component_bindgen::InstantiationMode { } } +impl From for js_component_bindgen::StaticWasmSourceImportsMode { + fn from(value: StaticWasmSourceImportsMode) -> Self { + match value { + StaticWasmSourceImportsMode::ProposedStandardImportSource => { + js_component_bindgen::StaticWasmSourceImportsMode::ProposedStandardImportSource + } + StaticWasmSourceImportsMode::NonStandardImport => { + js_component_bindgen::StaticWasmSourceImportsMode::NonStandardImport + } + } + } +} + impl From for js_component_bindgen::BindingsMode { fn from(value: BindingsMode) -> Self { match value { @@ -78,6 +91,8 @@ impl Guest for JsComponentBindgenComponent { name: options.name, no_typescript: options.no_typescript.unwrap_or(false), instantiation: options.instantiation.map(Into::into), + cache_wasm_compile: options.cache_wasm_compile.unwrap_or(false), + static_wasm_source_imports: options.static_wasm_source_imports.map(Into::into), esm_imports: options.esm_imports.unwrap_or(false), map: options.map.map(|map| map.into_iter().collect()), no_nodejs_compat: options.no_nodejs_compat.unwrap_or(false), @@ -168,6 +183,8 @@ impl Guest for JsComponentBindgenComponent { no_typescript: false, no_nodejs_compat: false, instantiation: opts.instantiation.map(Into::into), + cache_wasm_compile: false, + static_wasm_source_imports: None, esm_imports: opts.esm_imports.unwrap_or(false), map: opts.map.map(|map| map.into_iter().collect()), tla_compat: opts.tla_compat.unwrap_or(false), diff --git a/crates/js-component-bindgen-component/wit/js-component-bindgen.wit b/crates/js-component-bindgen-component/wit/js-component-bindgen.wit index 8ef9ed0f0..b23d39761 100644 --- a/crates/js-component-bindgen-component/wit/js-component-bindgen.wit +++ b/crates/js-component-bindgen-component/wit/js-component-bindgen.wit @@ -9,6 +9,11 @@ world js-component-bindgen { sync, } + variant static-wasm-source-imports-mode { + proposed-standard-import-source, + non-standard-import, + } + variant bindings-mode { js, hybrid, @@ -28,6 +33,14 @@ world js-component-bindgen { /// of the direct importable native ESM output. instantiation: option, + /// If `instantiation` is set, on the first `instantiate` call the compiled + /// Wasm modules are cached for subsequent `instantiate` calls. + cache-wasm-compile: option, + + /// Static import of Wasm module with the proposed standard source phase + /// imports or use the non-standard import syntax. + static-wasm-source-imports: option, + /// If `instantiation` is set, instead of providing an import object, use /// ESM imports. esm-imports: option, diff --git a/crates/js-component-bindgen/src/lib.rs b/crates/js-component-bindgen/src/lib.rs index a932ec391..fe05204a9 100644 --- a/crates/js-component-bindgen/src/lib.rs +++ b/crates/js-component-bindgen/src/lib.rs @@ -8,7 +8,9 @@ pub mod function_bindgen; pub mod intrinsics; pub mod names; pub mod source; -pub use transpile_bindgen::{AsyncMode, BindingsMode, InstantiationMode, TranspileOpts}; +pub use transpile_bindgen::{ + AsyncMode, BindingsMode, InstantiationMode, StaticWasmSourceImportsMode, TranspileOpts, +}; use anyhow::Result; use transpile_bindgen::transpile_bindgen; diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index 34c4bba0f..a64d67aea 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -43,6 +43,12 @@ pub struct TranspileOpts { /// Provide a custom JS instantiation API for the component instead /// of the direct importable native ESM output. pub instantiation: Option, + /// If `instantiation` is set, on the first `instantiate` call the compiled + /// Wasm modules are cached for subsequent `instantiate` calls. + pub cache_wasm_compile: bool, + /// Static import of Wasm module with the proposed standard source phase + /// imports or use the non-standard import syntax. + pub static_wasm_source_imports: Option, /// If `instantiation` is set, instead of providing an import object, use /// ESM imports. pub esm_imports: bool, @@ -90,6 +96,13 @@ pub enum AsyncMode { }, } +#[derive(Default, Clone, Debug)] +pub enum StaticWasmSourceImportsMode { + #[default] + ProposedStandardImportSource, + NonStandardImport, +} + #[derive(Default, Clone, Debug)] pub enum InstantiationMode { #[default] @@ -288,16 +301,46 @@ impl<'a> JsBindgen<'a> { } } + // adds a default implementation of `getCoreModule` + if self.opts.static_wasm_source_imports.is_none() + && matches!(self.opts.instantiation, Some(InstantiationMode::Async)) + { + uwriteln!( + compilation_promises, + "if (!getCoreModule) getCoreModule = (name) => {}(new URL(`./${{name}}`, import.meta.url));", + self.intrinsic(Intrinsic::FetchCompile) + ); + } + // Setup the compilation data and compilation promises let mut removed = BTreeSet::new(); + let mut module_cache_declarations = source::Source::default(); for i in 0..self.core_module_cnt { let local_name = format!("module{}", i); let mut name_idx = core_file_name(name, i as u32); - if self.opts.instantiation.is_some() { - uwriteln!( - compilation_promises, - "const {local_name} = getCoreModule('{name_idx}');" - ); + if let Some(mode) = &self.opts.static_wasm_source_imports { + match mode { + StaticWasmSourceImportsMode::ProposedStandardImportSource => { + uwriteln!(output, "import source {local_name} from './{name_idx}';",); + } + StaticWasmSourceImportsMode::NonStandardImport => { + uwriteln!(output, "import {local_name} from './{name_idx}';",); + } + } + } else if self.opts.instantiation.is_some() { + if self.opts.cache_wasm_compile { + let cache_name = self.local_names.create_once(&format!("cached{local_name}")); + uwriteln!(module_cache_declarations, "let {cache_name};"); + uwriteln!( + compilation_promises, + "const {local_name} = {cache_name} || ({cache_name} = getCoreModule('{name_idx}'));" + ); + } else { + uwriteln!( + compilation_promises, + "const {local_name} = getCoreModule('{name_idx}');" + ); + } } else if files.get_size(&name_idx).unwrap() < self.opts.base64_cutoff { assert!(removed.insert(i)); let data = files.remove(&name_idx).unwrap(); @@ -335,6 +378,9 @@ impl<'a> JsBindgen<'a> { self.esm_bindgen .render_imports(&mut output, None, &mut self.local_names); } + if self.opts.cache_wasm_compile { + uwrite!(output, "\n{}", &module_cache_declarations as &str); + } uwrite!( output, @@ -347,7 +393,7 @@ impl<'a> JsBindgen<'a> { if self.opts.esm_imports { "" } else { - ", imports" + ", imports = {}" }, match instantiation { InstantiationMode::Async => "WebAssembly.instantiate", @@ -645,7 +691,7 @@ impl<'a> Instantiator<'a, '_> { if let Some(InstantiationMode::Async) = self.gen.opts.instantiation { // To avoid uncaught promise rejection errors, we attach an intermediate // Promise.all with a rejection handler, if there are multiple promises. - if self.modules.len() > 1 { + if self.modules.len() > 1 && self.gen.opts.static_wasm_source_imports.is_none() { self.src.js_init.push_str("Promise.all(["); for i in 0..self.modules.len() { if i > 0 { @@ -1098,13 +1144,18 @@ impl<'a> Instantiator<'a, '_> { Some(InstantiationMode::Async) | None => { uwriteln!( self.src.js_init, - "({{ exports: exports{iu32} }} = yield {instantiate}(yield module{}{imports}));", + "({{ exports: exports{iu32} }} = yield {instantiate}({maybe_async_module}module{}{imports}));", idx.as_u32(), instantiate = if self.use_asyncify { self.gen.intrinsic(Intrinsic::AsyncifyAsyncInstantiate) } else { instantiate }, + maybe_async_module = if self.gen.opts.static_wasm_source_imports.is_some() { + "" + } else { + "yield " + }, ) } diff --git a/crates/js-component-bindgen/src/ts_bindgen.rs b/crates/js-component-bindgen/src/ts_bindgen.rs index 69da965c9..15f769f62 100644 --- a/crates/js-component-bindgen/src/ts_bindgen.rs +++ b/crates/js-component-bindgen/src/ts_bindgen.rs @@ -299,7 +299,7 @@ pub fn ts_bindgen( let maybe_imports_obj = if opts.esm_imports { "" } else { - "\nimports: ImportObject," + "\nimports?: ImportObject," }; match opts.instantiation { Some(InstantiationMode::Async) => { @@ -326,7 +326,7 @@ pub fn ts_bindgen( * on the web, for example. */ export function instantiate( - getCoreModule: (path: string) => WebAssembly.Module | Promise,{maybe_imports_obj} + getCoreModule?: (path: string) => WebAssembly.Module | Promise,{maybe_imports_obj} instantiateCore?: (module: WebAssembly.Module, imports: Record) => WebAssembly.Instance | Promise ): {camel} | Promise<{camel}>; ", diff --git a/src/cmd/transpile.js b/src/cmd/transpile.js index ce4e0b294..d4b6acac0 100644 --- a/src/cmd/transpile.js +++ b/src/cmd/transpile.js @@ -167,6 +167,8 @@ async function wasm2Js (source) { * @param {{ * name: string, * instantiation?: 'async' | 'sync', + * cacheWasmCompile?: bool, + * staticWasmSourceImports?: 'proposed-standard-import-source' | 'non-standard-import', * esmImports?: bool, * importBindings?: 'js' | 'optimized' | 'hybrid' | 'direct-optimized', * map?: Record, @@ -223,6 +225,11 @@ export async function transpileComponent (component, opts = {}) { instantiation = { tag: 'async' }; } + let staticWasmSourceImports = null; + if (opts.staticWasmSourceImports) { + staticWasmSourceImports = { tag: opts.staticWasmSourceImports }; + } + const asyncMode = !opts.asyncMode || opts.asyncMode === 'sync' ? null : { @@ -237,6 +244,8 @@ export async function transpileComponent (component, opts = {}) { name: opts.name ?? 'component', map: Object.entries(opts.map ?? {}), instantiation, + cacheWasmCompile: opts.cacheWasmCompile, + staticWasmSourceImports, esmImports: opts.esmImports, asyncMode, importBindings: opts.importBindings ? { tag: opts.importBindings } : null, diff --git a/src/jco.js b/src/jco.js index 453d05175..d3429c710 100755 --- a/src/jco.js +++ b/src/jco.js @@ -65,6 +65,8 @@ program.command('transpile') .option('--stub', 'generate a stub implementation from a WIT file directly') .option('--js', 'output JS instead of core WebAssembly') .addOption(new Option('-I, --instantiation [mode]', 'output for custom module instantiation').choices(['async', 'sync']).preset('async')) + .option('--cache-wasm-compile', 'first `instantiate` call caches the compiled Wasm modules for subsequent `instantiate` calls') + .addOption(new Option('--static-wasm-source-imports [mode]', 'static import of Wasm module').choices(['proposed-standard-import-source', 'non-standard-import'])) .option('--esm-imports', 'if `--instantiation` is set, use ESM imports instead of providing an import object') .option('-q, --quiet', 'disable output summary') .option('--no-namespaced-exports', 'disable namespaced exports for typescript compatibility') diff --git a/xtask/src/build/jco.rs b/xtask/src/build/jco.rs index 3fbbefecf..f05e81b23 100644 --- a/xtask/src/build/jco.rs +++ b/xtask/src/build/jco.rs @@ -75,6 +75,8 @@ fn transpile(component_path: &str, name: String, optimize: bool) -> Result<()> { name, no_typescript: false, instantiation: None, + cache_wasm_compile: false, + static_wasm_source_imports: None, esm_imports: false, map: Some(import_map), no_nodejs_compat: false, diff --git a/xtask/src/generate/wasi_types.rs b/xtask/src/generate/wasi_types.rs index dec888f1c..c9a078f0d 100644 --- a/xtask/src/generate/wasi_types.rs +++ b/xtask/src/generate/wasi_types.rs @@ -30,6 +30,8 @@ pub(crate) fn run() -> Result<()> { no_typescript: false, no_nodejs_compat: false, instantiation: None, + cache_wasm_compile: false, + static_wasm_source_imports: None, esm_imports: false, map: None, tla_compat: false, From 3473a00927221f56fd8e662e3e0ff189f4ca6454 Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Sun, 3 Nov 2024 08:28:51 -0600 Subject: [PATCH 26/35] updated test --- test/cli.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cli.js b/test/cli.js index d6ca48625..7860e38c5 100644 --- a/test/cli.js +++ b/test/cli.js @@ -145,7 +145,7 @@ export async function cliTest(_fixtures) { ); const m = await import(`${outDir}/${name}.js`); const inst = await m.instantiate( - (fileName) => readFile(`${outDir}/${fileName}`).then((file) => WebAssembly.compile(file)), + undefined, { 'something:test/test-interface': { callAsync: async () => "called async", From ca94f4d0212a8ac2544b15c550bdb40d63dcf319 Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Sun, 3 Nov 2024 09:17:22 -0600 Subject: [PATCH 27/35] added jspi test --- package.json | 2 +- test/cli.js | 41 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e20b1f070..fbe7702b7 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "build:types:preview2-shim": "cargo xtask generate wasi-types", "lint": "eslint -c eslintrc.cjs src/**/*.js packages/*/lib/**/*.js", "test:lts": "mocha -u tdd test/test.js --timeout 120000", - "test": "node --stack-trace-limit=100 node_modules/mocha/bin/mocha.js -u tdd test/test.js --timeout 120000", + "test": "node --experimental-wasm-jspi --stack-trace-limit=100 node_modules/mocha/bin/mocha.js -u tdd test/test.js --timeout 120000", "prepublishOnly": "cargo xtask build release && npm run test" }, "files": [ diff --git a/test/cli.js b/test/cli.js index 7860e38c5..240d2bc55 100644 --- a/test/cli.js +++ b/test/cli.js @@ -121,7 +121,46 @@ export async function cliTest(_fixtures) { ok(source.toString().includes("export { test")); }); - test("Transpile & Asyncify", async () => { + test("Transpile with Async Mode for JSPI", async () => { + const name = "async_call"; + const { stderr } = await exec( + jcoPath, + "transpile", + `test/fixtures/components/${name}.component.wasm`, + `--name=${name}`, + "--valid-lifting-optimization", + "--tla-compat", + "--instantiation=async", + "--base64-cutoff=0", + "--async-mode=jspi", + "--async-imports=something:test/test-interface#call-async", + "--async-exports=run-async", + "-o", + outDir + ); + strictEqual(stderr, ""); + await writeFile( + `${outDir}/package.json`, + JSON.stringify({ type: "module" }) + ); + const m = await import(`${outDir}/${name}.js`); + const inst = await m.instantiate( + undefined, + { + 'something:test/test-interface': { + callAsync: async () => "called async", + callSync: () => "called sync", + }, + }, + ); + strictEqual(inst.runSync instanceof AsyncFunction, false); + strictEqual(inst.runAsync instanceof AsyncFunction, true); + + strictEqual(inst.runSync(), "called sync"); + strictEqual(await inst.runAsync(), "called async"); + }); + + test("Transpile with Async Mode for Asyncify", async () => { const name = "async_call"; const { stderr } = await exec( jcoPath, From 1f18fcf6ca545b94f53e83bb97aaa2a247ed6198 Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Sun, 3 Nov 2024 09:35:14 -0600 Subject: [PATCH 28/35] feature detect jspi support in test suite --- test/cli.js | 74 +++++++++++++++++++++++++++-------------------------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/test/cli.js b/test/cli.js index 240d2bc55..b82f1ec05 100644 --- a/test/cli.js +++ b/test/cli.js @@ -121,44 +121,46 @@ export async function cliTest(_fixtures) { ok(source.toString().includes("export { test")); }); - test("Transpile with Async Mode for JSPI", async () => { - const name = "async_call"; - const { stderr } = await exec( - jcoPath, - "transpile", - `test/fixtures/components/${name}.component.wasm`, - `--name=${name}`, - "--valid-lifting-optimization", - "--tla-compat", - "--instantiation=async", - "--base64-cutoff=0", - "--async-mode=jspi", - "--async-imports=something:test/test-interface#call-async", - "--async-exports=run-async", - "-o", - outDir - ); - strictEqual(stderr, ""); - await writeFile( - `${outDir}/package.json`, - JSON.stringify({ type: "module" }) - ); - const m = await import(`${outDir}/${name}.js`); - const inst = await m.instantiate( - undefined, - { - 'something:test/test-interface': { - callAsync: async () => "called async", - callSync: () => "called sync", + if (typeof WebAssembly.Suspending === 'function') { + test("Transpile with Async Mode for JSPI", async () => { + const name = "async_call"; + const { stderr } = await exec( + jcoPath, + "transpile", + `test/fixtures/components/${name}.component.wasm`, + `--name=${name}`, + "--valid-lifting-optimization", + "--tla-compat", + "--instantiation=async", + "--base64-cutoff=0", + "--async-mode=jspi", + "--async-imports=something:test/test-interface#call-async", + "--async-exports=run-async", + "-o", + outDir + ); + strictEqual(stderr, ""); + await writeFile( + `${outDir}/package.json`, + JSON.stringify({ type: "module" }) + ); + const m = await import(`${outDir}/${name}.js`); + const inst = await m.instantiate( + undefined, + { + 'something:test/test-interface': { + callAsync: async () => "called async", + callSync: () => "called sync", + }, }, - }, - ); - strictEqual(inst.runSync instanceof AsyncFunction, false); - strictEqual(inst.runAsync instanceof AsyncFunction, true); + ); + strictEqual(inst.runSync instanceof AsyncFunction, false); + strictEqual(inst.runAsync instanceof AsyncFunction, true); - strictEqual(inst.runSync(), "called sync"); - strictEqual(await inst.runAsync(), "called async"); - }); + strictEqual(inst.runSync(), "called sync"); + strictEqual(await inst.runAsync(), "called async"); + }); + } test("Transpile with Async Mode for Asyncify", async () => { const name = "async_call"; From e209a8c0272570dbfa31fb86937898d7b3156a90 Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Sun, 3 Nov 2024 17:11:45 -0600 Subject: [PATCH 29/35] fix for test --- test/cli.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/cli.js b/test/cli.js index b82f1ec05..25fb97b1e 100644 --- a/test/cli.js +++ b/test/cli.js @@ -144,7 +144,7 @@ export async function cliTest(_fixtures) { `${outDir}/package.json`, JSON.stringify({ type: "module" }) ); - const m = await import(`${outDir}/${name}.js`); + const m = await import(`${pathToFileURL(outDir)}/${name}.js`); const inst = await m.instantiate( undefined, { @@ -184,7 +184,7 @@ export async function cliTest(_fixtures) { `${outDir}/package.json`, JSON.stringify({ type: "module" }) ); - const m = await import(`${outDir}/${name}.js`); + const m = await import(`${pathToFileURL(outDir)}/${name}.js`); const inst = await m.instantiate( undefined, { From f218fef183824c3e939a7bef3ad8e6ad96ca1661 Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Sun, 3 Nov 2024 18:30:41 -0600 Subject: [PATCH 30/35] starting to setup wasi shims for async --- .../preview2-shim/lib/browser-async/index.js | 17 +++++++++++++++++ packages/preview2-shim/package.json | 10 +++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 packages/preview2-shim/lib/browser-async/index.js diff --git a/packages/preview2-shim/lib/browser-async/index.js b/packages/preview2-shim/lib/browser-async/index.js new file mode 100644 index 000000000..da23919e7 --- /dev/null +++ b/packages/preview2-shim/lib/browser-async/index.js @@ -0,0 +1,17 @@ +import * as clocks from "./clocks.js"; +import * as filesystem from "./filesystem.js"; +import * as http from "./http.js"; +import * as io from "./io.js"; +import * as random from "./random.js"; +import * as sockets from "./sockets.js"; +import * as cli from "./cli.js"; + +export { + clocks, + filesystem, + http, + io, + random, + sockets, + cli, +} diff --git a/packages/preview2-shim/package.json b/packages/preview2-shim/package.json index 54ed9b969..fdcbf6992 100644 --- a/packages/preview2-shim/package.json +++ b/packages/preview2-shim/package.json @@ -15,10 +15,18 @@ "types": "./types/*.d.ts", "node": "./lib/nodejs/*.js", "default": "./lib/browser/*.js" + }, + "./async": { + "types": "./types-async/index.d.ts", + "default": "./lib/browser-async/index.js" + }, + "./async/*": { + "types": "./types/*.d.ts", + "default": "./lib/browser-async/*.js" } }, "scripts": { - "test": "node --expose-gc ../../node_modules/mocha/bin/mocha.js -u tdd test/test.js --timeout 30000" + "test": "node --experimental-wasm-jspi --expose-gc ../../node_modules/mocha/bin/mocha.js -u tdd test/test.js --timeout 30000" }, "files": [ "types", From 748077ec99b5ae8ec164ac2d95cbc8da8b404549 Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Mon, 4 Nov 2024 20:43:53 -0600 Subject: [PATCH 31/35] added async WASI P2 shims --- .../preview2-shim/lib/browser-async/cli.js | 135 ++++ .../preview2-shim/lib/browser-async/clocks.js | 5 + .../browser-async/clocks/monotonic-clock.js | 66 ++ .../lib/browser-async/clocks/timezone.js | 25 + .../lib/browser-async/clocks/wall-clock.js | 39 + .../lib/browser-async/filesystem.js | 291 ++++++++ .../preview2-shim/lib/browser-async/http.js | 5 + .../browser-async/http/incoming-handler.js | 18 + .../browser-async/http/outgoing-handler.js | 7 + .../lib/browser-async/http/types.js | 703 ++++++++++++++++++ .../preview2-shim/lib/browser-async/index.js | 4 +- .../preview2-shim/lib/browser-async/io.js | 7 + .../lib/browser-async/io/error.js | 18 + .../lib/browser-async/io/poll.js | 78 ++ .../lib/browser-async/io/streams.js | 268 +++++++ .../preview2-shim/lib/browser-async/random.js | 4 + .../lib/browser-async/random/insecure.js | 12 + .../lib/browser-async/random/random.js | 37 + .../lib/browser-async/sockets.js | 186 +++++ packages/preview2-shim/lib/browser/cli.js | 2 +- packages/preview2-shim/lib/browser/random.js | 2 +- src/cmd/transpile.js | 17 +- 22 files changed, 1917 insertions(+), 12 deletions(-) create mode 100644 packages/preview2-shim/lib/browser-async/cli.js create mode 100644 packages/preview2-shim/lib/browser-async/clocks.js create mode 100644 packages/preview2-shim/lib/browser-async/clocks/monotonic-clock.js create mode 100644 packages/preview2-shim/lib/browser-async/clocks/timezone.js create mode 100644 packages/preview2-shim/lib/browser-async/clocks/wall-clock.js create mode 100644 packages/preview2-shim/lib/browser-async/filesystem.js create mode 100644 packages/preview2-shim/lib/browser-async/http.js create mode 100644 packages/preview2-shim/lib/browser-async/http/incoming-handler.js create mode 100644 packages/preview2-shim/lib/browser-async/http/outgoing-handler.js create mode 100644 packages/preview2-shim/lib/browser-async/http/types.js create mode 100644 packages/preview2-shim/lib/browser-async/io.js create mode 100644 packages/preview2-shim/lib/browser-async/io/error.js create mode 100644 packages/preview2-shim/lib/browser-async/io/poll.js create mode 100644 packages/preview2-shim/lib/browser-async/io/streams.js create mode 100644 packages/preview2-shim/lib/browser-async/random.js create mode 100644 packages/preview2-shim/lib/browser-async/random/insecure.js create mode 100644 packages/preview2-shim/lib/browser-async/random/random.js create mode 100644 packages/preview2-shim/lib/browser-async/sockets.js diff --git a/packages/preview2-shim/lib/browser-async/cli.js b/packages/preview2-shim/lib/browser-async/cli.js new file mode 100644 index 000000000..0d21db821 --- /dev/null +++ b/packages/preview2-shim/lib/browser-async/cli.js @@ -0,0 +1,135 @@ +import { InputStream, OutputStream } from './io/streams.js'; +import { _setCwd as fsSetCwd } from './filesystem.js'; + +const textDecoder = new TextDecoder(); + +let _env = [], _args = [], _cwd = "/"; +export function _setEnv (envObj) { + _env = Object.entries(envObj); +} +export function _setArgs (args) { + _args = args; +} + +export function _setCwd (cwd) { + fsSetCwd(_cwd = cwd); +} + +export const environment = { + getEnvironment () { + return _env; + }, + getArguments () { + return _args; + }, + initialCwd () { + return _cwd; + } +}; + +class ComponentExit extends Error { + constructor(code) { + super(`Component exited ${code === 0 ? 'successfully' : 'with error'}`); + this.exitError = true; + this.code = code; + } +} + +export const exit = { + exit (status) { + throw new ComponentExit(status.tag === 'err' ? 1 : 0); + }, + exitWithCode (code) { + throw new ComponentExit(code); + } +}; + +let stdinStream; +export const stdin = { + InputStream, + getStdin () { + if (!stdinStream) { + stdinStream = new InputStream(); + } + return stdinStream; + } +}; + +let stdoutStream; +export const stdout = { + OutputStream, + getStdout () { + if (!stdoutStream) { + stdoutStream = new OutputStream( + new WritableStream({ + write: (contents) => { + // console.log() inserts a '\n' (which is 10) so try to skip that + if (contents[contents.length - 1] === 10) { + contents = contents.subarray(0, contents.length - 1); + } + console.log(textDecoder.decode(contents)); + }, + }) + ); + } + return stdoutStream; + } +}; + +let stderrStream; +export const stderr = { + OutputStream, + getStderr () { + if (!stderrStream) { + stderrStream = new OutputStream( + new WritableStream({ + write: (contents) => { + // console.log() inserts a '\n' (which is 10) so try to skip that + if (contents[contents.length - 1] === 10) { + contents = contents.subarray(0, contents.length - 1); + } + console.error(textDecoder.decode(contents)); + }, + }) + ); + } + return stderrStream; + } +}; + +class TerminalInput {} +class TerminalOutput {} + +const terminalStdoutInstance = new TerminalOutput(); +const terminalStderrInstance = new TerminalOutput(); +const terminalStdinInstance = new TerminalInput(); + +export const terminalInput = { + TerminalInput +}; + +export const terminalOutput = { + TerminalOutput +}; + +export const terminalStderr = { + TerminalOutput, + getTerminalStderr () { + return terminalStderrInstance; + } +}; + +export const terminalStdin = { + TerminalInput, + getTerminalStdin () { + return terminalStdinInstance; + } +}; + +export const terminalStdout = { + TerminalOutput, + getTerminalStdout () { + return terminalStdoutInstance; + } +}; + diff --git a/packages/preview2-shim/lib/browser-async/clocks.js b/packages/preview2-shim/lib/browser-async/clocks.js new file mode 100644 index 000000000..1047a210a --- /dev/null +++ b/packages/preview2-shim/lib/browser-async/clocks.js @@ -0,0 +1,5 @@ +// wasi:clocks@0.2.2 interfaces + +export * as monotonicClock from './clocks/monotonic-clock.js'; +export * as timezone from './clocks/timezone.js'; +export * as wallClock from './clocks/wall-clock.js'; diff --git a/packages/preview2-shim/lib/browser-async/clocks/monotonic-clock.js b/packages/preview2-shim/lib/browser-async/clocks/monotonic-clock.js new file mode 100644 index 000000000..7b7fe9d32 --- /dev/null +++ b/packages/preview2-shim/lib/browser-async/clocks/monotonic-clock.js @@ -0,0 +1,66 @@ +// wasi:clocks/monotonic-clock@0.2.0 interface + +import { Pollable } from "../io/poll.js"; + +/** + * An instant in time, in nanoseconds. An instant is relative to an unspecified + * initial value, and can only be compared to instances from the same monotonic-clock. + * + * @typedef {bigint} Instant + */ + +/** + * A duration of time, in nanoseconds. + * + * @typedef {bigint} Duration + */ + +/** + * Read the current value of the clock. + * + * The clock is monotonic, therefore calling this function repeatedly will produce a + * sequence of non-decreasing values. + * + * @returns {Instant} + */ +export const now = () => { + // performance.now() is in milliseconds, convert to nanoseconds + return BigInt(Math.floor(performance.now() * 1e6)); +}; + +/** + * Query the resolution of the clock. Returns the duration of time corresponding to a + * clock tick. + * + * @returns {Duration} + */ +export const resolution = () => { + // millisecond accuracy + return BigInt(1e6); +}; + +/** + * Create a `Pollable` which will resolve once the specified instant occured. + * + * @param {Instant} when + * @returns {Pollable} + */ +export const subscribeInstant = (when) => subscribeDuration(when - now()); + +/** + * Create a `Pollable` which will resolve once the given duration has elapsed, starting + * at the time at which this function was called. occured. + * + * Implemented with `setTimeout` that is specified in millisecond resolution. + * + * @param {Duration} when + * @returns {Pollable} + */ +export const subscribeDuration = (when) => { + if (when < 0) return new Pollable(); + return new Pollable( + new Promise((resolve) => { + setTimeout(resolve, Math.ceil(Number(when) / 1e6)); + }), + ); +}; diff --git a/packages/preview2-shim/lib/browser-async/clocks/timezone.js b/packages/preview2-shim/lib/browser-async/clocks/timezone.js new file mode 100644 index 000000000..917c68160 --- /dev/null +++ b/packages/preview2-shim/lib/browser-async/clocks/timezone.js @@ -0,0 +1,25 @@ +// wasi:clocks/timezone@0.2.2 interface + +/** + * @typedef {{ + * utcOffset: number, + * name: string, + * inDaylightSavingTime: boolean, + * }} TimezoneDisplay + */ + +/** + * @param {Datetime} _when + * @returns {TimezoneDisplay} + */ +export const display = (_when) => { + throw 'unimplemented'; +}; + +/** + * @param {Datetime} _when + * @returns {number} + */ +export const utcOffset = (_when) => { + throw 'unimplemented'; +}; diff --git a/packages/preview2-shim/lib/browser-async/clocks/wall-clock.js b/packages/preview2-shim/lib/browser-async/clocks/wall-clock.js new file mode 100644 index 000000000..2ef8de532 --- /dev/null +++ b/packages/preview2-shim/lib/browser-async/clocks/wall-clock.js @@ -0,0 +1,39 @@ +// wasi:clocks/wall-clock@0.2.0 interface + +/** + * A time and date in seconds plus nanoseconds. + * + * @typdef{{seconds: bigint, nanoseconds: number}} Datetime + */ + +/** + * Read the current value of the clock. + * + * This clock is not monotonic, therefore calling this function repeatedly will + * not necessarily produce a sequence of non-decreasing values. + * + * The returned timestamps represent the number of seconds since + * 1970-01-01T00:00:00Z, also known as POSIX's Seconds Since the Epoch, also + * known as Unix Time. + * + * The nanoseconds field of the output is always less than 1000000000. + * + * @returns {Datetime} + */ +export const now = () => { + const now = Date.now(); // in milliseconds + const seconds = BigInt(Math.floor(now / 1e3)); + const nanoseconds = (now % 1e3) * 1e6; + return { seconds, nanoseconds }; +}; + +/** + * Query the resolution of the clock. + * + * The nanoseconds field of the output is always less than 1000000000. + * + * @returns {Datetime} + */ +export const resolution = () => { + return { seconds: 0n, nanoseconds: 1e6 }; +}; diff --git a/packages/preview2-shim/lib/browser-async/filesystem.js b/packages/preview2-shim/lib/browser-async/filesystem.js new file mode 100644 index 000000000..f37a09436 --- /dev/null +++ b/packages/preview2-shim/lib/browser-async/filesystem.js @@ -0,0 +1,291 @@ +import { InputStream } from './io/streams.js'; +import { environment } from './cli.js'; + +let _cwd = "/"; + +export function _setCwd (cwd) { + _cwd = cwd; +} + +export function _setFileData (fileData) { + _fileData = fileData; + _rootPreopen[0] = new Descriptor(fileData); + const cwd = environment.initialCwd(); + _setCwd(cwd || '/'); +} + +export function _getFileData () { + return JSON.stringify(_fileData); +} + +let _fileData = { dir: {} }; + +const timeZero = { + seconds: BigInt(0), + nanoseconds: 0 +}; + +function getChildEntry (parentEntry, subpath, openFlags) { + if (subpath === '.' && _rootPreopen && descriptorGetEntry(_rootPreopen[0]) === parentEntry) { + subpath = _cwd; + if (subpath.startsWith('/') && subpath !== '/') + subpath = subpath.slice(1); + } + let entry = parentEntry; + let segmentIdx; + do { + if (!entry || !entry.dir) throw 'not-directory'; + segmentIdx = subpath.indexOf('/'); + const segment = segmentIdx === -1 ? subpath : subpath.slice(0, segmentIdx); + if (segment === '..') throw 'no-entry'; + if (segment === '.' || segment === ''); + else if (!entry.dir[segment] && openFlags.create) + entry = entry.dir[segment] = openFlags.directory ? { dir: {} } : { source: new Uint8Array([]) }; + else + entry = entry.dir[segment]; + subpath = subpath.slice(segmentIdx + 1); + } while (segmentIdx !== -1) + if (!entry) throw 'no-entry'; + return entry; +} + +function getSource (fileEntry) { + if (typeof fileEntry.source === 'string') { + fileEntry.source = new TextEncoder().encode(fileEntry.source); + } + return fileEntry.source; +} + +class DirectoryEntryStream { + constructor (entries) { + this.idx = 0; + this.entries = entries; + } + readDirectoryEntry () { + if (this.idx === this.entries.length) + return null; + const [name, entry] = this.entries[this.idx]; + this.idx += 1; + return { + name, + type: entry.dir ? 'directory' : 'regular-file' + }; + } +} + +class Descriptor { + #stream; + #entry; + #mtime = 0; + + _getEntry (descriptor) { + return descriptor.#entry; + } + + constructor (entry, isStream) { + if (isStream) + this.#stream = entry; + else + this.#entry = entry; + } + + readViaStream(offset) { + let buf = getSource(this.#entry).subarray(Number(offset)); + return new InputStream( + new ReadableStream({ + pull: (controller) => { + if (buf.byteLength === 0) { + buf = null; + controller.close(); + } else { + controller.enqueue(buf.slice(0, 4096)); // max 4KB; slice() to copy + buf = buf.subarray(4096); + } + }, + cancel: () => { + buf = null; + }, + }), + ); + } + + writeViaStream(_offset) { + throw 'unimplemented'; + //const entry = this.#entry; + //let offset = Number(_offset); + //return new OutputStream({ + // write (buf) { + // const newSource = new Uint8Array(buf.byteLength + entry.source.byteLength); + // newSource.set(entry.source, 0); + // newSource.set(buf, offset); + // offset += buf.byteLength; + // entry.source = newSource; + // return buf.byteLength; + // } + //}); + } + + appendViaStream() { + console.log(`[filesystem] APPEND STREAM`); + } + + advise(descriptor, offset, length, advice) { + console.log(`[filesystem] ADVISE`, descriptor, offset, length, advice); + } + + syncData() { + console.log(`[filesystem] SYNC DATA`); + } + + getFlags() { + console.log(`[filesystem] FLAGS FOR`); + } + + getType() { + if (this.#stream) return 'fifo'; + if (this.#entry.dir) return 'directory'; + if (this.#entry.source) return 'regular-file'; + return 'unknown'; + } + + setSize(size) { + console.log(`[filesystem] SET SIZE`, size); + } + + setTimes(dataAccessTimestamp, dataModificationTimestamp) { + console.log(`[filesystem] SET TIMES`, dataAccessTimestamp, dataModificationTimestamp); + } + + read(length, offset) { + const source = getSource(this.#entry); + return [source.slice(offset, offset + length), offset + length >= source.byteLength]; + } + + write(buffer, offset) { + if (offset !== 0) throw 'invalid-seek'; + this.#entry.source = buffer; + return buffer.byteLength; + } + + readDirectory() { + if (!this.#entry?.dir) + throw 'bad-descriptor'; + return new DirectoryEntryStream(Object.entries(this.#entry.dir).sort(([a], [b]) => a > b ? 1 : -1)); + } + + sync() { + console.log(`[filesystem] SYNC`); + } + + createDirectoryAt(path) { + const entry = getChildEntry(this.#entry, path, { create: true, directory: true }); + if (entry.source) throw 'exist'; + } + + stat() { + let type = 'unknown', size = BigInt(0); + if (this.#entry.source) { + type = 'regular-file'; + const source = getSource(this.#entry); + size = BigInt(source.byteLength); + } + else if (this.#entry.dir) { + type = 'directory'; + } + return { + type, + linkCount: BigInt(0), + size, + dataAccessTimestamp: timeZero, + dataModificationTimestamp: timeZero, + statusChangeTimestamp: timeZero, + } + } + + statAt(_pathFlags, path) { + const entry = getChildEntry(this.#entry, path, { create: false, directory: false }); + let type = 'unknown', size = BigInt(0); + if (entry.source) { + type = 'regular-file'; + const source = getSource(entry); + size = BigInt(source.byteLength); + } + else if (entry.dir) { + type = 'directory'; + } + return { + type, + linkCount: BigInt(0), + size, + dataAccessTimestamp: timeZero, + dataModificationTimestamp: timeZero, + statusChangeTimestamp: timeZero, + }; + } + + setTimesAt() { + console.log(`[filesystem] SET TIMES AT`); + } + + linkAt() { + console.log(`[filesystem] LINK AT`); + } + + openAt(_pathFlags, path, openFlags, _descriptorFlags, _modes) { + const childEntry = getChildEntry(this.#entry, path, openFlags); + return new Descriptor(childEntry); + } + + readlinkAt() { + console.log(`[filesystem] READLINK AT`); + } + + removeDirectoryAt() { + console.log(`[filesystem] REMOVE DIR AT`); + } + + renameAt() { + console.log(`[filesystem] RENAME AT`); + } + + symlinkAt() { + console.log(`[filesystem] SYMLINK AT`); + } + + unlinkFileAt() { + console.log(`[filesystem] UNLINK FILE AT`); + } + + isSameObject(other) { + return other === this; + } + + metadataHash() { + let upper = BigInt(0); + upper += BigInt(this.#mtime); + return { upper, lower: BigInt(0) }; + } + + metadataHashAt(_pathFlags, _path) { + let upper = BigInt(0); + upper += BigInt(this.#mtime); + return { upper, lower: BigInt(0) }; + } +} +const descriptorGetEntry = Descriptor.prototype._getEntry; +delete Descriptor.prototype._getEntry; + +let _preopens = [[new Descriptor(_fileData), '/']], _rootPreopen = _preopens[0]; + +export const preopens = { + getDirectories () { + return _preopens; + } +} + +export const types = { + Descriptor, + DirectoryEntryStream +}; + +export { types as filesystemTypes } diff --git a/packages/preview2-shim/lib/browser-async/http.js b/packages/preview2-shim/lib/browser-async/http.js new file mode 100644 index 000000000..7d5929419 --- /dev/null +++ b/packages/preview2-shim/lib/browser-async/http.js @@ -0,0 +1,5 @@ +// wasi:http@0.2.0 interfaces + +export * as types from './http/types.js'; +export * as incomingHandler from './http/incoming-handler.js'; +export * as outgoingHandler from './http/outgoing-handler.js'; diff --git a/packages/preview2-shim/lib/browser-async/http/incoming-handler.js b/packages/preview2-shim/lib/browser-async/http/incoming-handler.js new file mode 100644 index 000000000..a1d905218 --- /dev/null +++ b/packages/preview2-shim/lib/browser-async/http/incoming-handler.js @@ -0,0 +1,18 @@ +// wasi:http/incoming-handler@0.2.0 interface + +import { IncomingRequest, ResponseOutparam } from "./types.js"; + +export const handle = async (_request, _responseOut) => {}; + +//type wasiHandle = (request: IncomingRequest, responseOut: ResponseOutparam) => Promise; + +export const getHandler = (handle) => async (req) => { + const responseOut = new ResponseOutparam(); + await handle(IncomingRequest.fromRequest(req), responseOut); + const result = await responseOut.promise; + if (result.tag === 'ok') { + return result.val.toResponse(); + } else { + throw result; // error + } +}; diff --git a/packages/preview2-shim/lib/browser-async/http/outgoing-handler.js b/packages/preview2-shim/lib/browser-async/http/outgoing-handler.js new file mode 100644 index 000000000..1bc119e70 --- /dev/null +++ b/packages/preview2-shim/lib/browser-async/http/outgoing-handler.js @@ -0,0 +1,7 @@ +// wasi:http/outgoing-handler@0.2.0 interface + +import { FutureIncomingResponse } from "./types.js"; + +export const handle = (request, _options) => { + return new FutureIncomingResponse(request); +}; diff --git a/packages/preview2-shim/lib/browser-async/http/types.js b/packages/preview2-shim/lib/browser-async/http/types.js new file mode 100644 index 000000000..835a415e6 --- /dev/null +++ b/packages/preview2-shim/lib/browser-async/http/types.js @@ -0,0 +1,703 @@ +// wasi:http/types@0.2.0 interface + +//import { Duration } from "../clocks/monotonic-clock.js"; +import { InputStream, OutputStream } from "../io/streams.js"; +import { Pollable } from "../io/poll.js"; + +//export type Result = { tag: 'ok', val: T } | { tag: 'err', val: E }; +const symbolDispose = Symbol.dispose || Symbol.for("dispose"); + +//export type Method = MethodGet | MethodHead | MethodPost | MethodPut | MethodDelete | MethodConnect | MethodOptions | MethodTrace | MethodPatch | MethodOther; +//export interface MethodGet { +// tag: 'get', +//} +//export interface MethodHead { +// tag: 'head', +//} +//export interface MethodPost { +// tag: 'post', +//} +//export interface MethodPut { +// tag: 'put', +//} +//export interface MethodDelete { +// tag: 'delete', +//} +//export interface MethodConnect { +// tag: 'connect', +//} +//export interface MethodOptions { +// tag: 'options', +//} +//export interface MethodTrace { +// tag: 'trace', +//} +//export interface MethodPatch { +// tag: 'patch', +//} +//export interface MethodOther { +// tag: 'other', +// val: string, +//} +// +// +//export type Scheme = SchemeHttp | SchemeHttps | SchemeOther; +//export interface SchemeHttp { +// tag: 'HTTP', +//} +//export interface SchemeHttps { +// tag: 'HTTPS', +//} +//export interface SchemeOther { +// tag: 'other', +// val: string, +//} +// +// +//export interface DnsErrorPayload { +// rcode?: string, +// infoCode?: number, +//} +// +// +//export interface TlsAlertReceivedPayload { +// alertId?: number, +// alertMessage?: string, +//} +// +// +//export interface FieldSizePayload { +// fieldName?: string, +// fieldSize?: number, +//} +// +// +//export type ErrorCode = ErrorCodeDnsTimeout | ErrorCodeDnsError | ErrorCodeDestinationNotFound | ErrorCodeDestinationUnavailable | ErrorCodeDestinationIpProhibited | ErrorCodeDestinationIpUnroutable | ErrorCodeConnectionRefused | ErrorCodeConnectionTerminated | ErrorCodeConnectionTimeout | ErrorCodeConnectionReadTimeout | ErrorCodeConnectionWriteTimeout | ErrorCodeConnectionLimitReached | ErrorCodeTlsProtocolError | ErrorCodeTlsCertificateError | ErrorCodeTlsAlertReceived | ErrorCodeHttpRequestDenied | ErrorCodeHttpRequestLengthRequired | ErrorCodeHttpRequestBodySize | ErrorCodeHttpRequestMethodInvalid | ErrorCodeHttpRequestUriInvalid | ErrorCodeHttpRequestUriTooLong | ErrorCodeHttpRequestHeaderSectionSize | ErrorCodeHttpRequestHeaderSize | ErrorCodeHttpRequestTrailerSectionSize | ErrorCodeHttpRequestTrailerSize | ErrorCodeHttpResponseIncomplete | ErrorCodeHttpResponseHeaderSectionSize | ErrorCodeHttpResponseHeaderSize | ErrorCodeHttpResponseBodySize | ErrorCodeHttpResponseTrailerSectionSize | ErrorCodeHttpResponseTrailerSize | ErrorCodeHttpResponseTransferCoding | ErrorCodeHttpResponseContentCoding | ErrorCodeHttpResponseTimeout | ErrorCodeHttpUpgradeFailed | ErrorCodeHttpProtocolError | ErrorCodeLoopDetected | ErrorCodeConfigurationError | ErrorCodeInternalError; +//export interface ErrorCodeDnsTimeout { +// tag: 'DNS-timeout', +//} +//export interface ErrorCodeDnsError { +// tag: 'DNS-error', +// val: DnsErrorPayload, +//} +//export interface ErrorCodeDestinationNotFound { +// tag: 'destination-not-found', +//} +//export interface ErrorCodeDestinationUnavailable { +// tag: 'destination-unavailable', +//} +//export interface ErrorCodeDestinationIpProhibited { +// tag: 'destination-IP-prohibited', +//} +//export interface ErrorCodeDestinationIpUnroutable { +// tag: 'destination-IP-unroutable', +//} +//export interface ErrorCodeConnectionRefused { +// tag: 'connection-refused', +//} +//export interface ErrorCodeConnectionTerminated { +// tag: 'connection-terminated', +//} +//export interface ErrorCodeConnectionTimeout { +// tag: 'connection-timeout', +//} +//export interface ErrorCodeConnectionReadTimeout { +// tag: 'connection-read-timeout', +//} +//export interface ErrorCodeConnectionWriteTimeout { +// tag: 'connection-write-timeout', +//} +//export interface ErrorCodeConnectionLimitReached { +// tag: 'connection-limit-reached', +//} +//export interface ErrorCodeTlsProtocolError { +// tag: 'TLS-protocol-error', +//} +//export interface ErrorCodeTlsCertificateError { +// tag: 'TLS-certificate-error', +//} +//export interface ErrorCodeTlsAlertReceived { +// tag: 'TLS-alert-received', +// val: TlsAlertReceivedPayload, +//} +//export interface ErrorCodeHttpRequestDenied { +// tag: 'HTTP-request-denied', +//} +//export interface ErrorCodeHttpRequestLengthRequired { +// tag: 'HTTP-request-length-required', +//} +//export interface ErrorCodeHttpRequestBodySize { +// tag: 'HTTP-request-body-size', +// val: bigint | undefined, +//} +//export interface ErrorCodeHttpRequestMethodInvalid { +// tag: 'HTTP-request-method-invalid', +//} +//export interface ErrorCodeHttpRequestUriInvalid { +// tag: 'HTTP-request-URI-invalid', +//} +//export interface ErrorCodeHttpRequestUriTooLong { +// tag: 'HTTP-request-URI-too-long', +//} +//export interface ErrorCodeHttpRequestHeaderSectionSize { +// tag: 'HTTP-request-header-section-size', +// val: number | undefined, +//} +//export interface ErrorCodeHttpRequestHeaderSize { +// tag: 'HTTP-request-header-size', +// val: FieldSizePayload | undefined, +//} +//export interface ErrorCodeHttpRequestTrailerSectionSize { +// tag: 'HTTP-request-trailer-section-size', +// val: number | undefined, +//} +//export interface ErrorCodeHttpRequestTrailerSize { +// tag: 'HTTP-request-trailer-size', +// val: FieldSizePayload, +//} +//export interface ErrorCodeHttpResponseIncomplete { +// tag: 'HTTP-response-incomplete', +//} +//export interface ErrorCodeHttpResponseHeaderSectionSize { +// tag: 'HTTP-response-header-section-size', +// val: number | undefined, +//} +//export interface ErrorCodeHttpResponseHeaderSize { +// tag: 'HTTP-response-header-size', +// val: FieldSizePayload, +//} +//export interface ErrorCodeHttpResponseBodySize { +// tag: 'HTTP-response-body-size', +// val: bigint | undefined, +//} +//export interface ErrorCodeHttpResponseTrailerSectionSize { +// tag: 'HTTP-response-trailer-section-size', +// val: number | undefined, +//} +//export interface ErrorCodeHttpResponseTrailerSize { +// tag: 'HTTP-response-trailer-size', +// val: FieldSizePayload, +//} +//export interface ErrorCodeHttpResponseTransferCoding { +// tag: 'HTTP-response-transfer-coding', +// val: string | undefined, +//} +//export interface ErrorCodeHttpResponseContentCoding { +// tag: 'HTTP-response-content-coding', +// val: string | undefined, +//} +//export interface ErrorCodeHttpResponseTimeout { +// tag: 'HTTP-response-timeout', +//} +//export interface ErrorCodeHttpUpgradeFailed { +// tag: 'HTTP-upgrade-failed', +//} +//export interface ErrorCodeHttpProtocolError { +// tag: 'HTTP-protocol-error', +//} +//export interface ErrorCodeLoopDetected { +// tag: 'loop-detected', +//} +//export interface ErrorCodeConfigurationError { +// tag: 'configuration-error', +//} +//// This is a catch-all error for anything that doesn't fit cleanly into a +//// more specific case. It also includes an optional string for an +//// unstructured description of the error. Users should not depend on the +//// string for diagnosing errors, as it's not required to be consistent +//// between implementations. +//export interface ErrorCodeInternalError { +// tag: 'internal-error', +// val: string | undefined, +//} +// +// +//export type HeaderError = HeaderErrorInvalidSyntax | HeaderErrorForbidden | HeaderErrorImmutable; +//export interface HeaderErrorInvalidSyntax { +// tag: 'invalid-syntax', +//} +//export interface HeaderErrorForbidden { +// tag: 'forbidden', +//} +//export interface HeaderErrorImmutable { +// tag: 'immutable', +//} +// +// +//export type FieldKey = string; +//export type FieldValue = Uint8Array; + +/** + * @typedef {string} FieldKey + */ + +/** + * @typedef {Uint8Array} FieldValue + */ + +export class Fields { + headers; + immutable; + + /** + * @param {Headers|undefined} headers + * @param {boolean|undefined} immutable + */ + constructor(headers = new Headers(), immutable = false) { + this.headers = headers; + this.immutable = immutable; + } + + /** + * @param {Array<[FieldKey, FieldValue]>} entries + * @returns {Fields} + */ + static fromList(entries) { + const fields = new Fields(); + const dec = new TextDecoder(); + for (const [key, val] of entries) { + fields.headers.append(key, dec.decode(val)); + } + return fields; + } + + /** + * @param {FieldKey} name + * @returns {Array} + */ + get(name) { + const enc = new TextEncoder(); + return this.headers.get(name)?.split(', ').map((val) => enc.encode(val)) || []; + } + /** + * @param {FieldKey} name + * @returns {boolean} + */ + has(name) { + return this.headers.has(name); + } + /** + * @param {FieldKey} name + * @param {Array} value + */ + set(name, value) { + if (this.immutable) { + throw { tag: 'immutable' }; + } + const dec = new TextDecoder(); + this.headers.set(name, value.map((val) => dec.decode(val)).join(', ')); + } + /** + * @param {FieldKey} name + */ + 'delete'(name) { + if (this.immutable) { + throw { tag: 'immutable' }; + } + this.headers.delete(name); + } + /** + * @param {FieldKey} name + * @param {FieldValue} value + */ + append(name, value) { + if (this.immutable) { + throw { tag: 'immutable' }; + } + const dec = new TextDecoder(); + this.headers.append(name, dec.decode(value)); + } + /** + * @returns {Array<[FieldKey, FieldValue]>} + */ + entries() { + const entries = []; + const enc = new TextEncoder(); + this.headers.forEach((val, key) => { + entries.push([key, enc.encode(val)]); + }); + return entries; + } + /** + * @returns {Fields} + */ + clone() { + const fields = new Fields(); + this.headers.forEach((val, key) => { + fields.headers.set(key, val); + }); + return fields; + } +} + +//export type Headers = Fields; +//export type Trailers = Fields; + + +export class IncomingRequest { + #method; + #pathWithQuery; + #scheme; + #authority; + #headers; + #body; + + /** + * @param {Method} method + * @param {string|undefined} pathWithQuery + * @param {Scheme|undefined} scheme + * @param {string|undefined} authority + * @param {Fields|undefined} headers + * @param {IncomingBody|undefined} body + */ + constructor(method, pathWithQuery, scheme, authority, headers, body) { + this.#method = method; + this.#pathWithQuery = pathWithQuery; + this.#scheme = scheme; + this.#authority = authority; + this.#headers = headers || new Fields(); + this.#body = body; + } + + /** + * @returns {Method} + */ + method() { + return this.#method; + } + /** + * @returns {string|undefined} + */ + pathWithQuery() { + return this.#pathWithQuery; + } + /** + * @returns {Scheme|undefined} + */ + scheme() { + return this.#scheme; + } + /** + * @returns {string|undefined} + */ + authority() { + return this.#authority; + } + /** + * @returns {Fields} + */ + headers() { + return this.#headers; + } + /** + * @returns {IncomingBody} + */ + consume() { + if (this.#body) { + return this.#body; + } + throw undefined; + } + [symbolDispose]() { + } + + /** + * @param {Request} req + * @returns {IncomingRequest} + */ + static fromRequest(req) { + const method = { tag: req.method.toLowerCase() }; + const url = new URL(req.url); + const scheme = { tag: url.protocol.slice(0, -1).toUpperCase() }; + const authority = url.host; + const pathWithQuery = `${url.pathname}${url.search}${url.hash}`; + const headers = new Fields(req.headers, true); + const body = new IncomingBody(new InputStream(req.body)); + + return new IncomingRequest(method, pathWithQuery, scheme, authority, headers, body); + } +} + +export class OutgoingRequest { + #headers; + #method; + #pathWithQuery; + #scheme; + #authority; + #body; + + /** + * @param {Fields} headers + */ + constructor(headers) { + headers.immutable = true; + this.#headers = headers; + this.#body = new OutgoingBody; + } + /** + * @returns {OutgoingBody} + */ + body() { + return this.#body; + } + /** + * @returns {Method} + */ + method() { + return this.#method || { tag: 'get' }; + } + /** + * @param {Method} method + */ + setMethod(method) { + this.#method = method; + } + /** + * @returns {string|undefined} + */ + pathWithQuery() { + return this.#pathWithQuery; + } + /** + * @param {string|undefined} pathWithQuery + */ + setPathWithQuery(pathWithQuery) { + this.#pathWithQuery = pathWithQuery; + } + scheme() { + return this.#scheme; + } + setScheme(scheme) { + this.#scheme = scheme; + } + authority() { + return this.#authority; + } + setAuthority(authority) { + this.#authority = authority; + } + headers() { + return this.#headers; + } + + toRequest() { + if (this.#scheme && this.#scheme.tag === 'other' || !this.#authority) { + throw { tag: 'destination-not-found' }; + } + const path = this.#pathWithQuery ? + (this.#pathWithQuery.startsWith('/') ? this.#pathWithQuery : `/${this.#pathWithQuery}`) + : ''; + + const method = (this.#method ? this.#method.tag : 'get'); + const body = method === 'get' || method === 'head' ? undefined : this.#body.stream.readable; + return new Request(`${this.#scheme ? this.#scheme.tag : 'HTTPS'}://${this.#authority}${path}`, { + method, + headers: this.#headers.headers, + body, + }); + } +} + +// TODO +export class RequestOptions { + constructor() {} + connectTimeout() { + return; + } + setConnectTimeout(_duration) { + return; + } + firstByteTimeout() { + return; + } + setFirstByteTimeout(_duration) { + return; + } + betweenBytesTimeout() { + return; + } + setBetweenBytesTimeout(_duration) { + return; + } +} + +export class ResponseOutparam { + promise; /** Promise> */ + resolve; /** (result: Result) => void */ + + constructor() { + this.promise = new Promise((resolve) => { + this.resolve = resolve; + }); + } + + static set(param, response) { + param.resolve(response); + } +} + +//export type StatusCode = number; + +export class IncomingResponse { + #statusCode; + #headers; + #body; + + constructor(statusCode, headers, body) { + this.#statusCode = statusCode; + this.#headers = headers; + this.#body = body; + } + + status() { + return this.#statusCode; + } + headers() { + return this.#headers; + } + consume() { + return this.#body; + } +} + +export class IncomingBody { + #stream; + + constructor(stream) { + this.#stream = stream; + } + stream() { + return this.#stream; + } + static finish(_body) { + return new FutureTrailers(); + } + [symbolDispose]() { + } +} + +export class FutureTrailers { + #trailers; + #errCode; + + constructor(trailers, errCode) { + this.#trailers = trailers; + this.#errCode = errCode; + } + subscribe() { + return new Pollable(); + } + get() { + if (this.#errCode) { + return { tag: 'ok', val: { tag: 'err', val: this.#errCode } }; + } + return { tag: 'ok', val: { tag: 'ok', val: this.#trailers } }; + } +} + +export class OutgoingResponse { + #headers; + #statusCode; + #body; + + constructor(headers) { + this.#headers = headers; + this.#statusCode = 200; + this.#body = new OutgoingBody(); + } + statusCode() { + return this.#statusCode; + } + setStatusCode(statusCode) { + this.#statusCode = statusCode; + } + headers() { + return this.#headers; + } + body() { + return this.#body; + } + [symbolDispose]() { + } + + toResponse() { + return new Response( + this.#body.stream.readable, + { + status: this.#statusCode, + headers: this.#headers.headers, + }, + ); + } +} + +export class OutgoingBody { + finished; + stream; + + constructor() { + this.finished = false; + this.stream = new OutputStream(); + } + write() { + return this.stream; + } + static finish(body, trailers) { + // trailers not supported + if (trailers) { + throw { tag: 'HTTP-request-trailer-section-size' }; + } + body.stream.close(); + body.finished = true; + } + [symbolDispose]() { + OutgoingBody.finish(this); + } +} + +export class FutureIncomingResponse { + #promise; + #resolvedResponse; + #ready = false; + #error; + + constructor(request) { + try { + this.#promise = fetch(request.toRequest()).then((response) => { + this.#ready = true; + this.#resolvedResponse = response; + }); + } catch (err) { + this.#promise = Promise.resolve(); + this.#ready = true; + // TODO better error handling + this.#error = { tag: 'internal-error', val: err.toString() }; + } + } + + subscribe() { + return new PollablePromise(this.#promise); + } + get() { + if (!this.#ready) return; + if (this.#error) return { tag: 'err', val: this.#error }; + + const res = this.#resolvedResponse; + + return { + tag: 'ok', + val: { + tag: 'ok', + val: new IncomingResponse( + res.status, + new Fields(res.headers, true), + new IncomingBody(new InputStream(res.body)), + ), + }, + }; + } +} + +export const httpErrorCode = (_err) => { + return; +}; diff --git a/packages/preview2-shim/lib/browser-async/index.js b/packages/preview2-shim/lib/browser-async/index.js index da23919e7..58a86db6c 100644 --- a/packages/preview2-shim/lib/browser-async/index.js +++ b/packages/preview2-shim/lib/browser-async/index.js @@ -1,17 +1,17 @@ +import * as cli from "./cli.js"; import * as clocks from "./clocks.js"; import * as filesystem from "./filesystem.js"; import * as http from "./http.js"; import * as io from "./io.js"; import * as random from "./random.js"; import * as sockets from "./sockets.js"; -import * as cli from "./cli.js"; export { + cli, clocks, filesystem, http, io, random, sockets, - cli, } diff --git a/packages/preview2-shim/lib/browser-async/io.js b/packages/preview2-shim/lib/browser-async/io.js new file mode 100644 index 000000000..8208ab47b --- /dev/null +++ b/packages/preview2-shim/lib/browser-async/io.js @@ -0,0 +1,7 @@ +// wasi:io@0.2.0 interfaces + +import { IoError } from './io/error.js'; +export const error = { Error: IoError }; + +export * as poll from './io/poll.js'; +export * as streams from './io/streams.js'; diff --git a/packages/preview2-shim/lib/browser-async/io/error.js b/packages/preview2-shim/lib/browser-async/io/error.js new file mode 100644 index 000000000..8df66fe87 --- /dev/null +++ b/packages/preview2-shim/lib/browser-async/io/error.js @@ -0,0 +1,18 @@ +// wasi:io/error@0.2.0 interface + +// A resource which represents some error information. +// +// The only method provided by this resource is to-debug-string, +// which provides some human-readable information about the error. +export class IoError extends Error { + #msg; + constructor(msg) { + super(msg); + this.#msg; + } + // Returns a string that is suitable to assist humans in debugging + // this error. + toDebugString() { + return this.#msg; + } +}; diff --git a/packages/preview2-shim/lib/browser-async/io/poll.js b/packages/preview2-shim/lib/browser-async/io/poll.js new file mode 100644 index 000000000..1d266eb70 --- /dev/null +++ b/packages/preview2-shim/lib/browser-async/io/poll.js @@ -0,0 +1,78 @@ +// wasi:io/poll@0.2.0 interface + +// Pollable represents a single I/O event which may be ready, or not. +export class Pollable { + #ready = false; + #promise; + + /** + * Sets the pollable to ready whether the promise is resolved or + * rejected. + * + * @param {Promise|undefined|null} promise + */ + constructor(promise) { + const setReady = () => { + this.#ready = true; + }; + this.#promise = (promise || Promise.resolve()).then(setReady, setReady); + } + + /** + * Return the readiness of a pollable. This function never blocks. + * + * Returns `true` when the pollable is ready, and `false` otherwise. + * + * @returns {boolean} + */ + ready() { + return this.#ready; + } + + /** + * Returns immediately if the pollable is ready, and otherwise blocks + * until ready. + * + * This function is equivalent to calling `poll.poll` on a list + * containing only this pollable. + */ + async block() { + await this.#promise; + } +} + +/** + * Poll for completion on a set of pollables. + * + * This function takes a list of pollables, which identify I/O + * sources of interest, and waits until one or more of the events + * is ready for I/O. + * + * The result list contains one or more indices of handles + * in the argument list that is ready for I/O. + * + * @param {Array} inList + * @returns {Promise} + */ +export const poll = async (inList) => { + if (inList.length === 1) { + // handle this common case faster + await inList[0].block(); + return new Uint32Array(1); // zero initialized of length 1 + } + + // wait until at least one is ready + await Promise.race(inList.map((pollable) => pollable.block())); + + // allocate a Uint32Array list as if all are ready + const ready = new Uint32Array(inList.length); + let pos = 0; + for (let i = 0; i < inList.length; i++) { + if (inList[i].ready()) { + ready[pos] = i; + pos++; + } + } + + return ready.subarray(0, pos); +}; diff --git a/packages/preview2-shim/lib/browser-async/io/streams.js b/packages/preview2-shim/lib/browser-async/io/streams.js new file mode 100644 index 000000000..c67b18280 --- /dev/null +++ b/packages/preview2-shim/lib/browser-async/io/streams.js @@ -0,0 +1,268 @@ +// wasi:io/streams@0.2.0 interface + +import { Pollable } from "./poll.js"; +import { IoError } from "./error.js"; + +export class InputStream { + #closed = false; + #reader = null; + #buffer = new Uint8Array(); + + /** + * @param {ReadableStream|undefined} stream + */ + constructor(stream) { + if (stream) { + this.#reader = stream.getReader(); + } else { + this.#closed = true; + } + } + + async #fillBuffer() { + const { value, done } = await this.#reader.read(); + if (done) this.#closed = done; + this.#buffer = value || new Uint8Array(0); + } + + /** + * @param {number|bigint} len + * @returns {Uint8Array} + */ + read(len) { + if (this.#buffer.byteLength === 0 && this.#closed) throw { tag: 'closed' }; + const n = Number(len); + if (n >= this.#buffer.byteLength) { + // read all that is in the buffer and reset buffer + const buf = this.#buffer; + this.#buffer = new Uint8Array(0); + return buf; + } else { + // read portion of the buffer and advance the buffer for next read + const buf = this.#buffer.subarray(0, n); + this.#buffer = this.#buffer.subarray(n); + return buf; + } + } + + /** + * @param {number|bigint} len + * @returns {Promise} + */ + async blockingRead(len) { + // if buffer has data, read that first + if (this.#buffer.byteLength > 0) return this.read(len); + if (this.#buffer.byteLength === 0 && this.#closed) throw { tag: 'closed' }; + await this.#fillBuffer(); + return this.read(len); + } + + /** + * @param {number|bigint} len + * @returns {bigint} + */ + skip(len) { + if (this.#buffer.byteLength === 0 && this.#closed) throw { tag: 'closed' }; + const n = Number(len); + if (n >= this.#buffer.byteLength) { + // skip all in buffer + const skipped = BigInt(this.#buffer.byteLength); + this.#buffer = new Uint8Array(0); + return skipped; + } else { + // skip part of the buffer + this.#buffer = this.#buffer.subarray(n); + return len; + } + } + + /** + * @param {number|bigint} len + * @returns {Promise} + */ + async blockingSkip(len) { + // if buffer has data, skip that first + if (this.#buffer.byteLength > 0) return this.skip(len); + await this.#fillBuffer(); + return this.skip(len); + } + + /** + * @returns {Pollable} + */ + subscribe() { + // return ready pollable if has bytes in buffer + if (this.#buffer.byteLength > 0) return new Pollable(); + return new Pollable(this.#fillBuffer()); + } +} + +export class OutputStream { + #readable; + #readableController; + #writer; + #prevWritePromise; + #prevWriteError; + #closed = false; + + /** + * @param {WritableStream|undefined} stream + */ + constructor(stream) { + if (stream) { + this.#writer = stream.getWriter(); + } else { + // enqueue a ReadableStream internally + this.readable = new ReadableStream({ + start: (controller) => { + this.#readableController = controller; + }, + cancel: () => { + this.#closed = true; + }, + type: 'bytes', + autoAllocateChunkSize: 4096, + }); + } + } + + /** + * @returns {ReadableStream|undefined} + */ + getReadableStream() { + return this.#readable; + } + + close() { + this.#closed = true; + if (this.#readableController) { + this.#readableController.close(); + } else { + this.#writer.close(); + } + } + + /** + * @returns {bigint} + */ + checkWrite() { + if (this.#closed) throw { tag: 'closed' }; + if (this.#prevWriteError) { + const err = this.#prevWriteError; + this.#prevWriteError = null; + throw err; + } + if (this.#prevWritePromise) return 0n; // not ready, waiting on previous write + return 4096n; // TODO for WritableStream + } + + /** + * @param {Uint8Array} contents + */ + write(contents) { + if (this.#closed) throw { tag: 'closed' }; + if (this.#readableController) { + this.#readableController?.enqueue(contents); + } else if (this.#prevWritePromise) { + throw new Error("waiting for previous write to finish"); + } else { + this.#prevWritePromise = this.#writer.write(contents).then( + () => { + this.#prevWritePromise = null; + }, + (err) => { + this.#prevWriteError = { tag: 'last-operation-failed', val: new IoError(err.toString()) }; + this.#prevWritePromise = null; + }, + ); + } + } + + /** + * @param {Uint8Array} contents + * @returns {Promise} + */ + async blockingWriteAndFlush(contents) { + if (this.#readableController) { + this.#readableController?.enqueue(contents); + } else if (this.#prevWritePromise) { + throw new Error("waiting for previous write to finish"); + } else { + try { + await this.#writer.write(contents); + } catch (err) { + throw { tag: 'last-operation-failed', val: new IoError(err.toString()) }; + } + } + } + + flush() { + if (this.#closed) throw { tag: 'closed' }; + if (this.#prevWriteError) { + const err = this.#prevWriteError; + this.#prevWriteError = null; + throw err; + } + } + + /** + * @returns {Promise} + */ + async blockingFlush() { + if (this.#closed) throw { tag: 'closed' }; + if (this.#prevWritePromise) { + await this.#prevWritePromise; + if (this.#prevWriteError) { + const err = this.#prevWriteError; + this.#prevWriteError = null; + throw err; + } + } + } + + /** + * @returns {Pollable} + */ + subscribe() { + return new Pollable(this.#prevWritePromise); + } + + /** + * @param {number|bigint} len + */ + writeZeroes(len) { + this.write(new Uint8Array(Number(len))); + } + + /** + * @param {number|bigint} len + * @returns {Promise} + */ + async blockingWriteZeroesAndFlush(len) { + await this.blockingWriteAndFlush(new Uint8Array(Number(len))); + } + + /** + * @param {InputStream} src + * @param {number|bigint} len + * @returns {bigint} + */ + splice(src, len) { + const n = this.checkWrite(); + const contents = src.read(Number(len) < n ? len : n); + this.write(contents); + return BigInt(contents.byteLength); + } + + /** + * @param {InputStream} src + * @param {number|bigint} len + * @returns {Promise} + */ + async blockingSplice(src, len) { + const n = this.checkWrite(); + const contents = await src.blockingRead(len < n ? len : n); + await this.blockingWriteAndFlush(contents); + return BigInt(contents.byteLength); + } +} diff --git a/packages/preview2-shim/lib/browser-async/random.js b/packages/preview2-shim/lib/browser-async/random.js new file mode 100644 index 000000000..c9f765c04 --- /dev/null +++ b/packages/preview2-shim/lib/browser-async/random.js @@ -0,0 +1,4 @@ +// wasi:random@0.2.0 interfaces + +export * as random from './random/random.js'; +export * as insecure from './random/insecure.js'; diff --git a/packages/preview2-shim/lib/browser-async/random/insecure.js b/packages/preview2-shim/lib/browser-async/random/insecure.js new file mode 100644 index 000000000..47239a7b8 --- /dev/null +++ b/packages/preview2-shim/lib/browser-async/random/insecure.js @@ -0,0 +1,12 @@ +// wasi:random/insecure@0.2.0 interface +// The insecure interface for insecure pseudo-random numbers. + +import { getRandomBytes, getRandomU64 } from "./random.js"; + +// Return len insecure pseudo-random bytes. +// In this case, just reuse the wasi:random/random interface. +export const getInsecureRandomBytes = getRandomBytes; + +// Return an insecure pseudo-random u64 value. +// In this case, just reuse the wasi:random/random interface. +export const getInsecureRandomU64 = getRandomU64; diff --git a/packages/preview2-shim/lib/browser-async/random/random.js b/packages/preview2-shim/lib/browser-async/random/random.js new file mode 100644 index 000000000..50bab1daa --- /dev/null +++ b/packages/preview2-shim/lib/browser-async/random/random.js @@ -0,0 +1,37 @@ +// wasi:random/random@0.2.0 interface +// WASI Random is a random data API. + +const MAX_BYTES = 65536; + +/** + * Return len cryptographically-secure random or pseudo-random bytes. + * + * @param {number|bigint} len + * @returns {Uint8Array} + */ +export const getRandomBytes = (len) => { + const bytes = new Uint8Array(Number(len)); + + if (len > MAX_BYTES) { + // this is the max bytes crypto.getRandomValues + // can do at once see https://developer.mozilla.org/en-US/docs/Web/API/window.crypto.getRandomValues + for (let pos = 0; pos < bytes.byteLength; pos += MAX_BYTES) { + // buffer.slice automatically checks if the end is past the end of + // the buffer so we don't have to here + crypto.getRandomValues(bytes.subarray(pos, pos + MAX_BYTES)); + } + } else { + crypto.getRandomValues(bytes); + } + + return bytes; +}; + +/** + * Return a cryptographically-secure random or pseudo-random u64 value. + * + * @returns {bigint} + */ +export const getRandomU64 = () => { + return crypto.getRandomValues(new BigUint64Array(1))[0]; +}; diff --git a/packages/preview2-shim/lib/browser-async/sockets.js b/packages/preview2-shim/lib/browser-async/sockets.js new file mode 100644 index 000000000..6dda749c7 --- /dev/null +++ b/packages/preview2-shim/lib/browser-async/sockets.js @@ -0,0 +1,186 @@ +export const instanceNetwork = { + instanceNetwork () { + console.log(`[sockets] instance network`); + } +}; + +export const ipNameLookup = { + dropResolveAddressStream () { + + }, + subscribe () { + + }, + resolveAddresses () { + + }, + resolveNextAddress () { + + }, + nonBlocking () { + + }, + setNonBlocking () { + + }, +}; + +export const network = { + dropNetwork () { + + } +}; + +export const tcpCreateSocket = { + createTcpSocket () { + + } +}; + +export const tcp = { + subscribe () { + + }, + dropTcpSocket() { + + }, + bind() { + + }, + connect() { + + }, + listen() { + + }, + accept() { + + }, + localAddress() { + + }, + remoteAddress() { + + }, + addressFamily() { + + }, + setListenBacklogSize() { + + }, + keepAlive() { + + }, + setKeepAlive() { + + }, + noDelay() { + + }, + setNoDelay() { + + }, + unicastHopLimit() { + + }, + setUnicastHopLimit() { + + }, + receiveBufferSize() { + + }, + setReceiveBufferSize() { + + }, + sendBufferSize() { + + }, + setSendBufferSize() { + + }, + nonBlocking() { + + }, + setNonBlocking() { + + }, + shutdown() { + + } +}; + +export const udp = { + subscribe () { + + }, + + dropUdpSocket () { + + }, + + bind () { + + }, + + connect () { + + }, + + receive () { + + }, + + send () { + + }, + + localAddress () { + + }, + + remoteAddress () { + + }, + + addressFamily () { + + }, + + unicastHopLimit () { + + }, + + setUnicastHopLimit () { + + }, + + receiveBufferSize () { + + }, + + setReceiveBufferSize () { + + }, + + sendBufferSize () { + + }, + + setSendBufferSize () { + + }, + + nonBlocking () { + + }, + + setNonBlocking () { + + } +}; + +export const udpCreateSocket = { + createUdpSocket () { + + } +}; diff --git a/packages/preview2-shim/lib/browser/cli.js b/packages/preview2-shim/lib/browser/cli.js index a2654118d..9238fa0e9 100644 --- a/packages/preview2-shim/lib/browser/cli.js +++ b/packages/preview2-shim/lib/browser/cli.js @@ -75,7 +75,7 @@ const stdinStream = new InputStream({ // TODO } }); -let textDecoder = new TextDecoder(); +const textDecoder = new TextDecoder(); const stdoutStream = new OutputStream({ write (contents) { if (contents[contents.length - 1] == 10) { diff --git a/packages/preview2-shim/lib/browser/random.js b/packages/preview2-shim/lib/browser/random.js index 0c473858e..cd99fb4ab 100644 --- a/packages/preview2-shim/lib/browser/random.js +++ b/packages/preview2-shim/lib/browser/random.js @@ -30,7 +30,7 @@ export const random = { if (len > MAX_BYTES) { // this is the max bytes crypto.getRandomValues // can do at once see https://developer.mozilla.org/en-US/docs/Web/API/window.crypto.getRandomValues - for (var generated = 0; generated < len; generated += MAX_BYTES) { + for (let generated = 0; generated < len; generated += MAX_BYTES) { // buffer.slice automatically checks if the end is past the end of // the buffer so we don't have to here crypto.getRandomValues(bytes.subarray(generated, generated + MAX_BYTES)); diff --git a/src/cmd/transpile.js b/src/cmd/transpile.js index d4b6acac0..7e9d543e1 100644 --- a/src/cmd/transpile.js +++ b/src/cmd/transpile.js @@ -193,7 +193,7 @@ async function wasm2Js (source) { */ export async function transpileComponent (component, opts = {}) { await $init; - if (opts.instantiation) opts.wasiShim = false; + if (opts.instantiation && !opts.esmImports) opts.wasiShim = false; // TODO double check let spinner; const showSpinner = getShowSpinner(); @@ -203,14 +203,15 @@ export async function transpileComponent (component, opts = {}) { } if (opts.wasiShim !== false) { + const maybeAsync = !opts.asyncMode || opts.asyncMode === 'sync' ? '' : 'async/'; opts.map = Object.assign({ - 'wasi:cli/*': '@bytecodealliance/preview2-shim/cli#*', - 'wasi:clocks/*': '@bytecodealliance/preview2-shim/clocks#*', - 'wasi:filesystem/*': '@bytecodealliance/preview2-shim/filesystem#*', - 'wasi:http/*': '@bytecodealliance/preview2-shim/http#*', - 'wasi:io/*': '@bytecodealliance/preview2-shim/io#*', - 'wasi:random/*': '@bytecodealliance/preview2-shim/random#*', - 'wasi:sockets/*': '@bytecodealliance/preview2-shim/sockets#*', + 'wasi:cli/*': `@bytecodealliance/preview2-shim/${maybeAsync}cli#*`, + 'wasi:clocks/*': `@bytecodealliance/preview2-shim/${maybeAsync}clocks#*`, + 'wasi:filesystem/*': `@bytecodealliance/preview2-shim/${maybeAsync}filesystem#*`, + 'wasi:http/*': `@bytecodealliance/preview2-shim/${maybeAsync}http#*`, + 'wasi:io/*': `@bytecodealliance/preview2-shim/${maybeAsync}io#*`, + 'wasi:random/*': `@bytecodealliance/preview2-shim/${maybeAsync}random#*`, + 'wasi:sockets/*': `@bytecodealliance/preview2-shim/${maybeAsync}sockets#*`, }, opts.map || {}); } From 5314a5e05bf3cf2f12c19e26e4022137e932e4ca Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Mon, 4 Nov 2024 20:51:42 -0600 Subject: [PATCH 32/35] typo fix in refactor --- packages/preview2-shim/lib/browser-async/http/types.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/preview2-shim/lib/browser-async/http/types.js b/packages/preview2-shim/lib/browser-async/http/types.js index 835a415e6..c2c32e8ce 100644 --- a/packages/preview2-shim/lib/browser-async/http/types.js +++ b/packages/preview2-shim/lib/browser-async/http/types.js @@ -676,7 +676,7 @@ export class FutureIncomingResponse { } subscribe() { - return new PollablePromise(this.#promise); + return new Pollable(this.#promise); } get() { if (!this.#ready) return; From b2ea7b710364a5e3447b34854f6b225b87649d91 Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Tue, 5 Nov 2024 13:51:54 -0600 Subject: [PATCH 33/35] added ability to set stdin --- packages/preview2-shim/lib/browser-async/cli.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/preview2-shim/lib/browser-async/cli.js b/packages/preview2-shim/lib/browser-async/cli.js index 0d21db821..cf7c47fd8 100644 --- a/packages/preview2-shim/lib/browser-async/cli.js +++ b/packages/preview2-shim/lib/browser-async/cli.js @@ -3,6 +3,7 @@ import { _setCwd as fsSetCwd } from './filesystem.js'; const textDecoder = new TextDecoder(); +let stdinStream, stdoutStream, stderrStream; let _env = [], _args = [], _cwd = "/"; export function _setEnv (envObj) { _env = Object.entries(envObj); @@ -10,10 +11,13 @@ export function _setEnv (envObj) { export function _setArgs (args) { _args = args; } - export function _setCwd (cwd) { fsSetCwd(_cwd = cwd); } +export function _setStdin (stream) { + stdinStream = stream; +} + export const environment = { getEnvironment () { @@ -44,7 +48,6 @@ export const exit = { } }; -let stdinStream; export const stdin = { InputStream, getStdin () { @@ -55,7 +58,6 @@ export const stdin = { } }; -let stdoutStream; export const stdout = { OutputStream, getStdout () { @@ -76,7 +78,6 @@ export const stdout = { } }; -let stderrStream; export const stderr = { OutputStream, getStderr () { From b9a8f25d39b5ba34f6819b4d6d5ff83fccb01ca7 Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Fri, 8 Nov 2024 16:55:37 -0600 Subject: [PATCH 34/35] added strip-debug --- src/cmd/opt.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cmd/opt.js b/src/cmd/opt.js index 7c75e783e..8b16b1383 100644 --- a/src/cmd/opt.js +++ b/src/cmd/opt.js @@ -67,7 +67,7 @@ export async function optimizeComponent (componentBytes, opts) { spinner.text = spinnerText(); } - const args = opts?.optArgs ? [...opts.optArgs] : ['-Oz', '--low-memory-unused', '--enable-bulk-memory']; + const args = opts?.optArgs ? [...opts.optArgs] : ['-Oz', '--low-memory-unused', '--enable-bulk-memory', '--strip-debug']; if (opts?.asyncMode === 'asyncify') args.push('--asyncify'); const optimizedCoreModules = await Promise.all(coreModules.map(async ([coreModuleStart, coreModuleEnd]) => { From 93df040df12ebc2c4264202071e28c2fbd781caa Mon Sep 17 00:00:00 2001 From: Calvin Prewitt Date: Mon, 11 Nov 2024 10:14:15 -0600 Subject: [PATCH 35/35] fixes --- .../browser-async/http/incoming-handler.js | 2 +- .../lib/browser-async/http/types.js | 104 ++++++++++-------- 2 files changed, 62 insertions(+), 44 deletions(-) diff --git a/packages/preview2-shim/lib/browser-async/http/incoming-handler.js b/packages/preview2-shim/lib/browser-async/http/incoming-handler.js index a1d905218..078a02b0c 100644 --- a/packages/preview2-shim/lib/browser-async/http/incoming-handler.js +++ b/packages/preview2-shim/lib/browser-async/http/incoming-handler.js @@ -10,7 +10,7 @@ export const getHandler = (handle) => async (req) => { const responseOut = new ResponseOutparam(); await handle(IncomingRequest.fromRequest(req), responseOut); const result = await responseOut.promise; - if (result.tag === 'ok') { + if (result.tag === "ok") { return result.val.toResponse(); } else { throw result; // error diff --git a/packages/preview2-shim/lib/browser-async/http/types.js b/packages/preview2-shim/lib/browser-async/http/types.js index c2c32e8ce..8ef6730e8 100644 --- a/packages/preview2-shim/lib/browser-async/http/types.js +++ b/packages/preview2-shim/lib/browser-async/http/types.js @@ -267,7 +267,12 @@ export class Fields { */ get(name) { const enc = new TextEncoder(); - return this.headers.get(name)?.split(', ').map((val) => enc.encode(val)) || []; + return ( + this.headers + .get(name) + ?.split(", ") + .map((val) => enc.encode(val)) || [] + ); } /** * @param {FieldKey} name @@ -282,17 +287,17 @@ export class Fields { */ set(name, value) { if (this.immutable) { - throw { tag: 'immutable' }; + throw { tag: "immutable" }; } const dec = new TextDecoder(); - this.headers.set(name, value.map((val) => dec.decode(val)).join(', ')); + this.headers.set(name, value.map((val) => dec.decode(val)).join(", ")); } /** * @param {FieldKey} name */ - 'delete'(name) { + delete(name) { if (this.immutable) { - throw { tag: 'immutable' }; + throw { tag: "immutable" }; } this.headers.delete(name); } @@ -302,7 +307,7 @@ export class Fields { */ append(name, value) { if (this.immutable) { - throw { tag: 'immutable' }; + throw { tag: "immutable" }; } const dec = new TextDecoder(); this.headers.append(name, dec.decode(value)); @@ -333,7 +338,6 @@ export class Fields { //export type Headers = Fields; //export type Trailers = Fields; - export class IncomingRequest { #method; #pathWithQuery; @@ -398,8 +402,7 @@ export class IncomingRequest { } throw undefined; } - [symbolDispose]() { - } + [symbolDispose]() {} /** * @param {Request} req @@ -414,7 +417,14 @@ export class IncomingRequest { const headers = new Fields(req.headers, true); const body = new IncomingBody(new InputStream(req.body)); - return new IncomingRequest(method, pathWithQuery, scheme, authority, headers, body); + return new IncomingRequest( + method, + pathWithQuery, + scheme, + authority, + headers, + body, + ); } } @@ -432,7 +442,7 @@ export class OutgoingRequest { constructor(headers) { headers.immutable = true; this.#headers = headers; - this.#body = new OutgoingBody; + this.#body = new OutgoingBody(); } /** * @returns {OutgoingBody} @@ -444,7 +454,7 @@ export class OutgoingRequest { * @returns {Method} */ method() { - return this.#method || { tag: 'get' }; + return this.#method || { tag: "get" }; } /** * @param {Method} method @@ -481,20 +491,32 @@ export class OutgoingRequest { } toRequest() { - if (this.#scheme && this.#scheme.tag === 'other' || !this.#authority) { - throw { tag: 'destination-not-found' }; + if ((this.#scheme && this.#scheme.tag === "other") || !this.#authority) { + throw { tag: "destination-not-found" }; } - const path = this.#pathWithQuery ? - (this.#pathWithQuery.startsWith('/') ? this.#pathWithQuery : `/${this.#pathWithQuery}`) - : ''; - - const method = (this.#method ? this.#method.tag : 'get'); - const body = method === 'get' || method === 'head' ? undefined : this.#body.stream.readable; - return new Request(`${this.#scheme ? this.#scheme.tag : 'HTTPS'}://${this.#authority}${path}`, { - method, - headers: this.#headers.headers, - body, - }); + const path = this.#pathWithQuery + ? this.#pathWithQuery.startsWith("/") + ? this.#pathWithQuery + : `/${this.#pathWithQuery}` + : ""; + + const method = this.#method ? this.#method.tag : "get"; + const body = + method === "get" || method === "head" + ? undefined + : this.#body.stream.readable; + // see: https://fetch.spec.whatwg.org/#ref-for-dom-requestinit-duplex + // see: https://developer.chrome.com/docs/capabilities/web-apis/fetch-streaming-requests#half_duplex + const duplex = body ? "half" : undefined; + return new Request( + `${this.#scheme ? this.#scheme.tag : "HTTPS"}://${this.#authority}${path}`, + { + method, + headers: this.#headers.headers, + body, + duplex, + }, + ); } } @@ -572,8 +594,7 @@ export class IncomingBody { static finish(_body) { return new FutureTrailers(); } - [symbolDispose]() { - } + [symbolDispose]() {} } export class FutureTrailers { @@ -589,9 +610,9 @@ export class FutureTrailers { } get() { if (this.#errCode) { - return { tag: 'ok', val: { tag: 'err', val: this.#errCode } }; + return { tag: "ok", val: { tag: "err", val: this.#errCode } }; } - return { tag: 'ok', val: { tag: 'ok', val: this.#trailers } }; + return { tag: "ok", val: { tag: "ok", val: this.#trailers } }; } } @@ -617,17 +638,13 @@ export class OutgoingResponse { body() { return this.#body; } - [symbolDispose]() { - } + [symbolDispose]() {} toResponse() { - return new Response( - this.#body.stream.readable, - { - status: this.#statusCode, - headers: this.#headers.headers, - }, - ); + return new Response(this.#body.stream.readable, { + status: this.#statusCode, + headers: this.#headers.headers, + }); } } @@ -645,7 +662,7 @@ export class OutgoingBody { static finish(body, trailers) { // trailers not supported if (trailers) { - throw { tag: 'HTTP-request-trailer-section-size' }; + throw { tag: "HTTP-request-trailer-section-size" }; } body.stream.close(); body.finished = true; @@ -668,10 +685,11 @@ export class FutureIncomingResponse { this.#resolvedResponse = response; }); } catch (err) { + console.error(err); this.#promise = Promise.resolve(); this.#ready = true; // TODO better error handling - this.#error = { tag: 'internal-error', val: err.toString() }; + this.#error = { tag: "internal-error", val: err.toString() }; } } @@ -680,14 +698,14 @@ export class FutureIncomingResponse { } get() { if (!this.#ready) return; - if (this.#error) return { tag: 'err', val: this.#error }; + if (this.#error) return { tag: "err", val: this.#error }; const res = this.#resolvedResponse; return { - tag: 'ok', + tag: "ok", val: { - tag: 'ok', + tag: "ok", val: new IncomingResponse( res.status, new Fields(res.headers, true),