1
0
mirror of https://github.com/fluencelabs/wasm-bindgen synced 2025-05-10 12:58:13 +00:00

Fix conditional #[wasm_bindgen] in impls

Reported in  the fix requires us to get a bit creative I think. The
general gist is that a block like this:

    #[wasm_bindgen]
    impl Foo {
        pub fn foo() {}
    }

was previously expanded all in one go. Now, however, it's expanded into:

    impl Foo {
        #[__wasm_bindgen_class_marker(Foo = "Foo")]
        pub fn foo() {}
    }

    // goop generated by orginal #[wasm_bindgen]

This method of expansion takes advantage of rustc's recursive expansion
feature. It also allows us to expand `impl` blocks and allow inner items
to not be fully expanded yet, such as still having `#[cfg]` attributes
(like in the original bug report).

We use theinternal `__wasm_bindgen_class_marker` to indicate that we're
parsing an `ImplItemMethod` unconditionally, and then generation
proceeds as usual. The only final catch is that when we're expanding in
an `impl` block we have to generate tokens for the `Program`
(wasm-bindgen injected goop like the custom section) inside the body
of the function itself instead of next to it. Otherwise we'd get syntax
errors inside of impl blocks!

Closes 
This commit is contained in:
Alex Crichton 2019-01-25 14:31:50 -08:00
parent c56dff8ede
commit c35d6f4b0a
10 changed files with 197 additions and 55 deletions

@ -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<TokenStream, Diag
Ok(tokens)
}
/// Takes the parsed input from a `#[wasm_bindgen]` macro and returns the generated bindings
pub fn expand_class_marker(attr: TokenStream, input: TokenStream) -> Result<TokenStream, Diagnostic> {
parser::reset_attrs_used();
let mut item = syn::parse2::<syn::ImplItemMethod>(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<Self> {
let class = input.parse::<syn::Ident>()?;
input.parse::<Token![=]>()?;
let js_class = input.parse::<syn::LitStr>()?.value();
Ok(ClassMarker { class, js_class })
}
}

@ -796,7 +796,7 @@ impl<'a> MacroParse<(Option<BindgenAttrs>, &'a mut TokenStream)> for syn::Item {
}
impl<'a> MacroParse<BindgenAttrs> 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<BindgenAttrs> 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,15 +840,21 @@ impl<'a> MacroParse<BindgenAttrs> for &'a mut syn::ItemImpl {
}
}
impl<'a, 'b> MacroParse<&'a BindgenAttrs> for (&'a Ident, &'b mut syn::ImplItem) {
fn macro_parse(
self,
program: &mut ast::Program,
impl_opts: &'a BindgenAttrs,
// 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 (class, item) = self;
let method = match item {
syn::ImplItem::Method(ref mut m) => m,
syn::ImplItem::Method(m) => m,
syn::ImplItem::Const(_) => {
bail_span!(
&*item,
@ -864,53 +870,76 @@ impl<'a, 'b> MacroParse<&'a BindgenAttrs> for (&'a Ident, &'b mut syn::ImplItem)
"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?"),
};
match method.vis {
syn::Visibility::Public(_) => {}
_ => return Ok(()),
}
if method.defaultness.is_some() {
panic!("default methods are not supported");
}
if method.sig.constness.is_some() {
bail_span!(
method.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",);
}
let opts = BindgenAttrs::find(&mut method.attrs)?;
let comments = extract_doc_comments(&method.attrs);
let is_constructor = opts.constructor().is_some();
let (function, method_self) = function_from_decl(
&method.sig.ident,
&opts,
Box::new(method.sig.decl.clone()),
method.attrs.clone(),
method.vis.clone(),
true,
Some(class),
)?;
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,
(class, js_class): (&'a Ident, &'a str),
) -> Result<(), Diagnostic> {
match self.vis {
syn::Visibility::Public(_) => {}
_ => return Ok(()),
}
if self.defaultness.is_some() {
panic!("default methods are not supported");
}
if self.sig.constness.is_some() {
bail_span!(
self.sig.constness,
"can only #[wasm_bindgen] non-const functions",
);
}
if self.sig.unsafety.is_some() {
bail_span!(self.sig.unsafety, "can only bindgen safe functions",);
}
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(
&self.sig.ident,
&opts,
Box::new(self.sig.decl.clone()),
self.attrs.clone(),
self.vis.clone(),
true,
Some(class),
)?;
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(())

@ -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(),
}
}

@ -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() {}
}

@ -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

@ -4,6 +4,8 @@ extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;
struct A;
#[wasm_bindgen]
impl A {
#[wasm_bindgen(method)]

@ -1,13 +1,13 @@
error: unused #[wasm_bindgen] attribute
--> $DIR/unused-attributes.rs:9:20
--> $DIR/unused-attributes.rs:11:20
|
9 | #[wasm_bindgen(method)]
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

@ -37,6 +37,8 @@ macro_rules! if_std {
/// ```
pub mod prelude {
pub use wasm_bindgen_macro::wasm_bindgen;
#[doc(hidden)]
pub use wasm_bindgen_macro::__wasm_bindgen_class_marker;
pub use JsValue;
if_std! {

@ -143,3 +143,8 @@ exports.js_renamed_export = () => {
x.foo();
x.bar(x);
};
exports.js_conditional_bindings = () => {
const x = new wasm.ConditionalBindings();
x.free();
};

@ -22,6 +22,7 @@ extern "C" {
fn js_js_rename();
fn js_access_fields();
fn js_renamed_export();
fn js_conditional_bindings();
}
#[wasm_bindgen_test]
@ -402,3 +403,19 @@ impl RenamedExport {
fn renamed_export() {
js_renamed_export();
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
pub struct ConditionalBindings {
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
impl ConditionalBindings {
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(constructor))]
pub fn new() -> ConditionalBindings {
ConditionalBindings {}
}
}
#[wasm_bindgen_test]
fn conditional_bindings() {
js_conditional_bindings();
}