diff --git a/crates/backend/src/ast.rs b/crates/backend/src/ast.rs index ce5baacf..37dfbc61 100644 --- a/crates/backend/src/ast.rs +++ b/crates/backend/src/ast.rs @@ -207,6 +207,7 @@ pub struct Enum { pub name: Ident, pub variants: Vec, pub comments: Vec, + pub hole: u32, } #[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))] diff --git a/crates/backend/src/codegen.rs b/crates/backend/src/codegen.rs index 8edfa49d..6b88c7e2 100644 --- a/crates/backend/src/codegen.rs +++ b/crates/backend/src/codegen.rs @@ -1038,6 +1038,7 @@ impl<'a> ToTokens for DescribeImport<'a> { impl ToTokens for ast::Enum { fn to_tokens(&self, into: &mut TokenStream) { let enum_name = &self.name; + let hole = &self.hole; let cast_clauses = self.variants.iter().map(|variant| { let variant_name = &variant.name; quote! { @@ -1061,6 +1062,7 @@ impl ToTokens for ast::Enum { type Abi = u32; #[allow(clippy::*)] + #[inline] unsafe fn from_abi( js: u32, _extra: &mut ::wasm_bindgen::convert::Stack, @@ -1071,11 +1073,22 @@ impl ToTokens for ast::Enum { } } + impl ::wasm_bindgen::convert::OptionFromWasmAbi for #enum_name { + #[inline] + fn is_none(val: &u32) -> bool { *val == #hole } + } + + impl ::wasm_bindgen::convert::OptionIntoWasmAbi for #enum_name { + #[inline] + fn none() -> Self::Abi { #hole } + } + impl ::wasm_bindgen::describe::WasmDescribe for #enum_name { #[allow(clippy::*)] fn describe() { use wasm_bindgen::describe::*; inform(ENUM); + inform(#hole); } } }) diff --git a/crates/cli-support/src/descriptor.rs b/crates/cli-support/src/descriptor.rs index 1bcea29f..3479a74d 100644 --- a/crates/cli-support/src/descriptor.rs +++ b/crates/cli-support/src/descriptor.rs @@ -59,7 +59,7 @@ pub enum Descriptor { Vector(Box), String, Anyref, - Enum, + Enum { hole: u32 }, RustStruct(String), Char, Option(Box), @@ -128,7 +128,7 @@ impl Descriptor { OPTIONAL => Descriptor::Option(Box::new(Descriptor::_decode(data))), STRING => Descriptor::String, ANYREF => Descriptor::Anyref, - ENUM => Descriptor::Enum, + ENUM => Descriptor::Enum { hole: get(data) }, RUST_STRUCT => { let name = (0..get(data)) .map(|_| char::from_u32(get(data)).unwrap()) @@ -159,7 +159,7 @@ impl Descriptor { | Descriptor::U32 | Descriptor::F32 | Descriptor::F64 - | Descriptor::Enum => true, + | Descriptor::Enum { .. } => true, _ => return false, } } diff --git a/crates/cli-support/src/js/js2rust.rs b/crates/cli-support/src/js/js2rust.rs index aae626ed..91b4b4f2 100644 --- a/crates/cli-support/src/js/js2rust.rs +++ b/crates/cli-support/src/js/js2rust.rs @@ -306,6 +306,14 @@ impl<'a, 'b> Js2Rust<'a, 'b> { .push(format!("isLikeNone({0}) ? 0 : {0}.codePointAt(0)", name)); return Ok(self); } + Descriptor::Enum { hole } => { + self.cx.expose_is_like_none(); + self.js_arguments + .push((name.clone(), "number | undefined".to_string())); + self.rust_arguments + .push(format!("isLikeNone({0}) ? {1} : {0}", name, hole)); + return Ok(self); + } _ => bail!( "unsupported optional argument type for calling Rust function from JS: {:?}", arg @@ -609,6 +617,14 @@ impl<'a, 'b> Js2Rust<'a, 'b> { .to_string(); return Ok(self); } + Descriptor::Enum { hole } => { + self.ret_ty = "number | undefined".to_string(); + self.ret_expr = format!(" + const ret = RET; + return ret === {} ? undefined : ret; + ", hole); + return Ok(self); + } _ => bail!( "unsupported optional return type for calling Rust function from JS: {:?}", ty @@ -734,12 +750,16 @@ impl<'a, 'b> Js2Rust<'a, 'b> { .map(|s| format!("{}: {}", s.0, s.1)) .collect::>() .join(", "); - let mut ts = format!("{} {}({})", prefix, self.js_name, ts_args); + let mut ts = if prefix.is_empty() { + format!("{}({})", self.js_name, ts_args) + } else { + format!("{} {}({})", prefix, self.js_name, ts_args) + }; if self.constructor.is_none() { ts.push_str(": "); ts.push_str(&self.ret_ty); } - ts.push_str(";\n"); + ts.push(';'); (js, ts, self.js_doc_comments()) } } diff --git a/crates/cli-support/src/js/mod.rs b/crates/cli-support/src/js/mod.rs index 97a062f4..0ff36301 100644 --- a/crates/cli-support/src/js/mod.rs +++ b/crates/cli-support/src/js/mod.rs @@ -845,7 +845,7 @@ impl<'a> Context<'a> { ", name, )); - ts_dst.push_str("free(): void;\n"); + ts_dst.push_str(" free(): void;"); dst.push_str(&class.contents); ts_dst.push_str(&class.typescript); dst.push_str("}\n"); @@ -2399,6 +2399,8 @@ impl<'a, 'b> SubContext<'a, 'b> { .contents .push_str(&format_doc_comments(&export.comments, Some(js_doc))); + class.typescript.push_str(" "); // Indentation + if export.is_constructor { if class.has_constructor { bail!("found duplicate constructor `{}`", export.function.name); @@ -2571,12 +2573,12 @@ impl<'a, 'b> SubContext<'a, 'b> { .typescript .push_str(&format!("export enum {} {{", enum_.name)); - variants.clear(); for variant in enum_.variants.iter() { - variants.push_str(&format!("{},", variant.name)); + self.cx + .typescript + .push_str(&format!("\n {},", variant.name)); } - self.cx.typescript.push_str(&variants); - self.cx.typescript.push_str("}\n"); + self.cx.typescript.push_str("\n}\n"); } fn generate_struct(&mut self, struct_: &decode::Struct) -> Result<(), Error> { @@ -2596,7 +2598,7 @@ impl<'a, 'b> SubContext<'a, 'b> { .argument(&descriptor)? .ret(&Descriptor::Unit)?; ts_dst.push_str(&format!( - "{}{}: {};\n", + "\n {}{}: {};", if field.readonly { "readonly " } else { "" }, field.name, &cx.js_arguments[0].1 diff --git a/crates/cli-support/src/js/rust2js.rs b/crates/cli-support/src/js/rust2js.rs index b2081c6d..43cb8e2e 100644 --- a/crates/cli-support/src/js/rust2js.rs +++ b/crates/cli-support/src/js/rust2js.rs @@ -200,6 +200,11 @@ impl<'a, 'b> Rust2Js<'a, 'b> { .push(format!("{0} === 0xFFFFFF ? undefined : {0} !== 0", abi)); return Ok(()); } + Descriptor::Enum { hole } => { + self.js_arguments + .push(format!("{0} === {1} ? undefined : {0}", abi, hole)); + return Ok(()); + } Descriptor::Char => { let value = self.shim_argument(); self.js_arguments.push(format!( @@ -441,6 +446,14 @@ impl<'a, 'b> Rust2Js<'a, 'b> { .to_string(); return Ok(()); } + Descriptor::Enum { hole } => { + self.cx.expose_is_like_none(); + self.ret_expr = format!(" + const val = JS; + return isLikeNone(val) ? {} : val; + ", hole); + return Ok(()); + } _ => bail!( "unsupported optional return type for calling JS function from Rust: {:?}", ty diff --git a/crates/js-sys/src/lib.rs b/crates/js-sys/src/lib.rs index cf70f351..556b827f 100644 --- a/crates/js-sys/src/lib.rs +++ b/crates/js-sys/src/lib.rs @@ -4841,7 +4841,7 @@ macro_rules! arrays { /// Additionally the returned object can be safely mutated but the /// input slice isn't guaranteed to be mutable. /// - /// Finally, the returned objet is disconnected from the input + /// Finally, the returned object is disconnected from the input /// slice's lifetime, so there's no guarantee that the data is read /// at the right time. pub unsafe fn view(rust: &[$ty]) -> $name { diff --git a/crates/macro-support/src/lib.rs b/crates/macro-support/src/lib.rs index b931bc82..c9c70494 100644 --- a/crates/macro-support/src/lib.rs +++ b/crates/macro-support/src/lib.rs @@ -13,8 +13,11 @@ extern crate wasm_bindgen_shared as shared; use backend::{Diagnostic, TryToTokens}; pub use parser::BindgenAttrs; +use quote::ToTokens; use parser::MacroParse; use proc_macro2::TokenStream; +use syn::parse::{Parse, ParseStream, Result as SynResult}; +use quote::TokenStreamExt; mod parser; @@ -36,3 +39,68 @@ pub fn expand(attr: TokenStream, input: TokenStream) -> Result Result { + parser::reset_attrs_used(); + let mut item = syn::parse2::(input)?; + let opts: ClassMarker = syn::parse2(attr)?; + + let mut program = backend::ast::Program::default(); + item.macro_parse(&mut program, (&opts.class, &opts.js_class))?; + parser::assert_all_attrs_checked(); // same as above + + // This is where things are slightly different, we are being expanded in the + // context of an impl so we can't inject arbitrary item-like tokens into the + // output stream. If we were to do that then it wouldn't parse! + // + // Instead what we want to do is to generate the tokens for `program` into + // the header of the function. This'll inject some no_mangle functions and + // statics and such, and they should all be valid in the context of the + // start of a function. + // + // We manually implement `ToTokens for ImplItemMethod` here, injecting our + // program's tokens before the actual method's inner body tokens. + let mut tokens = proc_macro2::TokenStream::new(); + tokens.append_all(item.attrs.iter().filter(|attr| { + match attr.style { + syn::AttrStyle::Outer => true, + _ => false, + } + })); + item.vis.to_tokens(&mut tokens); + item.sig.to_tokens(&mut tokens); + let mut err = None; + item.block.brace_token.surround(&mut tokens, |tokens| { + if let Err(e) = program.try_to_tokens(tokens) { + err = Some(e); + } + tokens.append_all(item.attrs.iter().filter(|attr| { + match attr.style { + syn::AttrStyle::Inner(_) => true, + _ => false, + } + })); + tokens.append_all(&item.block.stmts); + }); + + if let Some(err) = err { + return Err(err) + } + + Ok(tokens) +} + +struct ClassMarker { + class: syn::Ident, + js_class: String, +} + +impl Parse for ClassMarker { + fn parse(input: ParseStream) -> SynResult { + let class = input.parse::()?; + input.parse::()?; + let js_class = input.parse::()?.value(); + Ok(ClassMarker { class, js_class }) + } +} diff --git a/crates/macro-support/src/parser.rs b/crates/macro-support/src/parser.rs index ef85cc5f..c3ea5ad9 100644 --- a/crates/macro-support/src/parser.rs +++ b/crates/macro-support/src/parser.rs @@ -796,7 +796,7 @@ impl<'a> MacroParse<(Option, &'a mut TokenStream)> for syn::Item { } impl<'a> MacroParse for &'a mut syn::ItemImpl { - fn macro_parse(self, program: &mut ast::Program, opts: BindgenAttrs) -> Result<(), Diagnostic> { + fn macro_parse(self, _program: &mut ast::Program, opts: BindgenAttrs) -> Result<(), Diagnostic> { if self.defaultness.is_some() { bail_span!( self.defaultness, @@ -830,7 +830,7 @@ impl<'a> MacroParse for &'a mut syn::ItemImpl { }; let mut errors = Vec::new(); for item in self.items.iter_mut() { - if let Err(e) = (&name, item).macro_parse(program, &opts) { + if let Err(e) = prepare_for_impl_recursion(item, &name, &opts) { errors.push(e); } } @@ -840,77 +840,106 @@ impl<'a> MacroParse for &'a mut syn::ItemImpl { } } -impl<'a, 'b> MacroParse<&'a BindgenAttrs> for (&'a Ident, &'b mut syn::ImplItem) { +// Prepare for recursion into an `impl` block. Here we want to attach an +// internal attribute, `__wasm_bindgen_class_marker`, with any metadata we need +// to pass from the impl to the impl item. Recursive macro expansion will then +// expand the `__wasm_bindgen_class_marker` attribute. +// +// Note that we currently do this because inner items may have things like cfgs +// on them, so we want to expand the impl first, let the insides get cfg'd, and +// then go for the rest. +fn prepare_for_impl_recursion( + item: &mut syn::ImplItem, + class: &Ident, + impl_opts: &BindgenAttrs +) -> Result<(), Diagnostic> { + let method = match item { + syn::ImplItem::Method(m) => m, + syn::ImplItem::Const(_) => { + bail_span!( + &*item, + "const definitions aren't supported with #[wasm_bindgen]" + ); + } + syn::ImplItem::Type(_) => bail_span!( + &*item, + "type definitions in impls aren't supported with #[wasm_bindgen]" + ), + syn::ImplItem::Existential(_) => bail_span!( + &*item, + "existentials in impls aren't supported with #[wasm_bindgen]" + ), + syn::ImplItem::Macro(_) => { + // In theory we want to allow this, but we have no way of expanding + // the macro and then placing our magical attributes on the expanded + // functions. As a result, just disallow it for now to hopefully + // ward off buggy results from this macro. + bail_span!(&*item, "macros in impls aren't supported"); + } + syn::ImplItem::Verbatim(_) => panic!("unparsed impl item?"), + }; + + let js_class = impl_opts + .js_class() + .map(|s| s.0.to_string()) + .unwrap_or(class.to_string()); + + method.attrs.insert(0, syn::Attribute { + pound_token: Default::default(), + style: syn::AttrStyle::Outer, + bracket_token: Default::default(), + path: syn::Ident::new("__wasm_bindgen_class_marker", Span::call_site()).into(), + tts: quote::quote! { (#class = #js_class) }.into(), + }); + + Ok(()) +} + +impl<'a, 'b> MacroParse<(&'a Ident, &'a str)> for &'b mut syn::ImplItemMethod { fn macro_parse( self, program: &mut ast::Program, - impl_opts: &'a BindgenAttrs, + (class, js_class): (&'a Ident, &'a str), ) -> Result<(), Diagnostic> { - let (class, item) = self; - let method = match item { - syn::ImplItem::Method(ref mut m) => m, - syn::ImplItem::Const(_) => { - bail_span!( - &*item, - "const definitions aren't supported with #[wasm_bindgen]" - ); - } - syn::ImplItem::Type(_) => bail_span!( - &*item, - "type definitions in impls aren't supported with #[wasm_bindgen]" - ), - syn::ImplItem::Existential(_) => bail_span!( - &*item, - "existentials in impls aren't supported with #[wasm_bindgen]" - ), - syn::ImplItem::Macro(_) => { - bail_span!(&*item, "macros in impls aren't supported"); - } - syn::ImplItem::Verbatim(_) => panic!("unparsed impl item?"), - }; - match method.vis { + match self.vis { syn::Visibility::Public(_) => {} _ => return Ok(()), } - if method.defaultness.is_some() { + if self.defaultness.is_some() { panic!("default methods are not supported"); } - if method.sig.constness.is_some() { + if self.sig.constness.is_some() { bail_span!( - method.sig.constness, + self.sig.constness, "can only #[wasm_bindgen] non-const functions", ); } - if method.sig.unsafety.is_some() { - bail_span!(method.sig.unsafety, "can only bindgen safe functions",); + if self.sig.unsafety.is_some() { + bail_span!(self.sig.unsafety, "can only bindgen safe functions",); } - let opts = BindgenAttrs::find(&mut method.attrs)?; - let comments = extract_doc_comments(&method.attrs); + let opts = BindgenAttrs::find(&mut self.attrs)?; + let comments = extract_doc_comments(&self.attrs); let is_constructor = opts.constructor().is_some(); let (function, method_self) = function_from_decl( - &method.sig.ident, + &self.sig.ident, &opts, - Box::new(method.sig.decl.clone()), - method.attrs.clone(), - method.vis.clone(), + Box::new(self.sig.decl.clone()), + self.attrs.clone(), + self.vis.clone(), true, Some(class), )?; - let js_class = impl_opts - .js_class() - .map(|s| s.0.to_string()) - .unwrap_or(class.to_string()); program.exports.push(ast::Export { rust_class: Some(class.clone()), - js_class: Some(js_class), + js_class: Some(js_class.to_string()), method_self, is_constructor, function, comments, start: false, - rust_name: method.sig.ident.clone(), + rust_name: self.sig.ident.clone(), }); opts.check_used()?; Ok(()) @@ -924,6 +953,12 @@ impl MacroParse<()> for syn::ItemEnum { _ => bail_span!(self, "only public enums are allowed with #[wasm_bindgen]"), } + if self.variants.len() == 0 { + bail_span!(self, "cannot export empty enums to JS"); + } + + let has_discriminant = self.variants[0].discriminant.is_some(); + let variants = self .variants .iter() @@ -933,6 +968,14 @@ impl MacroParse<()> for syn::ItemEnum { syn::Fields::Unit => (), _ => bail_span!(v.fields, "only C-Style enums allowed with #[wasm_bindgen]"), } + + // Require that everything either has a discriminant or doesn't. + // We don't really want to get in the business of emulating how + // rustc assigns values to enums. + if v.discriminant.is_some() != has_discriminant { + bail_span!(v, "must either annotate discriminant of all variants or none"); + } + let value = match v.discriminant { Some(( _, @@ -963,12 +1006,30 @@ impl MacroParse<()> for syn::ItemEnum { value, }) }) - .collect::>()?; + .collect::, Diagnostic>>()?; + + let mut values = variants.iter().map(|v| v.value).collect::>(); + values.sort(); + let hole = values.windows(2) + .filter_map(|window| { + if window[0] + 1 != window[1] { + Some(window[0] + 1) + } else { + None + } + }) + .next() + .unwrap_or(*values.last().unwrap() + 1); + for value in values { + assert!(hole != value); + } + let comments = extract_doc_comments(&self.attrs); program.enums.push(ast::Enum { name: self.ident, variants, comments, + hole, }); Ok(()) } diff --git a/crates/macro/src/lib.rs b/crates/macro/src/lib.rs index f94d2b52..69149fdd 100755 --- a/crates/macro/src/lib.rs +++ b/crates/macro/src/lib.rs @@ -19,3 +19,16 @@ pub fn wasm_bindgen(attr: TokenStream, input: TokenStream) -> TokenStream { Err(diagnostic) => (quote! { #diagnostic }).into(), } } + +#[proc_macro_attribute] +pub fn __wasm_bindgen_class_marker(attr: TokenStream, input: TokenStream) -> TokenStream { + match macro_support::expand_class_marker(attr.into(), input.into()) { + Ok(tokens) => { + if cfg!(feature = "xxx_debug_only_print_generated_code") { + println!("{}", tokens); + } + tokens.into() + } + Err(diagnostic) => (quote! { #diagnostic }).into(), + } +} diff --git a/crates/macro/ui-tests/invalid-methods.rs b/crates/macro/ui-tests/invalid-methods.rs index 78e43bfa..915b16d2 100644 --- a/crates/macro/ui-tests/invalid-methods.rs +++ b/crates/macro/ui-tests/invalid-methods.rs @@ -36,8 +36,14 @@ impl A { x!(); // pub default fn foo() {} // TODO: compiler's pretty printer totally broken +} - +#[wasm_bindgen] +impl A { pub const fn foo() {} +} + +#[wasm_bindgen] +impl A { pub unsafe fn foo() {} } diff --git a/crates/macro/ui-tests/invalid-methods.stderr b/crates/macro/ui-tests/invalid-methods.stderr index 9ac1b887..b01dc7c8 100644 --- a/crates/macro/ui-tests/invalid-methods.stderr +++ b/crates/macro/ui-tests/invalid-methods.stderr @@ -47,15 +47,15 @@ error: macros in impls aren't supported | ^^^^^ error: can only #[wasm_bindgen] non-const functions - --> $DIR/invalid-methods.rs:41:9 + --> $DIR/invalid-methods.rs:43:9 | -41 | pub const fn foo() {} +43 | pub const fn foo() {} | ^^^^^ error: can only bindgen safe functions - --> $DIR/invalid-methods.rs:42:9 + --> $DIR/invalid-methods.rs:48:9 | -42 | pub unsafe fn foo() {} +48 | pub unsafe fn foo() {} | ^^^^^^ error: aborting due to 10 previous errors diff --git a/crates/macro/ui-tests/unused-attributes.rs b/crates/macro/ui-tests/unused-attributes.rs index cafb137b..bc5423ef 100644 --- a/crates/macro/ui-tests/unused-attributes.rs +++ b/crates/macro/ui-tests/unused-attributes.rs @@ -4,6 +4,8 @@ extern crate wasm_bindgen; use wasm_bindgen::prelude::*; +struct A; + #[wasm_bindgen] impl A { #[wasm_bindgen(method)] diff --git a/crates/macro/ui-tests/unused-attributes.stderr b/crates/macro/ui-tests/unused-attributes.stderr index 7ec4ad08..33f2602c 100644 --- a/crates/macro/ui-tests/unused-attributes.stderr +++ b/crates/macro/ui-tests/unused-attributes.stderr @@ -1,13 +1,13 @@ error: unused #[wasm_bindgen] attribute - --> $DIR/unused-attributes.rs:9:20 - | -9 | #[wasm_bindgen(method)] - | ^^^^^^ + --> $DIR/unused-attributes.rs:11:20 + | +11 | #[wasm_bindgen(method)] + | ^^^^^^ error: unused #[wasm_bindgen] attribute - --> $DIR/unused-attributes.rs:10:20 + --> $DIR/unused-attributes.rs:12:20 | -10 | #[wasm_bindgen(method)] +12 | #[wasm_bindgen(method)] | ^^^^^^ error: aborting due to 2 previous errors diff --git a/examples/webaudio/index.js b/examples/webaudio/index.js index 6ff2e02f..116689cb 100644 --- a/examples/webaudio/index.js +++ b/examples/webaudio/index.js @@ -19,21 +19,21 @@ import('./pkg/webaudio') const primary_slider = document.getElementById("primary_input"); primary_slider.addEventListener("input", event => { if (fm) { - fm.set_note(event.target.value); + fm.set_note(parseInt(event.target.value)); } }); const fm_freq = document.getElementById("fm_freq"); fm_freq.addEventListener("input", event => { if (fm) { - fm.set_fm_frequency(event.target.value); + fm.set_fm_frequency(parseFloat(event.target.value)); } }); const fm_amount = document.getElementById("fm_amount"); fm_amount.addEventListener("input", event => { if (fm) { - fm.set_fm_amount(event.target.value); + fm.set_fm_amount(parseFloat(event.target.value)); } }); }) diff --git a/guide/src/reference/browser-support.md b/guide/src/reference/browser-support.md index 7362ea61..c9f4c5b7 100644 --- a/guide/src/reference/browser-support.md +++ b/guide/src/reference/browser-support.md @@ -44,6 +44,10 @@ also like to be aware of it! }; ``` + **Warning:** doing this implies the polyfill will always be used, + even if native APIs are available. This has a very significant + performance impact (the polyfill was measured to be 100x slower in Chromium)! + 2. If you're not using a bundler you can also include support manually by adding a `