Skip to content

Commit

Permalink
Enable nested namespace (rustwasm#951) (rustwasm#2105)
Browse files Browse the repository at this point in the history
* Enable nested namespace (rustwasm#951)

* Specify the namespace as array (rustwasm#951)

* added an example to the document

Co-authored-by: Alex Crichton <[email protected]>
  • Loading branch information
2 people authored and Perseus101 committed Jun 7, 2020
1 parent e0ad7bf commit f4eef6d
Show file tree
Hide file tree
Showing 11 changed files with 144 additions and 24 deletions.
2 changes: 1 addition & 1 deletion crates/backend/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ pub enum MethodSelf {
#[derive(Clone)]
pub struct Import {
pub module: ImportModule,
pub js_namespace: Option<Ident>,
pub js_namespace: Option<Vec<String>>,
pub kind: ImportKind,
}

Expand Down
31 changes: 17 additions & 14 deletions crates/backend/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::util::ShortHash;
use crate::Diagnostic;
use proc_macro2::{Ident, Literal, Span, TokenStream};
use quote::{quote, ToTokens};
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Mutex;
use syn;
Expand Down Expand Up @@ -32,28 +32,31 @@ impl TryToTokens for ast::Program {
for s in self.structs.iter() {
s.to_tokens(tokens);
}
let mut types = HashSet::new();
let mut types = HashMap::new();
for i in self.imports.iter() {
if let ast::ImportKind::Type(t) = &i.kind {
types.insert(t.rust_name.clone());
types.insert(t.rust_name.to_string(), t.rust_name.clone());
}
}
for i in self.imports.iter() {
DescribeImport { kind: &i.kind }.to_tokens(tokens);

// If there is a js namespace, check that name isn't a type. If it is,
// this import might be a method on that type.
if let Some(ns) = &i.js_namespace {
if types.contains(ns) && i.kind.fits_on_impl() {
let kind = match i.kind.try_to_token_stream() {
Ok(kind) => kind,
Err(e) => {
errors.push(e);
continue;
}
};
(quote! { impl #ns { #kind } }).to_tokens(tokens);
continue;
if let Some(nss) = &i.js_namespace {
// When the namespace is `A.B`, the type name should be `B`.
if let Some(ns) = nss.last().and_then(|t| types.get(t)) {
if i.kind.fits_on_impl() {
let kind = match i.kind.try_to_token_stream() {
Ok(kind) => kind,
Err(e) => {
errors.push(e);
continue;
}
};
(quote! { impl #ns { #kind } }).to_tokens(tokens);
continue;
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/backend/src/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ fn shared_import<'a>(i: &'a ast::Import, intern: &'a Interner) -> Result<Import<
ast::ImportModule::Inline(idx, _) => ImportModule::Inline(*idx as u32),
ast::ImportModule::None => ImportModule::None,
},
js_namespace: i.js_namespace.as_ref().map(|s| intern.intern(s)),
js_namespace: i.js_namespace.clone(),
kind: shared_import_kind(&i.kind, intern)?,
})
}
Expand Down
2 changes: 1 addition & 1 deletion crates/cli-support/src/js/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1998,7 +1998,7 @@ impl<'a> Context<'a> {

JsImportName::VendorPrefixed { name, prefixes } => {
self.imports_post.push_str("const l");
self.imports_post.push_str(&name);
self.imports_post.push_str(name);
self.imports_post.push_str(" = ");
switch(&mut self.imports_post, name, "", prefixes);
self.imports_post.push_str(";\n");
Expand Down
10 changes: 7 additions & 3 deletions crates/cli-support/src/wit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -898,7 +898,7 @@ impl<'a> Context<'a> {
"import of `{}` through js namespace `{}` isn't supported \
right now when it lists a polyfill",
item,
ns
ns.join(".")
);
}
return Ok(JsImport {
Expand All @@ -911,8 +911,12 @@ impl<'a> Context<'a> {
}

let (name, fields) = match import.js_namespace {
Some(ns) => (ns, vec![item.to_string()]),
None => (item, Vec::new()),
Some(ref ns) => {
let mut tail = (&ns[1..]).to_owned();
tail.push(item.to_string());
(ns[0].to_owned(), tail)
}
None => (item.to_owned(), Vec::new()),
};

let name = match import.module {
Expand Down
2 changes: 1 addition & 1 deletion crates/macro-support/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ extra-traits = ["syn/extra-traits"]
strict-macro = []

[dependencies]
syn = { version = '1.0.27', features = ['visit'] }
syn = { version = '1.0.27', features = ['visit', 'full'] }
quote = '1.0'
proc-macro2 = "1.0"
wasm-bindgen-backend = { path = "../backend", version = "=0.2.63" }
Expand Down
50 changes: 48 additions & 2 deletions crates/macro-support/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use quote::ToTokens;
use shared;
use syn;
use syn::parse::{Parse, ParseStream, Result as SynResult};
use syn::spanned::Spanned;

thread_local!(static ATTRS: AttributeParseState = Default::default());

Expand All @@ -34,7 +35,7 @@ macro_rules! attrgen {
(constructor, Constructor(Span)),
(method, Method(Span)),
(static_method_of, StaticMethodOf(Span, Ident)),
(js_namespace, JsNamespace(Span, Ident)),
(js_namespace, JsNamespace(Span, Vec<String>, Vec<Span>)),
(module, Module(Span, String, Span)),
(raw_module, RawModule(Span, String, Span)),
(inline_js, InlineJs(Span, String, Span)),
Expand Down Expand Up @@ -116,6 +117,21 @@ macro_rules! methods {
}
};

(@method $name:ident, $variant:ident(Span, Vec<String>, Vec<Span>)) => {
fn $name(&self) -> Option<(&[String], &[Span])> {
self.attrs
.iter()
.filter_map(|a| match &a.1 {
BindgenAttr::$variant(_, ss, spans) => {
a.0.set(true);
Some((&ss[..], &spans[..]))
}
_ => None,
})
.next()
}
};

(@method $name:ident, $variant:ident(Span, $($other:tt)*)) => {
#[allow(unused)]
fn $name(&self) -> Option<&$($other)*> {
Expand Down Expand Up @@ -280,6 +296,36 @@ impl Parse for BindgenAttr {
};
return Ok(BindgenAttr::$variant(attr_span, val, span))
});

(@parser $variant:ident(Span, Vec<String>, Vec<Span>)) => ({
input.parse::<Token![=]>()?;
let input_before_parse = input.fork();
let (vals, spans) = match input.parse::<syn::ExprArray>() {
Ok(exprs) => {
let mut vals = vec![];
let mut spans = vec![];

for expr in exprs.elems.iter() {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(ref str),
..
}) = expr {
vals.push(str.value());
spans.push(str.span());
} else {
return Err(syn::Error::new(expr.span(), "expected string literals"));
}
}

(vals, spans)
},
Err(_) => {
let ident = input_before_parse.parse::<AnyIdent>()?.0;
(vec![ident.to_string()], vec![ident.span()])
}
};
return Ok(BindgenAttr::$variant(attr_span, vals, spans))
});
}

attrgen!(parsers);
Expand Down Expand Up @@ -1270,7 +1316,7 @@ impl MacroParse<ast::ImportModule> for syn::ForeignItem {
};
BindgenAttrs::find(attrs)?
};
let js_namespace = item_opts.js_namespace().cloned();
let js_namespace = item_opts.js_namespace().map(|(s, _)| s.to_owned());
let kind = match self {
syn::ForeignItem::Fn(f) => f.convert((item_opts, &module))?,
syn::ForeignItem::Type(t) => t.convert(item_opts)?,
Expand Down
2 changes: 1 addition & 1 deletion crates/shared/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ macro_rules! shared_api {

struct Import<'a> {
module: ImportModule<'a>,
js_namespace: Option<&'a str>,
js_namespace: Option<Vec<String>>,
kind: ImportKind<'a>,
}

Expand Down
15 changes: 15 additions & 0 deletions guide/src/reference/attributes/on-js-imports/js_namespace.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,18 @@ Foo::new();
This is an example of how to bind namespaced items in Rust. The `log` and `Foo::new` functions will
be available in the Rust module and will be invoked as `console.log` and `new Bar.Foo` in
JavaScript.

It is also possible to access the JavaScript object under the nested namespace.
`js_namespace` also accepts the array of the string to specify the namespace.

```rust
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = ["window", "document"])]
fn write(s: &str);
}

write("hello, document!");
```

This example shows how to bind `window.document.write` in Rust.
28 changes: 28 additions & 0 deletions tests/wasm/import_class.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,31 @@ exports.StaticStructural = class {
return x + 3;
}
};

class InnerClass {
static inner_static_function(x) {
return x + 5;
}

static create_inner_instance() {
const ret = new InnerClass();
ret.internal_int = 3;
return ret;
}

get_internal_int() {
return this.internal_int;
}

append_to_internal_int(i) {
this.internal_int += i;
}

assert_internal_int(i) {
assert.strictEqual(this.internal_int, i);
}
}

exports.nestedNamespace = {
InnerClass: InnerClass
}
24 changes: 24 additions & 0 deletions tests/wasm/import_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,19 @@ extern "C" {
type StaticStructural;
#[wasm_bindgen(static_method_of = StaticStructural, structural)]
fn static_structural(a: u32) -> u32;

#[derive(Clone)]
type InnerClass;
#[wasm_bindgen(js_namespace = ["nestedNamespace", "InnerClass"])]
fn inner_static_function(a: u32) -> u32;
#[wasm_bindgen(js_namespace = ["nestedNamespace", "InnerClass"])]
fn create_inner_instance() -> InnerClass;
#[wasm_bindgen(method)]
fn get_internal_int(this: &InnerClass) -> u32;
#[wasm_bindgen(method)]
fn append_to_internal_int(this: &InnerClass, i: u32);
#[wasm_bindgen(method)]
fn assert_internal_int(this: &InnerClass, i: u32);
}

#[wasm_bindgen]
Expand Down Expand Up @@ -237,3 +250,14 @@ fn catch_constructors() {
fn static_structural() {
assert_eq!(StaticStructural::static_structural(30), 33);
}

#[wasm_bindgen_test]
fn nested_namespace() {
assert_eq!(InnerClass::inner_static_function(15), 20);

let f = InnerClass::create_inner_instance();
assert_eq!(f.get_internal_int(), 3);
assert_eq!(f.clone().get_internal_int(), 3);
f.append_to_internal_int(5);
f.assert_internal_int(8);
}

0 comments on commit f4eef6d

Please sign in to comment.