From dc7f46cf9bec201a04b8e559bc40c8a66252daff Mon Sep 17 00:00:00 2001 From: Daniel Vigovszky Date: Wed, 1 Apr 2026 18:50:48 +0200 Subject: [PATCH 1/4] Targeting wasm32-wasip2 --- AGENTS.md | 1 - README.md | 2 +- crates/wasm-rquickjs/skeleton/Cargo.lock | 380 +++++++++++++--------- crates/wasm-rquickjs/skeleton/Cargo.toml_ | 3 +- crates/wasm-rquickjs/skeleton/golem.yaml | 42 --- crates/wasm-rquickjs/src/conversions.rs | 368 ++++++++++++++++----- crates/wasm-rquickjs/src/exports.rs | 98 +++++- crates/wasm-rquickjs/src/imports.rs | 119 ++++--- crates/wasm-rquickjs/src/lib.rs | 51 ++- crates/wasm-rquickjs/src/skeleton.rs | 105 +----- crates/wasm-rquickjs/src/types.rs | 93 +++++- tests/binary_inject.rs | 8 +- tests/common/mod.rs | 10 +- tests/compilation.rs | 4 +- tests/node_compat/generate-report.sh | 2 +- tests/node_compat_report.rs | 6 +- tests/wizer_benchmark.rs | 10 +- 17 files changed, 833 insertions(+), 469 deletions(-) delete mode 100644 crates/wasm-rquickjs/skeleton/golem.yaml diff --git a/AGENTS.md b/AGENTS.md index fbe0c0ae..c08e44bc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -200,4 +200,3 @@ Key external dependencies: - `rquickjs` - QuickJS Rust bindings - `wit-parser` / `wit-encoder` - WebAssembly Interface Type support - `wasmtime` - WASM runtime for testing -- `cargo-component` - WASM component compilation (for end users) diff --git a/README.md b/README.md index 2a33fcf0..56269266 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ Usage: wasm-rquickjs generate-wrapper-crate --js --wit --output , + type_id: TypeId, +) -> anyhow::Result { + let typ = context.typ(type_id)?; + let name = typ + .name + .as_ref() + .ok_or_else(|| anyhow!("WASI type has no name"))?; + + // Build a unique wrapper name including the interface name to avoid conflicts + let interface_prefix = match &typ.owner { + TypeOwner::Interface(iface_id) => { + let iface = context + .resolve + .interfaces + .get(*iface_id) + .ok_or_else(|| anyhow!("Unknown interface id"))?; + if let Some(iface_name) = &iface.name { + iface_name.to_upper_camel_case() + } else { + String::new() + } + } + _ => String::new(), + }; + + let wrapper_name = format!("Js{}{}", interface_prefix, name.to_upper_camel_case()); + Ok(Ident::new(&wrapper_name, Span::call_site())) +} /// Generates the `/src/conversions.rs` file for the wrapper crate, implementing the IntoJs /// and FromJs typeclass instances for the types generated in the Rust bindings.. @@ -58,8 +94,56 @@ fn generate_conversion_instances_for_type( return Ok(None); } + let is_wasi_remapped = context.is_wasi_remapped_type(type_id); + let typ = context.typ(type_id)?; + // For WASI-remapped resource types, generate a newtype wrapper with IntoJs/FromJs + // since we can't implement those traits directly on the foreign wasip2 type. + if is_wasi_remapped && matches!(typ.kind, TypeDefKind::Resource) { + let type_path = type_id_to_type_ref(context, type_id)?; + let wrapper_name = wasi_wrapper_name(context, type_id)?; + let name = typ.name.as_ref().map(|n| n.as_str()).unwrap_or("Resource"); + let name_lit = LitStr::new(name, Span::call_site()); + return Ok(Some(quote! { + pub struct #wrapper_name(pub #type_path); + + impl<'js> rquickjs::IntoJs<'js> for #wrapper_name { + fn into_js(self, ctx: &rquickjs::Ctx<'js>) -> rquickjs::Result> { + rquickjs::class::Class::instance(ctx.clone(), self) + .and_then(|cls| rquickjs::IntoJs::into_js(cls, ctx)) + } + } + + impl<'js> rquickjs::FromJs<'js> for #wrapper_name { + fn from_js(ctx: &rquickjs::Ctx<'js>, value: rquickjs::Value<'js>) -> rquickjs::Result { + let cls = rquickjs::class::Class::::from_js(ctx, value)?; + let borrow = cls.try_borrow()?; + Ok(Self(unsafe { #type_path::from_handle(borrow.0.handle()) })) + } + } + + impl<'js> rquickjs::class::JsClass<'js> for #wrapper_name { + const NAME: &'static str = #name_lit; + type Mutable = rquickjs::class::Writable; + fn prototype(_ctx: &rquickjs::Ctx<'js>) -> rquickjs::Result>> { + Ok(None) + } + fn constructor(_ctx: &rquickjs::Ctx<'js>) -> rquickjs::Result>> { + Ok(None) + } + } + + impl<'js> rquickjs::class::Trace<'js> for #wrapper_name { + fn trace<'a>(&self, _tracer: rquickjs::class::Tracer<'a, 'js>) {} + } + + unsafe impl<'js> rquickjs::JsLifetime<'js> for #wrapper_name { + type Changed<'to> = #wrapper_name; + } + })); + } + match &typ.kind { TypeDefKind::Record(record) => { let type_path = type_id_to_type_ref(context, type_id)?; @@ -68,6 +152,13 @@ fn generate_conversion_instances_for_type( let mut get_fields = Vec::new(); let mut rust_field_list = Vec::new(); + // For WASI-remapped types, access fields through `.0.field` (newtype wrapper) + let field_accessor = if is_wasi_remapped { + quote! { self.0 } + } else { + quote! { self } + }; + for field in &record.fields { let js_field_name = escape_js_ident(field.name.to_lower_camel_case()); let rust_field_ident = Ident::new( @@ -86,7 +177,7 @@ fn generate_conversion_instances_for_type( let original_field_type = &field_type.original_type_ref; let wrapped_field_type = &field_type.wrapped_type_ref; - let wrapped_field = field_type.wrap.run(quote! { self.#rust_field_ident }); + let wrapped_field = field_type.wrap.run(quote! { #field_accessor.#rust_field_ident }); let unwrapped_field = field_type.unwrap.run(quote! { #rust_field_ident }); set_fields.push(quote! { @@ -102,26 +193,50 @@ fn generate_conversion_instances_for_type( rust_field_list.push(rust_field_ident); } - Ok(Some(quote! { - impl<'js> rquickjs::IntoJs<'js> for #type_path { - fn into_js(self, ctx: &rquickjs::Ctx<'js>) -> rquickjs::Result> { - // record - let obj = rquickjs::Object::new(ctx.clone())?; - #(#set_fields);* - Ok(obj.into_value()) + if is_wasi_remapped { + let wrapper_name = wasi_wrapper_name(context, type_id)?; + Ok(Some(quote! { + pub struct #wrapper_name(pub #type_path); + + impl<'js> rquickjs::IntoJs<'js> for #wrapper_name { + fn into_js(self, ctx: &rquickjs::Ctx<'js>) -> rquickjs::Result> { + let obj = rquickjs::Object::new(ctx.clone())?; + #(#set_fields);* + Ok(obj.into_value()) + } } - } - impl<'js> rquickjs::FromJs<'js> for #type_path { - fn from_js(_ctx: &rquickjs::Ctx<'js>, value: rquickjs::Value<'js>) -> rquickjs::Result { - let obj = rquickjs::Object::from_value(value)?; - #(#get_fields);* - Ok(Self { - #(#rust_field_list),* - }) + impl<'js> rquickjs::FromJs<'js> for #wrapper_name { + fn from_js(_ctx: &rquickjs::Ctx<'js>, value: rquickjs::Value<'js>) -> rquickjs::Result { + let obj = rquickjs::Object::from_value(value)?; + #(#get_fields);* + Ok(Self(#type_path { + #(#rust_field_list),* + })) + } } - } - })) + })) + } else { + Ok(Some(quote! { + impl<'js> rquickjs::IntoJs<'js> for #type_path { + fn into_js(self, ctx: &rquickjs::Ctx<'js>) -> rquickjs::Result> { + let obj = rquickjs::Object::new(ctx.clone())?; + #(#set_fields);* + Ok(obj.into_value()) + } + } + + impl<'js> rquickjs::FromJs<'js> for #type_path { + fn from_js(_ctx: &rquickjs::Ctx<'js>, value: rquickjs::Value<'js>) -> rquickjs::Result { + let obj = rquickjs::Object::from_value(value)?; + #(#get_fields);* + Ok(Self { + #(#rust_field_list),* + }) + } + } + })) + } } TypeDefKind::Flags(flags) => { let type_path = type_id_to_type_ref(context, type_id)?; @@ -129,6 +244,9 @@ fn generate_conversion_instances_for_type( let mut set_fields = Vec::new(); let mut get_fields = Vec::new(); + // For WASI-remapped types, access through `.0` (newtype wrapper) + let self_ref = if is_wasi_remapped { quote! { self.0 } } else { quote! { self } }; + for flag in &flags.flags { let js_field_name = escape_js_ident(flag.name.to_lower_camel_case()); let rust_field_ident = @@ -136,7 +254,7 @@ fn generate_conversion_instances_for_type( let field_name_lit = Lit::Str(LitStr::new(&js_field_name, Span::call_site())); set_fields.push(quote! { - obj.set(#field_name_lit, self & #type_path::#rust_field_ident == #type_path::#rust_field_ident)?; + obj.set(#field_name_lit, #self_ref & #type_path::#rust_field_ident == #type_path::#rust_field_ident)?; }); get_fields.push(quote! { @@ -146,24 +264,48 @@ fn generate_conversion_instances_for_type( }); } - Ok(Some(quote! { - impl<'js> rquickjs::IntoJs<'js> for #type_path { - fn into_js(self, ctx: &rquickjs::Ctx<'js>) -> rquickjs::Result> { - let obj = rquickjs::Object::new(ctx.clone())?; - #(#set_fields);* - Ok(obj.into_value()) + if is_wasi_remapped { + let wrapper_name = wasi_wrapper_name(context, type_id)?; + Ok(Some(quote! { + pub struct #wrapper_name(pub #type_path); + + impl<'js> rquickjs::IntoJs<'js> for #wrapper_name { + fn into_js(self, ctx: &rquickjs::Ctx<'js>) -> rquickjs::Result> { + let obj = rquickjs::Object::new(ctx.clone())?; + #(#set_fields);* + Ok(obj.into_value()) + } + } + + impl<'js> rquickjs::FromJs<'js> for #wrapper_name { + fn from_js(_ctx: &rquickjs::Ctx<'js>, value: rquickjs::Value<'js>) -> rquickjs::Result { + let obj = rquickjs::Object::from_value(value)?; + let mut result = #type_path::empty(); + #(#get_fields);* + Ok(Self(result)) + } + } + })) + } else { + Ok(Some(quote! { + impl<'js> rquickjs::IntoJs<'js> for #type_path { + fn into_js(self, ctx: &rquickjs::Ctx<'js>) -> rquickjs::Result> { + let obj = rquickjs::Object::new(ctx.clone())?; + #(#set_fields);* + Ok(obj.into_value()) + } } - } - impl<'js> rquickjs::FromJs<'js> for #type_path { - fn from_js(_ctx: &rquickjs::Ctx<'js>, value: rquickjs::Value<'js>) -> rquickjs::Result { - let obj = rquickjs::Object::from_value(value)?; - let mut result = #type_path::empty(); - #(#get_fields);* - Ok(result) + impl<'js> rquickjs::FromJs<'js> for #type_path { + fn from_js(_ctx: &rquickjs::Ctx<'js>, value: rquickjs::Value<'js>) -> rquickjs::Result { + let obj = rquickjs::Object::from_value(value)?; + let mut result = #type_path::empty(); + #(#get_fields);* + Ok(result) + } } - } - })) + })) + } } TypeDefKind::Variant(variant) => { let type_path = type_id_to_type_ref(context, type_id)?; @@ -222,32 +364,64 @@ fn generate_conversion_instances_for_type( Span::call_site(), )); - Ok(Some(quote! { - impl<'js> rquickjs::IntoJs<'js> for #type_path { - fn into_js(self, ctx: &rquickjs::Ctx<'js>) -> rquickjs::Result> { - let obj = rquickjs::Object::new(ctx.clone())?; - match self { - #(#into_cases)* + if is_wasi_remapped { + let wrapper_name = wasi_wrapper_name(context, type_id)?; + Ok(Some(quote! { + pub struct #wrapper_name(pub #type_path); + + impl<'js> rquickjs::IntoJs<'js> for #wrapper_name { + fn into_js(self, ctx: &rquickjs::Ctx<'js>) -> rquickjs::Result> { + let obj = rquickjs::Object::new(ctx.clone())?; + match self.0 { + #(#into_cases)* + } + Ok(obj.into_value()) } - Ok(obj.into_value()) } - } - impl<'js> rquickjs::FromJs<'js> for #type_path { - fn from_js(_ctx: &rquickjs::Ctx<'js>, value: rquickjs::Value<'js>) -> rquickjs::Result { - let obj = rquickjs::Object::from_value(value)?; - let tag: String = obj.get(crate::wrappers::TAG)?; - match tag.as_str() { - #(#from_cases)* - _ => Err(rquickjs::Error::new_from_js_message( - #lit_js_type, - #lit_wit_type, - format!("Unknown variant case: {tag}"), - )), + impl<'js> rquickjs::FromJs<'js> for #wrapper_name { + fn from_js(_ctx: &rquickjs::Ctx<'js>, value: rquickjs::Value<'js>) -> rquickjs::Result { + let obj = rquickjs::Object::from_value(value)?; + let tag: String = obj.get(crate::wrappers::TAG)?; + match tag.as_str() { + #(#from_cases)* + _ => Err(rquickjs::Error::new_from_js_message( + #lit_js_type, + #lit_wit_type, + format!("Unknown variant case: {tag}"), + )), + }.map(Self) } } - } - })) + })) + } else { + Ok(Some(quote! { + impl<'js> rquickjs::IntoJs<'js> for #type_path { + fn into_js(self, ctx: &rquickjs::Ctx<'js>) -> rquickjs::Result> { + let obj = rquickjs::Object::new(ctx.clone())?; + match self { + #(#into_cases)* + } + Ok(obj.into_value()) + } + } + + impl<'js> rquickjs::FromJs<'js> for #type_path { + fn from_js(_ctx: &rquickjs::Ctx<'js>, value: rquickjs::Value<'js>) -> rquickjs::Result { + let obj = rquickjs::Object::from_value(value)?; + let tag: String = obj.get(crate::wrappers::TAG)?; + match tag.as_str() { + #(#from_cases)* + _ => Err(rquickjs::Error::new_from_js_message( + #lit_js_type, + #lit_wit_type, + format!("Unknown variant case: {tag}"), + )), + } + } + } + })) + } } TypeDefKind::Enum(enm) => { let type_path = type_id_to_type_ref(context, type_id)?; @@ -271,34 +445,68 @@ fn generate_conversion_instances_for_type( let lit_js_type = Lit::Str(LitStr::new(&format!("JS {name}"), Span::call_site())); let lit_wit_type = Lit::Str(LitStr::new(&format!("WIT {name}"), Span::call_site())); - Ok(Some(quote! { - impl<'js> rquickjs::IntoJs<'js> for #type_path { - fn into_js(self, ctx: &rquickjs::Ctx<'js>) -> rquickjs::Result> { - match self { - #(#into_cases)* + if is_wasi_remapped { + let wrapper_name = wasi_wrapper_name(context, type_id)?; + Ok(Some(quote! { + pub struct #wrapper_name(pub #type_path); + + impl<'js> rquickjs::IntoJs<'js> for #wrapper_name { + fn into_js(self, ctx: &rquickjs::Ctx<'js>) -> rquickjs::Result> { + match self.0 { + #(#into_cases)* + } } } - } - impl<'js> rquickjs::FromJs<'js> for #type_path { - fn from_js(_ctx: &rquickjs::Ctx<'js>, value: rquickjs::Value<'js>) -> rquickjs::Result { - let value = value - .as_string() - .ok_or_else(|| { - rquickjs::Error::new_from_js_message(#lit_js_type, #lit_wit_type, "Expected a string") - })? - .to_string()?; - match value.as_str() { - #(#from_cases)* - _ => Err(rquickjs::Error::new_from_js_message( - #lit_js_type, - #lit_wit_type, - format!("Unknown case value: {value}"), - )), + impl<'js> rquickjs::FromJs<'js> for #wrapper_name { + fn from_js(_ctx: &rquickjs::Ctx<'js>, value: rquickjs::Value<'js>) -> rquickjs::Result { + let value = value + .as_string() + .ok_or_else(|| { + rquickjs::Error::new_from_js_message(#lit_js_type, #lit_wit_type, "Expected a string") + })? + .to_string()?; + match value.as_str() { + #(#from_cases)* + _ => Err(rquickjs::Error::new_from_js_message( + #lit_js_type, + #lit_wit_type, + format!("Unknown case value: {value}"), + )), + }.map(Self) } } - } - })) + })) + } else { + Ok(Some(quote! { + impl<'js> rquickjs::IntoJs<'js> for #type_path { + fn into_js(self, ctx: &rquickjs::Ctx<'js>) -> rquickjs::Result> { + match self { + #(#into_cases)* + } + } + } + + impl<'js> rquickjs::FromJs<'js> for #type_path { + fn from_js(_ctx: &rquickjs::Ctx<'js>, value: rquickjs::Value<'js>) -> rquickjs::Result { + let value = value + .as_string() + .ok_or_else(|| { + rquickjs::Error::new_from_js_message(#lit_js_type, #lit_wit_type, "Expected a string") + })? + .to_string()?; + match value.as_str() { + #(#from_cases)* + _ => Err(rquickjs::Error::new_from_js_message( + #lit_js_type, + #lit_wit_type, + format!("Unknown case value: {value}"), + )), + } + } + } + })) + } } TypeDefKind::Type(Type::Id(type_id)) => { generate_conversion_instances_for_type(context, *type_id, visited_types) diff --git a/crates/wasm-rquickjs/src/exports.rs b/crates/wasm-rquickjs/src/exports.rs index 2f868049..81702e2f 100644 --- a/crates/wasm-rquickjs/src/exports.rs +++ b/crates/wasm-rquickjs/src/exports.rs @@ -23,10 +23,20 @@ pub fn generate_export_impls( let guest_impls = generate_guest_impls(context)?; let module_defs = generate_module_defs(js_modules)?; + let world_name_lit = LitStr::new(&context.world_name, Span::call_site()); + let with_block = generate_wasi_remaps(context); + let lib_tokens = quote! { - #[allow(static_mut_refs)] #[allow(unsafe_op_in_unsafe_fn)] - mod bindings; + pub(crate) mod bindings { + wit_bindgen::generate!({ + path: "wit", + world: #world_name_lit, + ownership: Owning, + generate_all, + #with_block + }); + } mod builtin; mod conversions; #[allow(unused)] @@ -747,3 +757,87 @@ fn generate_module_defs(js_modules: &[JsModuleSpec]) -> anyhow::Result) -> TokenStream { + // Static mapping from unversioned WIT interface names to wasip2 Rust module paths. + // The actual `with:` entries use the fully-versioned names from the resolved world. + static WASI_REMAPS: &[(&str, &str)] = &[ + ("wasi:cli/environment", "wasip2::cli::environment"), + ("wasi:cli/exit", "wasip2::cli::exit"), + ("wasi:cli/stderr", "wasip2::cli::stderr"), + ("wasi:cli/stdin", "wasip2::cli::stdin"), + ("wasi:cli/stdout", "wasip2::cli::stdout"), + ("wasi:cli/terminal-input", "wasip2::cli::terminal_input"), + ("wasi:cli/terminal-output", "wasip2::cli::terminal_output"), + ("wasi:cli/terminal-stderr", "wasip2::cli::terminal_stderr"), + ("wasi:cli/terminal-stdin", "wasip2::cli::terminal_stdin"), + ("wasi:cli/terminal-stdout", "wasip2::cli::terminal_stdout"), + ("wasi:clocks/monotonic-clock", "wasip2::clocks::monotonic_clock"), + ("wasi:clocks/wall-clock", "wasip2::clocks::wall_clock"), + ("wasi:filesystem/preopens", "wasip2::filesystem::preopens"), + ("wasi:filesystem/types", "wasip2::filesystem::types"), + ("wasi:http/outgoing-handler", "wasip2::http::outgoing_handler"), + ("wasi:http/types", "wasip2::http::types"), + ("wasi:io/error", "wasip2::io::error"), + ("wasi:io/poll", "wasip2::io::poll"), + ("wasi:io/streams", "wasip2::io::streams"), + ("wasi:random/insecure", "wasip2::random::insecure"), + ("wasi:random/insecure-seed", "wasip2::random::insecure_seed"), + ("wasi:random/random", "wasip2::random::random"), + ("wasi:sockets/instance-network", "wasip2::sockets::instance_network"), + ("wasi:sockets/ip-name-lookup", "wasip2::sockets::ip_name_lookup"), + ("wasi:sockets/network", "wasip2::sockets::network"), + ("wasi:sockets/tcp", "wasip2::sockets::tcp"), + ("wasi:sockets/tcp-create-socket", "wasip2::sockets::tcp_create_socket"), + ("wasi:sockets/udp", "wasip2::sockets::udp"), + ("wasi:sockets/udp-create-socket", "wasip2::sockets::udp_create_socket"), + ]; + + // Collect all interfaces used in the world, with their fully-qualified versioned names + let world = &context.resolve.worlds[context.world]; + let mut used_interfaces = std::collections::BTreeMap::::new(); + + for (key, _) in world.imports.iter().chain(world.exports.iter()) { + if let WorldKey::Interface(id) = key { + let interface = &context.resolve.interfaces[*id]; + if let Some(ref name) = interface.name { + if let Some(package_id) = interface.package { + let package = &context.resolve.packages[package_id]; + let unversioned = format!( + "{}:{}/{}", + package.name.namespace, package.name.name, name + ); + let versioned = package.name.interface_id(name); + used_interfaces.insert(unversioned, versioned); + } + } + } + } + + // Build with: entries only for WASI interfaces that are actually used, + // using the fully-versioned WIT name as the key + let mut entries = Vec::new(); + for (wit_name, rust_path) in WASI_REMAPS { + if let Some(versioned_name) = used_interfaces.get(*wit_name) { + let wit_lit = LitStr::new(versioned_name, Span::call_site()); + let rust_path: syn::Path = syn::parse_str(rust_path) + .unwrap_or_else(|_| panic!("Invalid Rust path: {rust_path}")); + entries.push(quote! { #wit_lit: #rust_path }); + } + } + + if entries.is_empty() { + quote! {} + } else { + quote! { + with: { + #(#entries),* + }, + } + } +} diff --git a/crates/wasm-rquickjs/src/imports.rs b/crates/wasm-rquickjs/src/imports.rs index 0da67386..624ac22b 100644 --- a/crates/wasm-rquickjs/src/imports.rs +++ b/crates/wasm-rquickjs/src/imports.rs @@ -68,7 +68,15 @@ pub fn collect_imported_interfaces<'a>( }; match import { WorldItem::Interface { id, .. } => { - interfaces.push(context.get_imported_interface(id)?); + // Skip WASI-remapped interfaces — their types come from wasip2:: + // and we cannot implement IntoJs/FromJs for foreign types. + let interface = &context.resolve.interfaces[*id]; + let is_wasi_remapped = interface + .package + .is_some_and(|pkg_id| context.is_wasi_remapped_package(pkg_id)); + if !is_wasi_remapped { + interfaces.push(context.get_imported_interface(id)?); + } } WorldItem::Function(function) => { global_imports.push((name, function)); @@ -528,6 +536,65 @@ fn generate_import_module( let rquickjs_class = generate_rquickjs_class_module(resource_name, &resource_name_ident, &resource_name_lit); + // For WASI-remapped resources, skip the IntoJs/FromJs impls on the bindgen type + // (they're foreign types from wasip2::) and the BorrowWrapper, since those would + // violate the orphan rule. + let foreign_type_impls = if context.is_wasi_remapped_type(resource_type_id) { + quote! {} + } else { + quote! { + impl<'js> rquickjs::IntoJs<'js> for #bindgen_path { + fn into_js(self, ctx: &rquickjs::Ctx<'js>) -> rquickjs::Result> { + #resource_name_ident { + inner: Some(std::rc::Rc::new(self)), + } + .into_js(ctx) + } + } + + impl<'js> rquickjs::FromJs<'js> for #bindgen_path { + fn from_js(ctx: &rquickjs::Ctx<'js>, value: rquickjs::Value<'js>) -> rquickjs::Result { + let wrapper = #resource_name_ident::from_js(ctx, value)?; + unsafe { + Ok( + #bindgen_path::from_handle( + wrapper + .inner + .ok_or_else(|| rquickjs::Error::FromJs { from: "JavaScript object", to: #resource_name_lit, message: Some("Resource has already been disposed".to_string()) })? + .take_handle(), + ), + ) + } + } + } + + pub struct #borrow_wrapper_ident(pub #bindgen_path); + + impl<'js> rquickjs::FromJs<'js> for #borrow_wrapper_ident { + fn from_js(ctx: &rquickjs::Ctx<'js>, value: rquickjs::Value<'js>) -> rquickjs::Result { + let wrapper = #resource_name_ident::from_js(ctx, value)?; + unsafe { + Ok(#borrow_wrapper_ident( + #bindgen_path::from_handle( + wrapper + .inner + .ok_or_else(|| rquickjs::Error::FromJs { from: "JavaScript object", to: #resource_name_lit, message: Some("Resource has already been disposed".to_string()) })? + .handle(), + ), + )) + } + } + } + + impl Drop for #borrow_wrapper_ident { + fn drop(&mut self) { + // By taking out the handle from the resource it is not going to be dropped + let _ = self.0.take_handle(); + } + } + } + }; + bridge_classes.push(quote! { #[derive(Clone, JsLifetime, Trace)] pub struct #resource_name_ident { @@ -551,55 +618,7 @@ fn generate_import_module( #(#special_methods)* } - impl<'js> rquickjs::IntoJs<'js> for #bindgen_path { - fn into_js(self, ctx: &rquickjs::Ctx<'js>) -> rquickjs::Result> { - #resource_name_ident { - inner: Some(std::rc::Rc::new(self)), - } - .into_js(ctx) - } - } - - impl<'js> rquickjs::FromJs<'js> for #bindgen_path { - fn from_js(ctx: &rquickjs::Ctx<'js>, value: rquickjs::Value<'js>) -> rquickjs::Result { - let wrapper = #resource_name_ident::from_js(ctx, value)?; - unsafe { - Ok( - #bindgen_path::from_handle( - wrapper - .inner - .ok_or_else(|| rquickjs::Error::FromJs { from: "JavaScript object", to: #resource_name_lit, message: Some("Resource has already been disposed".to_string()) })? - .take_handle(), - ), - ) - } - } - } - - pub struct #borrow_wrapper_ident(pub #bindgen_path); - - impl<'js> rquickjs::FromJs<'js> for #borrow_wrapper_ident { - fn from_js(ctx: &rquickjs::Ctx<'js>, value: rquickjs::Value<'js>) -> rquickjs::Result { - let wrapper = #resource_name_ident::from_js(ctx, value)?; - unsafe { - Ok(#borrow_wrapper_ident( - #bindgen_path::from_handle( - wrapper - .inner - .ok_or_else(|| rquickjs::Error::FromJs { from: "JavaScript object", to: #resource_name_lit, message: Some("Resource has already been disposed".to_string()) })? - .handle(), - ), - )) - } - } - } - - impl Drop for #borrow_wrapper_ident { - fn drop(&mut self) { - // By taking out the handle from the resource it is not going to be dropped - let _ = self.0.take_handle(); - } - } + #foreign_type_impls }); let js_class_lit = LitStr::new( diff --git a/crates/wasm-rquickjs/src/lib.rs b/crates/wasm-rquickjs/src/lib.rs index ca35ff20..4874af33 100644 --- a/crates/wasm-rquickjs/src/lib.rs +++ b/crates/wasm-rquickjs/src/lib.rs @@ -1,9 +1,7 @@ use crate::conversions::generate_conversions; use crate::exports::generate_export_impls; use crate::imports::generate_import_modules; -use crate::skeleton::{ - copy_skeleton_lock, copy_skeleton_sources, generate_app_manifest, generate_cargo_toml, -}; +use crate::skeleton::{copy_skeleton_lock, copy_skeleton_sources, generate_cargo_toml}; use crate::wit::{add_get_script_import, add_wizer_init_export}; use anyhow::{Context, anyhow}; use camino::{Utf8Path, Utf8PathBuf}; @@ -16,6 +14,19 @@ use wit_parser::{ TypeId, TypeOwner, WorldId, WorldItem, }; +/// WASI package namespaces whose interfaces are remapped to `wasip2::` in the generated code. +/// These correspond to interfaces provided by the `wasip2` crate and are mapped via the +/// `with:` block in `wit_bindgen::generate!`. +const WASI_REMAP_NAMESPACES: &[(&str, &str)] = &[ + ("cli", "cli"), + ("clocks", "clocks"), + ("filesystem", "filesystem"), + ("http", "http"), + ("io", "io"), + ("random", "random"), + ("sockets", "sockets"), +]; + mod conversions; mod exports; mod imports; @@ -132,9 +143,6 @@ pub fn generate_wrapper_crate( // Copying the skeleton's Cargo.lock for faster dependency resolution copy_skeleton_lock(context.output).context("Failed to copy skeleton Cargo.lock")?; - // Generating a Golem App Manifest file (for debugging) - generate_app_manifest(&context)?; - // Copying the skeleton files copy_skeleton_sources(context.output).context("Failed to copy skeleton sources")?; @@ -326,6 +334,37 @@ impl<'a> GeneratorContext<'a> { .get(type_id) .ok_or_else(|| anyhow!("Unknown type id: {type_id:?}")) } + + /// Returns `true` if the given package is a WASI package whose interfaces are remapped + /// to `wasip2::` via the `with:` block in `wit_bindgen::generate!`. + fn is_wasi_remapped_package(&self, package_id: PackageId) -> bool { + let package = &self.resolve.packages[package_id]; + if package.name.namespace != "wasi" { + return false; + } + WASI_REMAP_NAMESPACES + .iter() + .any(|(pkg_name, _)| *pkg_name == package.name.name.as_str()) + } + + /// Returns `true` if the given type belongs to a WASI-remapped interface. + fn is_wasi_remapped_type(&self, type_id: TypeId) -> bool { + if let Some(typ) = self.resolve.types.get(type_id) { + match &typ.owner { + TypeOwner::Interface(interface_id) => { + if let Some(interface) = self.resolve.interfaces.get(*interface_id) { + if let Some(package_id) = interface.package { + return self.is_wasi_remapped_package(package_id); + } + } + false + } + _ => false, + } + } else { + false + } + } } pub struct ImportedInterface<'a> { diff --git a/crates/wasm-rquickjs/src/skeleton.rs b/crates/wasm-rquickjs/src/skeleton.rs index bf4b79d2..ff42c617 100644 --- a/crates/wasm-rquickjs/src/skeleton.rs +++ b/crates/wasm-rquickjs/src/skeleton.rs @@ -1,11 +1,8 @@ use crate::GeneratorContext; use anyhow::anyhow; use camino::Utf8Path; -use heck::ToSnakeCase; use include_dir::{Dir, include_dir}; -use std::collections::BTreeSet; -use std::path::Path; -use toml_edit::{DocumentMut, Item, Table, Value, value}; +use toml_edit::{DocumentMut, value}; static SKELETON: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/skeleton"); @@ -14,8 +11,6 @@ static SKELETON: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/skeleton"); /// /// Changes applied to the skeleton toml file: /// - Changing the package name to `crate_name` (which is the name of the chosen WIT world). -/// - Adding a `[package.metadata.component.target.dependencies]` section with all the WIT -/// dependencies of the WIT package. pub fn generate_cargo_toml(context: &GeneratorContext<'_>) -> anyhow::Result<()> { // Loading the skeleton Cargo.toml file let cargo_toml = SKELETON @@ -30,7 +25,6 @@ pub fn generate_cargo_toml(context: &GeneratorContext<'_>) -> anyhow::Result<()> .map_err(|err| anyhow!("Cargo.toml skeleton is not a valid TOML: {err}"))?; change_package_name(context, &mut doc); - add_wit_dependencies(&context, &mut doc)?; // Writing the result let output_path = context.output.join("Cargo.toml"); @@ -38,109 +32,12 @@ pub fn generate_cargo_toml(context: &GeneratorContext<'_>) -> anyhow::Result<()> Ok(()) } -pub fn generate_app_manifest(context: &GeneratorContext<'_>) -> anyhow::Result<()> { - // Load the source YAML from the skeleton - let raw_yaml = SKELETON - .get_file("golem.yaml") - .ok_or_else(|| anyhow!("Missing golem.yaml skeleton"))? - .contents_utf8() - .ok_or_else(|| anyhow!("golem.yaml skeleton is not valid UTF-8"))?; - - // Replacing `component_name` with the crate's name - let raw_yaml = raw_yaml - .replace("component_name", &context.world_name.to_snake_case()) - .replace("root:package", &context.root_package_name()); - - // Writing the result - let output_path = context.output.join("golem.yaml"); - crate::write_if_changed(output_path, &raw_yaml)?; - Ok(()) -} - /// Changes the crate's package name to the selected WIT world's name fn change_package_name(context: &GeneratorContext, doc: &mut DocumentMut) { let crate_name = &context.world_name; doc["package"]["name"] = value(crate_name); } -/// Lists all the WIT dependencies for cargo-component in the `[package.metadata.component.target.dependencies]` -/// section -fn add_wit_dependencies(context: &&GeneratorContext, doc: &mut DocumentMut) -> anyhow::Result<()> { - let dependencies = doc - .entry("package") - .or_insert(toml_edit::Item::Table(toml_edit::Table::new())) - .as_table_mut() - .and_then(|table| { - table - .entry("metadata") - .or_insert(toml_edit::Item::Table(toml_edit::Table::new())) - .as_table_mut() - }) - .and_then(|table| { - table - .entry("component") - .or_insert(toml_edit::Item::Table(toml_edit::Table::new())) - .as_table_mut() - }) - .and_then(|table| { - table - .entry("target") - .or_insert(toml_edit::Item::Table(toml_edit::Table::new())) - .as_table_mut() - }) - .and_then(|table| { - table - .entry("dependencies") - .or_insert(toml_edit::Item::Table(toml_edit::Table::new())) - .as_table_mut() - }) - .ok_or_else(|| { - anyhow!("Failed to create the package.metadata.component.target.dependencies table") - })?; - - for (package_id, package) in &context.resolve.packages { - if let Some(paths) = context.source_map.package_paths(package_id) { - let mut parents = BTreeSet::new(); - for path in paths { - let path = Utf8Path::from_path(path).ok_or_else(|| anyhow!("Invalid path"))?; - let relative_path = path.strip_prefix(context.wit_source_path).unwrap_or(path); - if let Some(parent) = relative_path.parent() { - parents.insert(parent); - } - } - - if parents.len() > 1 { - return Err(anyhow!( - "Package {:?} has multiple source directories: {:?}", - package.name, - parents - )); - } else if let Some(parent) = parents.first() { - if *parent != Path::new("") { - let mut package_name_without_version = package.name.clone(); - package_name_without_version.version = None; - - // Adding the package as a dependency - let mut target = Table::new(); - target.insert("path", Item::Value(Value::from(format!("wit/{parent}")))); - - dependencies.insert( - &package_name_without_version.to_string(), - Item::Table(target), - ); - } - } else { - return Err(anyhow!( - "Package {:?} has no source directories", - package.name - )); - } - } - } - - Ok(()) -} - /// Files in the skeleton `src/` directory that are always overwritten by code generation. /// Skipping them avoids unnecessary timestamp changes that would trigger recompilation. const GENERATED_FILES: &[&str] = &["src/lib.rs"]; diff --git a/crates/wasm-rquickjs/src/types.rs b/crates/wasm-rquickjs/src/types.rs index 3efb3f07..10522cff 100644 --- a/crates/wasm-rquickjs/src/types.rs +++ b/crates/wasm-rquickjs/src/types.rs @@ -227,6 +227,18 @@ pub fn ident_in_imported_interface( Span::call_site(), ); + // Check if this interface belongs to a WASI package remapped to wasip2:: + if let Some(package_id) = interface.package { + if context.is_wasi_remapped_package(package_id) { + let package = &context.resolve.packages[package_id]; + let pkg_name_ident = Ident::new( + &escape_rust_ident(&package.name.name.to_snake_case()), + Span::call_site(), + ); + return quote! { wasip2::#pkg_name_ident::#name_ident::#ident }; + } + } + let mut path = Vec::new(); path.push(quote! { crate }); path.push(quote! { bindings }); @@ -401,7 +413,13 @@ pub fn get_wrapped_type_internal( TypeDefKind::Result(result) => { get_wrapped_type_result(ctx, import_rust_type, export_rust_type, result) } - TypeDefKind::Record(_) | TypeDefKind::Variant(_) => get_wrapped_type_adt(ctx), + TypeDefKind::Record(_) + | TypeDefKind::Variant(_) + | TypeDefKind::Flags(_) + | TypeDefKind::Enum(_) => get_wrapped_type_adt(ctx, *type_id), + TypeDefKind::Handle(Handle::Own(resource_type_id)) => { + get_wrapped_type_own_handle(ctx, resource_type_id) + } TypeDefKind::Handle(Handle::Borrow(resource_type_id)) => { get_wrapped_type_borrow_handle(ctx, resource_type_id) } @@ -799,8 +817,77 @@ fn get_wrapped_type_borrow_handle( } } -fn get_wrapped_type_adt(ctx: GetWrappedTypeContext<'_>) -> anyhow::Result { - Ok(WrappedType::no_wrapping(ctx.original_type_ref)) +fn get_wrapped_type_own_handle( + ctx: GetWrappedTypeContext<'_>, + resource_type_id: &TypeId, +) -> anyhow::Result { + // Follow type aliases to find the actual resource definition, since + // `use wasi:io/streams.{output-stream}` creates a type alias in the + // importing interface that points to the actual WASI resource. + let resolved = follow_type_paths(ctx.context, *resource_type_id)?; + let resolved_is_wasi = resolved.owner != ctx.context.resolve.types[*resource_type_id].owner + && matches!(resolved.owner, TypeOwner::Interface(iface_id) if { + ctx.context.resolve.interfaces.get(iface_id) + .and_then(|iface| iface.package) + .is_some_and(|pkg_id| ctx.context.is_wasi_remapped_package(pkg_id)) + }); + let is_wasi = ctx.context.is_wasi_remapped_type(*resource_type_id) || resolved_is_wasi; + + if is_wasi { + // Find the actual resource type id by following aliases + let actual_resource_type_id = find_resource_type_id(ctx.context, *resource_type_id)?; + ctx.context.record_visited_type(actual_resource_type_id); + let wrapper_name = crate::conversions::wasi_wrapper_name(ctx.context, actual_resource_type_id)?; + let wrapper_name_for_ref = wrapper_name.clone(); + let original = ctx.original_type_ref; + Ok(WrappedType { + wrap: TokenStreamWrapper::new(move |ts| { + quote! { crate::conversions::#wrapper_name(#ts) } + }), + unwrap: TokenStreamWrapper::new(move |ts| { + quote! { #ts.0 } + }), + wrapped_type_ref: quote! { crate::conversions::#wrapper_name_for_ref }, + original_type_ref: original, + }) + } else { + Ok(WrappedType::no_wrapping(ctx.original_type_ref)) + } +} + +/// Follows type aliases to find the final resource TypeId +fn find_resource_type_id(context: &GeneratorContext<'_>, type_id: TypeId) -> anyhow::Result { + let mut current = type_id; + loop { + let typ = context.typ(current)?; + match &typ.kind { + TypeDefKind::Type(Type::Id(inner)) => { + current = *inner; + } + TypeDefKind::Resource => return Ok(current), + _ => return Ok(current), + } + } +} + +fn get_wrapped_type_adt(ctx: GetWrappedTypeContext<'_>, type_id: TypeId) -> anyhow::Result { + if ctx.context.is_wasi_remapped_type(type_id) { + let wrapper_name = crate::conversions::wasi_wrapper_name(ctx.context, type_id)?; + let wrapper_name_for_ref = wrapper_name.clone(); + let original = ctx.original_type_ref; + Ok(WrappedType { + wrap: TokenStreamWrapper::new(move |ts| { + quote! { crate::conversions::#wrapper_name(#ts) } + }), + unwrap: TokenStreamWrapper::new(move |ts| { + quote! { #ts.0 } + }), + wrapped_type_ref: quote! { crate::conversions::#wrapper_name_for_ref }, + original_type_ref: original, + }) + } else { + Ok(WrappedType::no_wrapping(ctx.original_type_ref)) + } } fn get_wrapped_type_string(ctx: GetWrappedTypeContext<'_>) -> anyhow::Result { diff --git a/tests/binary_inject.rs b/tests/binary_inject.rs index fbc212f4..09c9c0ca 100644 --- a/tests/binary_inject.rs +++ b/tests/binary_inject.rs @@ -41,17 +41,19 @@ impl BinarySlotTestBuilder { )?; eprintln!("Compiling wrapper crate..."); - let status = Command::new("cargo-component") + let status = Command::new("cargo") .arg("build") + .arg("--target") + .arg("wasm32-wasip2") .arg("--target-dir") .arg(&shared_target) .current_dir(&wrapper_crate_root) .status()?; - assert!(status.success(), "cargo-component build failed"); + assert!(status.success(), "cargo build failed"); let wasm_path = Utf8Path::new("tmp") .join("inject-target") - .join("wasm32-wasip1") + .join("wasm32-wasip2") .join("debug") .join(format!("{}.wasm", example_name.to_snake_case())); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index f2da5923..5357cb23 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -915,8 +915,8 @@ impl CompiledTest { )?; println!("Compiling wrapper crate in {wrapper_crate_root}"); - let mut command = Command::new("cargo-component"); - command.arg("build"); + let mut command = Command::new("cargo"); + command.arg("build").arg("--target").arg("wasm32-wasip2"); if use_shared_target { command.arg("--target-dir"); command.arg(shared_target); @@ -930,7 +930,7 @@ impl CompiledTest { Ok(status) } else { Err(std::io::Error::other(format!( - "cargo-component build failed for {wrapper_crate_root}" + "cargo build failed for {wrapper_crate_root}" ))) } })?; @@ -940,7 +940,7 @@ impl CompiledTest { wasm: Precompiled( Utf8Path::new("tmp") .join("rt-target") - .join("wasm32-wasip1") + .join("wasm32-wasip2") .join("debug") .join(format!("{}.wasm", name.to_snake_case())), ), @@ -950,7 +950,7 @@ impl CompiledTest { wasm: Precompiled( wrapper_crate_root .join("target") - .join("wasm32-wasip1") + .join("wasm32-wasip2") .join("debug") .join(format!("{}.wasm", name.to_snake_case())), ), diff --git a/tests/compilation.rs b/tests/compilation.rs index 960913f7..1a36dd97 100644 --- a/tests/compilation.rs +++ b/tests/compilation.rs @@ -60,8 +60,8 @@ fn compilation_test( )?; println!("Compiling wrapper crate in {wrapper_crate_root}"); - let mut cmd = Command::new("cargo-component"); - cmd.arg("build"); + let mut cmd = Command::new("cargo"); + cmd.arg("build").arg("--target").arg("wasm32-wasip2"); if share_target_dir { cmd.arg("--target-dir").arg(shared_target); } diff --git a/tests/node_compat/generate-report.sh b/tests/node_compat/generate-report.sh index 417cd9a4..11094f36 100755 --- a/tests/node_compat/generate-report.sh +++ b/tests/node_compat/generate-report.sh @@ -6,7 +6,7 @@ # # Prerequisites: # - The vendored test suite must be present (run vendor.sh first) -# - cargo-component must be installed +# - wasm32-wasip2 target must be installed (rustup target add wasm32-wasip2) # # The report is written to tests/node_compat/report.md diff --git a/tests/node_compat_report.rs b/tests/node_compat_report.rs index c1a540d6..cecfb2ae 100644 --- a/tests/node_compat_report.rs +++ b/tests/node_compat_report.rs @@ -411,7 +411,7 @@ async fn compile_runner() -> anyhow::Result { let wasm_path = Utf8Path::new("tmp") .join("rt-target") - .join("wasm32-wasip1") + .join("wasm32-wasip2") .join("debug") .join(format!("{}.wasm", name.to_snake_case())); @@ -434,8 +434,10 @@ async fn compile_runner() -> anyhow::Result { )?; println!("Compiling wrapper crate in {wrapper_crate_root}"); - let status = Command::new("cargo-component") + let status = Command::new("cargo") .arg("build") + .arg("--target") + .arg("wasm32-wasip2") .arg("--target-dir") .arg(&shared_target) .args([ diff --git a/tests/wizer_benchmark.rs b/tests/wizer_benchmark.rs index 80261e2a..9fea8d2d 100644 --- a/tests/wizer_benchmark.rs +++ b/tests/wizer_benchmark.rs @@ -89,18 +89,20 @@ fn build_example(name: &str) -> Utf8PathBuf { .expect("Failed to generate wrapper crate"); eprintln!("Compiling wrapper crate..."); - let status = Command::new("cargo-component") + let status = Command::new("cargo") .arg("build") + .arg("--target") + .arg("wasm32-wasip2") .arg("--target-dir") .arg(&shared_target) .current_dir(&wrapper_crate_root) .status() - .expect("Failed to run cargo-component"); - assert!(status.success(), "cargo-component build failed"); + .expect("Failed to run cargo build"); + assert!(status.success(), "cargo build failed"); Utf8Path::new("tmp") .join("rt-target") - .join("wasm32-wasip1") + .join("wasm32-wasip2") .join("debug") .join(format!("{}.wasm", name.to_snake_case())) } From 496440247daf2a95a72fc327705a7e57ec8e9ae7 Mon Sep 17 00:00:00 2001 From: Daniel Vigovszky Date: Wed, 1 Apr 2026 19:54:12 +0200 Subject: [PATCH 2/4] Clippy & format --- crates/wasm-rquickjs/src/conversions.rs | 17 +++++---- crates/wasm-rquickjs/src/exports.rs | 48 ++++++++++++++++--------- crates/wasm-rquickjs/src/lib.rs | 10 +++--- crates/wasm-rquickjs/src/types.rs | 31 +++++++++------- 4 files changed, 67 insertions(+), 39 deletions(-) diff --git a/crates/wasm-rquickjs/src/conversions.rs b/crates/wasm-rquickjs/src/conversions.rs index 8c6dd142..ef603f42 100644 --- a/crates/wasm-rquickjs/src/conversions.rs +++ b/crates/wasm-rquickjs/src/conversions.rs @@ -15,10 +15,7 @@ use wit_parser::{Type, TypeDefKind, TypeId, TypeOwner}; /// For WASI types (from wasip2::), we cannot implement IntoJs/FromJs directly /// due to the orphan rule, so we generate newtype wrappers in conversions.rs. /// This function generates the fully qualified path to the wrapper type. -pub fn wasi_wrapper_name( - context: &GeneratorContext<'_>, - type_id: TypeId, -) -> anyhow::Result { +pub fn wasi_wrapper_name(context: &GeneratorContext<'_>, type_id: TypeId) -> anyhow::Result { let typ = context.typ(type_id)?; let name = typ .name @@ -103,7 +100,7 @@ fn generate_conversion_instances_for_type( if is_wasi_remapped && matches!(typ.kind, TypeDefKind::Resource) { let type_path = type_id_to_type_ref(context, type_id)?; let wrapper_name = wasi_wrapper_name(context, type_id)?; - let name = typ.name.as_ref().map(|n| n.as_str()).unwrap_or("Resource"); + let name = typ.name.as_deref().unwrap_or("Resource"); let name_lit = LitStr::new(name, Span::call_site()); return Ok(Some(quote! { pub struct #wrapper_name(pub #type_path); @@ -177,7 +174,9 @@ fn generate_conversion_instances_for_type( let original_field_type = &field_type.original_type_ref; let wrapped_field_type = &field_type.wrapped_type_ref; - let wrapped_field = field_type.wrap.run(quote! { #field_accessor.#rust_field_ident }); + let wrapped_field = field_type + .wrap + .run(quote! { #field_accessor.#rust_field_ident }); let unwrapped_field = field_type.unwrap.run(quote! { #rust_field_ident }); set_fields.push(quote! { @@ -245,7 +244,11 @@ fn generate_conversion_instances_for_type( let mut get_fields = Vec::new(); // For WASI-remapped types, access through `.0` (newtype wrapper) - let self_ref = if is_wasi_remapped { quote! { self.0 } } else { quote! { self } }; + let self_ref = if is_wasi_remapped { + quote! { self.0 } + } else { + quote! { self } + }; for flag in &flags.flags { let js_field_name = escape_js_ident(flag.name.to_lower_camel_case()); diff --git a/crates/wasm-rquickjs/src/exports.rs b/crates/wasm-rquickjs/src/exports.rs index 81702e2f..07f0da8a 100644 --- a/crates/wasm-rquickjs/src/exports.rs +++ b/crates/wasm-rquickjs/src/exports.rs @@ -777,11 +777,17 @@ fn generate_wasi_remaps(context: &GeneratorContext<'_>) -> TokenStream { ("wasi:cli/terminal-stderr", "wasip2::cli::terminal_stderr"), ("wasi:cli/terminal-stdin", "wasip2::cli::terminal_stdin"), ("wasi:cli/terminal-stdout", "wasip2::cli::terminal_stdout"), - ("wasi:clocks/monotonic-clock", "wasip2::clocks::monotonic_clock"), + ( + "wasi:clocks/monotonic-clock", + "wasip2::clocks::monotonic_clock", + ), ("wasi:clocks/wall-clock", "wasip2::clocks::wall_clock"), ("wasi:filesystem/preopens", "wasip2::filesystem::preopens"), ("wasi:filesystem/types", "wasip2::filesystem::types"), - ("wasi:http/outgoing-handler", "wasip2::http::outgoing_handler"), + ( + "wasi:http/outgoing-handler", + "wasip2::http::outgoing_handler", + ), ("wasi:http/types", "wasip2::http::types"), ("wasi:io/error", "wasip2::io::error"), ("wasi:io/poll", "wasip2::io::poll"), @@ -789,13 +795,25 @@ fn generate_wasi_remaps(context: &GeneratorContext<'_>) -> TokenStream { ("wasi:random/insecure", "wasip2::random::insecure"), ("wasi:random/insecure-seed", "wasip2::random::insecure_seed"), ("wasi:random/random", "wasip2::random::random"), - ("wasi:sockets/instance-network", "wasip2::sockets::instance_network"), - ("wasi:sockets/ip-name-lookup", "wasip2::sockets::ip_name_lookup"), + ( + "wasi:sockets/instance-network", + "wasip2::sockets::instance_network", + ), + ( + "wasi:sockets/ip-name-lookup", + "wasip2::sockets::ip_name_lookup", + ), ("wasi:sockets/network", "wasip2::sockets::network"), ("wasi:sockets/tcp", "wasip2::sockets::tcp"), - ("wasi:sockets/tcp-create-socket", "wasip2::sockets::tcp_create_socket"), + ( + "wasi:sockets/tcp-create-socket", + "wasip2::sockets::tcp_create_socket", + ), ("wasi:sockets/udp", "wasip2::sockets::udp"), - ("wasi:sockets/udp-create-socket", "wasip2::sockets::udp_create_socket"), + ( + "wasi:sockets/udp-create-socket", + "wasip2::sockets::udp_create_socket", + ), ]; // Collect all interfaces used in the world, with their fully-qualified versioned names @@ -805,16 +823,14 @@ fn generate_wasi_remaps(context: &GeneratorContext<'_>) -> TokenStream { for (key, _) in world.imports.iter().chain(world.exports.iter()) { if let WorldKey::Interface(id) = key { let interface = &context.resolve.interfaces[*id]; - if let Some(ref name) = interface.name { - if let Some(package_id) = interface.package { - let package = &context.resolve.packages[package_id]; - let unversioned = format!( - "{}:{}/{}", - package.name.namespace, package.name.name, name - ); - let versioned = package.name.interface_id(name); - used_interfaces.insert(unversioned, versioned); - } + if let Some(ref name) = interface.name + && let Some(package_id) = interface.package + { + let package = &context.resolve.packages[package_id]; + let unversioned = + format!("{}:{}/{}", package.name.namespace, package.name.name, name); + let versioned = package.name.interface_id(name); + used_interfaces.insert(unversioned, versioned); } } } diff --git a/crates/wasm-rquickjs/src/lib.rs b/crates/wasm-rquickjs/src/lib.rs index 4874af33..4e2ee9a6 100644 --- a/crates/wasm-rquickjs/src/lib.rs +++ b/crates/wasm-rquickjs/src/lib.rs @@ -212,10 +212,12 @@ pub fn generate_dts( struct GeneratorContext<'a> { output: &'a Utf8Path, + #[allow(dead_code)] wit_source_path: &'a Utf8Path, resolve: Resolve, root_package: PackageId, world: WorldId, + #[allow(dead_code)] source_map: PackageSourceMap, visited_types: RefCell>, world_name: String, @@ -352,10 +354,10 @@ impl<'a> GeneratorContext<'a> { if let Some(typ) = self.resolve.types.get(type_id) { match &typ.owner { TypeOwner::Interface(interface_id) => { - if let Some(interface) = self.resolve.interfaces.get(*interface_id) { - if let Some(package_id) = interface.package { - return self.is_wasi_remapped_package(package_id); - } + if let Some(interface) = self.resolve.interfaces.get(*interface_id) + && let Some(package_id) = interface.package + { + return self.is_wasi_remapped_package(package_id); } false } diff --git a/crates/wasm-rquickjs/src/types.rs b/crates/wasm-rquickjs/src/types.rs index 10522cff..3ab06039 100644 --- a/crates/wasm-rquickjs/src/types.rs +++ b/crates/wasm-rquickjs/src/types.rs @@ -228,15 +228,15 @@ pub fn ident_in_imported_interface( ); // Check if this interface belongs to a WASI package remapped to wasip2:: - if let Some(package_id) = interface.package { - if context.is_wasi_remapped_package(package_id) { - let package = &context.resolve.packages[package_id]; - let pkg_name_ident = Ident::new( - &escape_rust_ident(&package.name.name.to_snake_case()), - Span::call_site(), - ); - return quote! { wasip2::#pkg_name_ident::#name_ident::#ident }; - } + if let Some(package_id) = interface.package + && context.is_wasi_remapped_package(package_id) + { + let package = &context.resolve.packages[package_id]; + let pkg_name_ident = Ident::new( + &escape_rust_ident(&package.name.name.to_snake_case()), + Span::call_site(), + ); + return quote! { wasip2::#pkg_name_ident::#name_ident::#ident }; } let mut path = Vec::new(); @@ -837,7 +837,8 @@ fn get_wrapped_type_own_handle( // Find the actual resource type id by following aliases let actual_resource_type_id = find_resource_type_id(ctx.context, *resource_type_id)?; ctx.context.record_visited_type(actual_resource_type_id); - let wrapper_name = crate::conversions::wasi_wrapper_name(ctx.context, actual_resource_type_id)?; + let wrapper_name = + crate::conversions::wasi_wrapper_name(ctx.context, actual_resource_type_id)?; let wrapper_name_for_ref = wrapper_name.clone(); let original = ctx.original_type_ref; Ok(WrappedType { @@ -856,7 +857,10 @@ fn get_wrapped_type_own_handle( } /// Follows type aliases to find the final resource TypeId -fn find_resource_type_id(context: &GeneratorContext<'_>, type_id: TypeId) -> anyhow::Result { +fn find_resource_type_id( + context: &GeneratorContext<'_>, + type_id: TypeId, +) -> anyhow::Result { let mut current = type_id; loop { let typ = context.typ(current)?; @@ -870,7 +874,10 @@ fn find_resource_type_id(context: &GeneratorContext<'_>, type_id: TypeId) -> any } } -fn get_wrapped_type_adt(ctx: GetWrappedTypeContext<'_>, type_id: TypeId) -> anyhow::Result { +fn get_wrapped_type_adt( + ctx: GetWrappedTypeContext<'_>, + type_id: TypeId, +) -> anyhow::Result { if ctx.context.is_wasi_remapped_type(type_id) { let wrapper_name = crate::conversions::wasi_wrapper_name(ctx.context, type_id)?; let wrapper_name_for_ref = wrapper_name.clone(); From 6a10a256c8fd2d938a5fa8bbb49386ec950ed0bf Mon Sep 17 00:00:00 2001 From: Daniel Vigovszky Date: Thu, 2 Apr 2026 12:52:32 +0200 Subject: [PATCH 3/4] Fixes --- crates/wasm-rquickjs/src/conversions.rs | 15 ++++- crates/wasm-rquickjs/src/imports.rs | 45 ++++++++++----- crates/wasm-rquickjs/src/lib.rs | 33 +++++++++++ crates/wasm-rquickjs/src/types.rs | 75 ++++++++++++++++++++----- 4 files changed, 136 insertions(+), 32 deletions(-) diff --git a/crates/wasm-rquickjs/src/conversions.rs b/crates/wasm-rquickjs/src/conversions.rs index ef603f42..cd46aef8 100644 --- a/crates/wasm-rquickjs/src/conversions.rs +++ b/crates/wasm-rquickjs/src/conversions.rs @@ -22,7 +22,9 @@ pub fn wasi_wrapper_name(context: &GeneratorContext<'_>, type_id: TypeId) -> any .as_ref() .ok_or_else(|| anyhow!("WASI type has no name"))?; - // Build a unique wrapper name including the interface name to avoid conflicts + // Build a unique wrapper name including the package and interface name to avoid conflicts + // between types with the same name in different WASI packages (e.g., wasi:filesystem/types + // and wasi:http/types both have error-code). let interface_prefix = match &typ.owner { TypeOwner::Interface(iface_id) => { let iface = context @@ -30,11 +32,18 @@ pub fn wasi_wrapper_name(context: &GeneratorContext<'_>, type_id: TypeId) -> any .interfaces .get(*iface_id) .ok_or_else(|| anyhow!("Unknown interface id"))?; - if let Some(iface_name) = &iface.name { + let pkg_prefix = if let Some(pkg_id) = iface.package { + let pkg = &context.resolve.packages[pkg_id]; + pkg.name.name.to_upper_camel_case() + } else { + String::new() + }; + let iface_prefix = if let Some(iface_name) = &iface.name { iface_name.to_upper_camel_case() } else { String::new() - } + }; + format!("{}{}", pkg_prefix, iface_prefix) } _ => String::new(), }; diff --git a/crates/wasm-rquickjs/src/imports.rs b/crates/wasm-rquickjs/src/imports.rs index 624ac22b..207f2190 100644 --- a/crates/wasm-rquickjs/src/imports.rs +++ b/crates/wasm-rquickjs/src/imports.rs @@ -68,15 +68,7 @@ pub fn collect_imported_interfaces<'a>( }; match import { WorldItem::Interface { id, .. } => { - // Skip WASI-remapped interfaces — their types come from wasip2:: - // and we cannot implement IntoJs/FromJs for foreign types. - let interface = &context.resolve.interfaces[*id]; - let is_wasi_remapped = interface - .package - .is_some_and(|pkg_id| context.is_wasi_remapped_package(pkg_id)); - if !is_wasi_remapped { - interfaces.push(context.get_imported_interface(id)?); - } + interfaces.push(context.get_imported_interface(id)?); } WorldItem::Function(function) => { global_imports.push((name, function)); @@ -536,11 +528,36 @@ fn generate_import_module( let rquickjs_class = generate_rquickjs_class_module(resource_name, &resource_name_ident, &resource_name_lit); - // For WASI-remapped resources, skip the IntoJs/FromJs impls on the bindgen type - // (they're foreign types from wasip2::) and the BorrowWrapper, since those would - // violate the orphan rule. + // For WASI-remapped resources, skip the IntoJs/FromJs impls on the foreign + // bindgen type (they're from wasip2:: and would violate the orphan rule). + // The BorrowWrapper is always generated since it's a local type. let foreign_type_impls = if context.is_wasi_remapped_type(resource_type_id) { - quote! {} + quote! { + pub struct #borrow_wrapper_ident(pub #bindgen_path); + + impl<'js> rquickjs::FromJs<'js> for #borrow_wrapper_ident { + fn from_js(ctx: &rquickjs::Ctx<'js>, value: rquickjs::Value<'js>) -> rquickjs::Result { + let wrapper = #resource_name_ident::from_js(ctx, value)?; + unsafe { + Ok(#borrow_wrapper_ident( + #bindgen_path::from_handle( + wrapper + .inner + .ok_or_else(|| rquickjs::Error::FromJs { from: "JavaScript object", to: #resource_name_lit, message: Some("Resource has already been disposed".to_string()) })? + .handle(), + ), + )) + } + } + } + + impl Drop for #borrow_wrapper_ident { + fn drop(&mut self) { + // By taking out the handle from the resource it is not going to be dropped + let _ = self.0.take_handle(); + } + } + } } else { quote! { impl<'js> rquickjs::IntoJs<'js> for #bindgen_path { @@ -599,7 +616,7 @@ fn generate_import_module( #[derive(Clone, JsLifetime, Trace)] pub struct #resource_name_ident { #[qjs(skip_trace = true)] - inner: Option>, + pub(crate) inner: Option>, } #rquickjs_class diff --git a/crates/wasm-rquickjs/src/lib.rs b/crates/wasm-rquickjs/src/lib.rs index 4e2ee9a6..59adfa9c 100644 --- a/crates/wasm-rquickjs/src/lib.rs +++ b/crates/wasm-rquickjs/src/lib.rs @@ -349,6 +349,39 @@ impl<'a> GeneratorContext<'a> { .any(|(pkg_name, _)| *pkg_name == package.name.name.as_str()) } + /// For a WASI-remapped resource type, returns the import module path and resource class + /// name (e.g., `crate::modules::wasi_io_0_2_3_poll::Pollable`). + fn wasi_resource_module_path( + &self, + type_id: TypeId, + ) -> Option<(proc_macro2::TokenStream, Ident)> { + let typ = self.resolve.types.get(type_id)?; + let resource_name = typ.name.as_ref()?; + let resource_ident = Ident::new(&resource_name.to_upper_camel_case(), Span::call_site()); + + let interface_id = match &typ.owner { + TypeOwner::Interface(id) => *id, + _ => return None, + }; + let interface = self.resolve.interfaces.get(interface_id)?; + let interface_name = interface.name.as_ref()?; + let package_id = interface.package?; + let package = self.resolve.packages.get(package_id)?; + let package_name = &package.name; + + let module_name = format!( + "{}_{}", + package_name.to_string().to_snake_case(), + interface_name.to_snake_case() + ); + let module_ident = Ident::new(&module_name, Span::call_site()); + + Some(( + quote::quote! { crate::modules::#module_ident }, + resource_ident, + )) + } + /// Returns `true` if the given type belongs to a WASI-remapped interface. fn is_wasi_remapped_type(&self, type_id: TypeId) -> bool { if let Some(typ) = self.resolve.types.get(type_id) { diff --git a/crates/wasm-rquickjs/src/types.rs b/crates/wasm-rquickjs/src/types.rs index 3ab06039..c2d6ae06 100644 --- a/crates/wasm-rquickjs/src/types.rs +++ b/crates/wasm-rquickjs/src/types.rs @@ -772,6 +772,8 @@ fn get_wrapped_type_result( let wrap_ok = ok.wrap.run(quote! { v }); let wrap_err = err.wrap.run(quote! { v }); + let unwrap_ok = ok.unwrap.run(quote! { v }); + let unwrap_err = err.unwrap.run(quote! { v }); Ok(WrappedType { wrap: TokenStreamWrapper::new(move |ts| { @@ -784,7 +786,14 @@ fn get_wrapped_type_result( ) } }), - unwrap: TokenStreamWrapper::new(|ts| quote! { #ts.0 }), + unwrap: TokenStreamWrapper::new(move |ts| { + quote! { + match #ts.0 { + Ok(v) => Ok(#unwrap_ok), + Err(v) => Err(#unwrap_err), + } + } + }), original_type_ref: ctx.original_type_ref, wrapped_type_ref: quote! { crate::wrappers::JsResult<#wrapped_ok, #wrapped_err> }, }) @@ -837,20 +846,56 @@ fn get_wrapped_type_own_handle( // Find the actual resource type id by following aliases let actual_resource_type_id = find_resource_type_id(ctx.context, *resource_type_id)?; ctx.context.record_visited_type(actual_resource_type_id); - let wrapper_name = - crate::conversions::wasi_wrapper_name(ctx.context, actual_resource_type_id)?; - let wrapper_name_for_ref = wrapper_name.clone(); - let original = ctx.original_type_ref; - Ok(WrappedType { - wrap: TokenStreamWrapper::new(move |ts| { - quote! { crate::conversions::#wrapper_name(#ts) } - }), - unwrap: TokenStreamWrapper::new(move |ts| { - quote! { #ts.0 } - }), - wrapped_type_ref: quote! { crate::conversions::#wrapper_name_for_ref }, - original_type_ref: original, - }) + + // Use the import module's resource class (which has all methods like promise/ready) + // instead of the bare conversions wrapper. + if let Some((module_path, resource_ident)) = ctx + .context + .wasi_resource_module_path(actual_resource_type_id) + { + let module_path_wrap = module_path.clone(); + let resource_ident_wrap = resource_ident.clone(); + let original = ctx.original_type_ref.clone(); + let original_for_unwrap = ctx.original_type_ref; + Ok(WrappedType { + wrap: TokenStreamWrapper::new(move |ts| { + quote! { + #module_path_wrap::#resource_ident_wrap { + inner: Some(std::rc::Rc::new(#ts)), + } + } + }), + unwrap: TokenStreamWrapper::new(move |ts| { + quote! { + unsafe { + #original_for_unwrap::from_handle( + #ts.inner + .expect("Resource has already been disposed") + .take_handle() + ) + } + } + }), + wrapped_type_ref: quote! { #module_path::#resource_ident }, + original_type_ref: original, + }) + } else { + // Fallback to conversions wrapper if we can't determine the module path + let wrapper_name = + crate::conversions::wasi_wrapper_name(ctx.context, actual_resource_type_id)?; + let wrapper_name_for_ref = wrapper_name.clone(); + let original = ctx.original_type_ref; + Ok(WrappedType { + wrap: TokenStreamWrapper::new(move |ts| { + quote! { crate::conversions::#wrapper_name(#ts) } + }), + unwrap: TokenStreamWrapper::new(move |ts| { + quote! { #ts.0 } + }), + wrapped_type_ref: quote! { crate::conversions::#wrapper_name_for_ref }, + original_type_ref: original, + }) + } } else { Ok(WrappedType::no_wrapping(ctx.original_type_ref)) } From ae8f9656284dd5ba679ea6b7cbaaaa490c60fa3b Mon Sep 17 00:00:00 2001 From: Daniel Vigovszky Date: Thu, 2 Apr 2026 13:09:20 +0200 Subject: [PATCH 4/4] node:fs fix --- crates/wasm-rquickjs/skeleton/src/builtin/fs.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/wasm-rquickjs/skeleton/src/builtin/fs.rs b/crates/wasm-rquickjs/skeleton/src/builtin/fs.rs index 074ced70..591e0fbb 100644 --- a/crates/wasm-rquickjs/skeleton/src/builtin/fs.rs +++ b/crates/wasm-rquickjs/skeleton/src/builtin/fs.rs @@ -929,9 +929,9 @@ pub mod native_module { return result; } } - match file.write(data) { - Ok(written) => { - result.set("bytesWritten", written as f64).unwrap(); + match file.write_all(data) { + Ok(()) => { + result.set("bytesWritten", data.len() as f64).unwrap(); } Err(err) => { result @@ -976,9 +976,9 @@ pub mod native_module { } } let bytes = data.as_bytes(); - match file.write(bytes) { - Ok(written) => { - result.set("bytesWritten", written as f64).unwrap(); + match file.write_all(bytes) { + Ok(()) => { + result.set("bytesWritten", bytes.len() as f64).unwrap(); } Err(err) => { result