Skip to content

Commit

Permalink
Add support for version specifications
Browse files Browse the repository at this point in the history
This commit adds a `#[wasm_bindgen(version = "...")]` attribute support. This
information is eventually written into a `__wasm_pack_unstable` section.
Currently this is a strawman for the proposal in ashleygwilliams/wasm-pack#101
  • Loading branch information
alexcrichton committed Apr 26, 2018
1 parent d9a71b4 commit 412bebc
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 0 deletions.
31 changes: 31 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,37 @@ controlling precisely how imports are imported and what they map to in JS. This
section is intended to hopefully be an exhaustive reference of the
possibilities!

* `module` and `version` - we've seen `module` so far indicating where we can
import items from but `version` is also allowed:

```rust
#[wasm_bindgen(module = "moment", version = "^2.0.0")]
extern {
type Moment;
fn moment() -> Moment;
#[wasm_bindgen(method)]
fn format(this: &Moment) -> String;
}
```

The `module` key is used to configure the module that each item is imported
from. The `version` key does not affect the generated wasm itself but rather
it's an informative directive for tools like [wasm-pack]. Tools like wasm-pack
will generate a `package.json` for you and the `version` listed here, when
`module` is also an NPM package, will correspond to what to write down in
`package.json`.

In other words the usage of `module` as the name of an NPM package and
`version` as the version requirement allows you to, inline in Rust, depend on
the NPM ecosystem and import functionality from those packages. When bundled
with a tool like [wasm-pack] everything will automatically get wired up with
bundlers and you should be good to go!

Note that the `version` is *required* if `module` doesn't start with `./`. If
`module` starts with `./` then it is an error to provide a version.

[wasm-pack]: https://github.com/ashleygwilliams/wasm-pack

* `catch` - as we saw before the `catch` attribute allows catching a JS
exception. This can be attached to any imported function and the function must
return a `Result` where the `Err` payload is a `JsValue`, like so:
Expand Down
42 changes: 42 additions & 0 deletions crates/backend/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub struct Export {

pub struct Import {
pub module: Option<String>,
pub version: Option<String>,
pub js_namespace: Option<syn::Ident>,
pub kind: ImportKind,
}
Expand Down Expand Up @@ -294,6 +295,7 @@ impl Program {
BindgenAttrs::find(attrs)
};
let module = item_opts.module().or(opts.module()).map(|s| s.to_string());
let version = item_opts.version().or(opts.version()).map(|s| s.to_string());
let js_namespace = item_opts.js_namespace().or(opts.js_namespace());
let mut kind = match item {
syn::ForeignItem::Fn(f) => self.push_foreign_fn(f, item_opts),
Expand All @@ -304,6 +306,7 @@ impl Program {

self.imports.push(Import {
module,
version,
js_namespace,
kind,
});
Expand Down Expand Up @@ -584,8 +587,29 @@ impl Variant {

impl Import {
fn shared(&self) -> shared::Import {
match (&self.module, &self.version) {
(&Some(ref m), None) if m.starts_with("./") => {}
(&Some(ref m), &Some(_)) if m.starts_with("./") => {
panic!("when a module path starts with `./` that indicates \
that a local file is being imported so the `version` \
key cannot also be specified");
}
(&Some(_), &Some(_)) => {}
(&Some(_), &None) => {
panic!("when the `module` directive doesn't start with `./` \
then it's interpreted as an NPM package which requires \
a `version` to be specified as well, try using \
#[wasm_bindgen(module = \"...\", version = \"...\")]")
}
(&None, &Some(_)) => {
panic!("the #[wasm_bindgen(version = \"...\")] attribute can only \
be used when `module = \"...\"` is also specified");
}
(&None, &None) => {}
}
shared::Import {
module: self.module.clone(),
version: self.version.clone(),
js_namespace: self.js_namespace.map(|s| s.as_ref().to_string()),
kind: self.kind.shared(),
}
Expand Down Expand Up @@ -746,6 +770,16 @@ impl BindgenAttrs {
.next()
}

fn version(&self) -> Option<&str> {
self.attrs
.iter()
.filter_map(|a| match *a {
BindgenAttr::Version(ref s) => Some(&s[..]),
_ => None,
})
.next()
}

pub fn catch(&self) -> bool {
self.attrs.iter().any(|a| match *a {
BindgenAttr::Catch => true,
Expand Down Expand Up @@ -844,6 +878,7 @@ enum BindgenAttr {
Method,
JsNamespace(syn::Ident),
Module(String),
Version(String),
Getter(Option<syn::Ident>),
Setter(Option<syn::Ident>),
Structural,
Expand Down Expand Up @@ -897,6 +932,13 @@ impl syn::synom::Synom for BindgenAttr {
(s.value())
)=> { BindgenAttr::Module }
|
do_parse!(
call!(term, "version") >>
punct!(=) >>
s: syn!(syn::LitStr) >>
(s.value())
)=> { BindgenAttr::Version }
|
do_parse!(
call!(term, "js_name") >>
punct!(=) >>
Expand Down
2 changes: 2 additions & 0 deletions crates/cli-support/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Shared support for the wasm-bindgen-cli package, an internal dependency
base64 = "0.9"
failure = "0.1"
parity-wasm = "0.27"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
wasm-bindgen-shared = { path = "../shared", version = '=0.2.5' }
wasm-gc-api = "0.1"
Expand Down
60 changes: 60 additions & 0 deletions crates/cli-support/src/js/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::mem;
use failure::{Error, ResultExt};
use parity_wasm::elements::*;
use parity_wasm;
use serde_json;
use shared;
use wasm_gc;

Expand All @@ -29,6 +30,7 @@ pub struct Context<'a> {
pub exported_classes: HashMap<String, ExportedClass>,
pub function_table_needed: bool,
pub run_descriptor: &'a Fn(&str) -> Vec<u32>,
pub module_versions: Vec<(String, String)>,
}

#[derive(Default)]
Expand Down Expand Up @@ -341,6 +343,7 @@ impl<'a> Context<'a> {

self.export_table();
self.gc()?;
self.add_wasm_pack_section();

while js.contains("\n\n\n") {
js = js.replace("\n\n\n", "\n\n");
Expand Down Expand Up @@ -1333,6 +1336,28 @@ impl<'a> Context<'a> {
self.globals.push_str(s);
self.globals.push_str("\n");
}

fn add_wasm_pack_section(&mut self) {
if self.module_versions.len() == 0 {
return
}

#[derive(Serialize)]
struct WasmPackSchema<'a> {
version: &'a str,
modules: &'a [(String, String)],
}

let contents = serde_json::to_string(&WasmPackSchema {
version: "0.0.1",
modules: &self.module_versions,
}).unwrap();

let mut section = CustomSection::default();
*section.name_mut() = "__wasm_pack_unstable".to_string();
*section.payload_mut() = contents.into_bytes();
self.module.sections_mut().push(Section::Custom(section));
}
}

impl<'a, 'b> SubContext<'a, 'b> {
Expand Down Expand Up @@ -1423,6 +1448,7 @@ impl<'a, 'b> SubContext<'a, 'b> {
}

fn generate_import(&mut self, import: &shared::Import) -> Result<(), Error> {
self.validate_import_module(import)?;
match import.kind {
shared::ImportKind::Function(ref f) => {
self.generate_import_function(import, f)
Expand All @@ -1443,6 +1469,40 @@ impl<'a, 'b> SubContext<'a, 'b> {
Ok(())
}

fn validate_import_module(&mut self, import: &shared::Import)
-> Result<(), Error>
{
let version = match import.version {
Some(ref s) => s,
None => return Ok(()),
};
let module = match import.module {
Some(ref s) => s,
None => return Ok(()),
};
if module.starts_with("./") {
return Ok(())
}
let pkg = if module.starts_with("@") {
// Translate `@foo/bar/baz` to `@foo/bar` and `@foo/bar` to itself
let first_slash = match module.find('/') {
Some(i) => i,
None => {
bail!("packages starting with `@` must be of the form \
`@foo/bar`, but found: `{}`", module)
}
};
match module[first_slash + 1..].find('/') {
Some(i) => &module[..i],
None => module,
}
} else {
module.split('/').next().unwrap()
};
self.cx.module_versions.push((pkg.to_string(), version.clone()));
Ok(())
}

fn generate_import_static(
&mut self,
info: &shared::Import,
Expand Down
3 changes: 3 additions & 0 deletions crates/cli-support/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
extern crate parity_wasm;
extern crate wasm_bindgen_shared as shared;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
extern crate wasm_gc;
extern crate wasmi;
Expand Down Expand Up @@ -131,6 +133,7 @@ impl Bindgen {
config: &self,
module: &mut module,
function_table_needed: false,
module_versions: Default::default(),
run_descriptor: &|name| {
let mut v = MyExternals(Vec::new());
let ret = instance
Expand Down
1 change: 1 addition & 0 deletions crates/shared/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub struct Program {
#[derive(Deserialize, Serialize)]
pub struct Import {
pub module: Option<String>,
pub version: Option<String>,
pub js_namespace: Option<String>,
pub kind: ImportKind,
}
Expand Down
44 changes: 44 additions & 0 deletions tests/all/imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -351,3 +351,47 @@ fn rename() {
"#)
.test();
}

#[test]
fn versions() {
project()
.file("src/lib.rs", r#"
#![feature(proc_macro, wasm_custom_section, wasm_import_module)]
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;
#[wasm_bindgen(module = "webpack", version = "^0.2.0")]
extern {
fn foo();
}
#[wasm_bindgen]
pub fn run() {
foo();
}
"#)
.file("test.js", r#"
const fs = require("fs");
const assert = require("assert");
exports.test = function() {
const bytes = fs.readFileSync('out_bg.wasm');
const m = new WebAssembly.Module(bytes);
const name = '__wasm_pack_unstable';
const sections = WebAssembly.Module.customSections(m, name);
assert.strictEqual(sections.length, 1);
const b = new Uint8Array(sections[0]);
const buf = new Buffer(b);
const map = JSON.parse(buf.toString());
assert.deepStrictEqual(map, {
version: '0.0.1',
modules: [
['webpack', '^0.2.0']
]
});
};
"#)
.test();
}

0 comments on commit 412bebc

Please sign in to comment.