From b762948456617ee263de8e43b3636bd3a4d1da75 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 25 Feb 2019 11:11:30 -0800 Subject: [PATCH] Implement the local JS snippets RFC This commit is an implementation of [RFC 6] which enables crates to inline local JS snippets into the final output artifact of `wasm-bindgen`. This is accompanied with a few minor breaking changes which are intended to be relatively minor in practice: * The `module` attribute disallows paths starting with `./` and `../`. It requires paths starting with `/` to actually exist on the filesystem. * The `--browser` flag no longer emits bundler-compatible code, but rather emits an ES module that can be natively loaded into a browser. Otherwise be sure to check out [the RFC][RFC 6] for more details, and otherwise this should implement at least the MVP version of the RFC! Notably at this time JS snippets with `--nodejs` or `--no-modules` are not supported and will unconditionally generate an error. [RFC 6]: https://github.com/rustwasm/rfcs/pull/6 Closes #1311 --- Cargo.toml | 1 + crates/backend/src/ast.rs | 31 +- crates/backend/src/codegen.rs | 27 +- crates/backend/src/encode.rs | 107 +++- crates/backend/src/util.rs | 2 +- crates/cli-support/src/js/mod.rs | 569 ++++++++++-------- crates/cli-support/src/lib.rs | 161 ++++- .../src/bin/wasm-bindgen-test-runner/main.rs | 3 +- .../bin/wasm-bindgen-test-runner/server.rs | 28 +- crates/cli/src/bin/wasm-bindgen.rs | 8 +- crates/macro-support/Cargo.toml | 1 + crates/macro-support/src/lib.rs | 4 +- crates/macro-support/src/parser.rs | 42 +- crates/macro/ui-tests/import-local.rs | 15 + crates/macro/ui-tests/import-local.stderr | 14 + crates/shared/src/lib.rs | 17 +- crates/webidl/src/lib.rs | 6 +- examples/raytrace-parallel/.gitignore | 2 + .../without-a-bundler-no-modules/Cargo.toml | 21 + .../without-a-bundler-no-modules/README.md | 13 + .../without-a-bundler-no-modules/index.html | 30 + .../without-a-bundler-no-modules/src/lib.rs | 24 + examples/without-a-bundler/README.md | 2 +- examples/without-a-bundler/build.sh | 15 + examples/without-a-bundler/index.html | 36 +- guide/src/SUMMARY.md | 1 + guide/src/examples/without-a-bundler.md | 37 +- guide/src/reference/deployment.md | 21 +- guide/src/reference/js-snippets.md | 78 +++ tests/{headless.rs => headless/main.rs} | 2 + tests/headless/snippets.rs | 42 ++ tests/headless/snippets1.js | 3 + 32 files changed, 984 insertions(+), 379 deletions(-) create mode 100644 crates/macro/ui-tests/import-local.rs create mode 100644 crates/macro/ui-tests/import-local.stderr create mode 100644 examples/raytrace-parallel/.gitignore create mode 100644 examples/without-a-bundler-no-modules/Cargo.toml create mode 100644 examples/without-a-bundler-no-modules/README.md create mode 100644 examples/without-a-bundler-no-modules/index.html create mode 100644 examples/without-a-bundler-no-modules/src/lib.rs create mode 100755 examples/without-a-bundler/build.sh create mode 100644 guide/src/reference/js-snippets.md rename tests/{headless.rs => headless/main.rs} (97%) mode change 100755 => 100644 create mode 100644 tests/headless/snippets.rs create mode 100644 tests/headless/snippets1.js diff --git a/Cargo.toml b/Cargo.toml index 513f10e9068..e9c92098950 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,6 +81,7 @@ members = [ "examples/webaudio", "examples/webgl", "examples/without-a-bundler", + "examples/without-a-bundler-no-modules", "tests/no-std", ] exclude = ['crates/typescript'] diff --git a/crates/backend/src/ast.rs b/crates/backend/src/ast.rs index 37dfbc61e7a..428fda8fb00 100644 --- a/crates/backend/src/ast.rs +++ b/crates/backend/src/ast.rs @@ -2,6 +2,7 @@ use proc_macro2::{Ident, Span}; use shared; use syn; use Diagnostic; +use std::hash::{Hash, Hasher}; /// An abstract syntax tree representing a rust program. Contains /// extra information for joining up this rust code with javascript. @@ -24,6 +25,8 @@ pub struct Program { pub dictionaries: Vec, /// custom typescript sections to be included in the definition file pub typescript_custom_sections: Vec, + /// Inline JS snippets + pub inline_js: Vec, } /// A rust to js interface. Allows interaction with rust objects/functions @@ -66,11 +69,37 @@ pub enum MethodSelf { #[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))] #[derive(Clone)] pub struct Import { - pub module: Option, + pub module: ImportModule, pub js_namespace: Option, pub kind: ImportKind, } +#[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))] +#[derive(Clone)] +pub enum ImportModule { + None, + Named(String, Span), + Inline(usize, Span), +} + +impl Hash for ImportModule { + fn hash(&self, h: &mut H) { + match self { + ImportModule::None => { + 0u8.hash(h); + } + ImportModule::Named(name, _) => { + 1u8.hash(h); + name.hash(h); + } + ImportModule::Inline(idx, _) => { + 2u8.hash(h); + idx.hash(h); + } + } + } +} + #[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))] #[derive(Clone)] pub enum ImportKind { diff --git a/crates/backend/src/codegen.rs b/crates/backend/src/codegen.rs index 7b0eef94c75..8919022541c 100644 --- a/crates/backend/src/codegen.rs +++ b/crates/backend/src/codegen.rs @@ -94,25 +94,46 @@ impl TryToTokens for ast::Program { shared::SCHEMA_VERSION, shared::version() ); + let encoded = encode::encode(self)?; let mut bytes = Vec::new(); bytes.push((prefix_json.len() >> 0) as u8); bytes.push((prefix_json.len() >> 8) as u8); bytes.push((prefix_json.len() >> 16) as u8); bytes.push((prefix_json.len() >> 24) as u8); bytes.extend_from_slice(prefix_json.as_bytes()); - bytes.extend_from_slice(&encode::encode(self)?); + bytes.extend_from_slice(&encoded.custom_section); let generated_static_length = bytes.len(); let generated_static_value = syn::LitByteStr::new(&bytes, Span::call_site()); + // We already consumed the contents of included files when generating + // the custom section, but we want to make sure that updates to the + // generated files will cause this macro to rerun incrementally. To do + // that we use `include_str!` to force rustc to think it has a + // dependency on these files. That way when the file changes Cargo will + // automatically rerun rustc which will rerun this macro. Other than + // this we don't actually need the results of the `include_str!`, so + // it's just shoved into an anonymous static. + let file_dependencies = encoded.included_files + .iter() + .map(|file| { + let file = file.to_str().unwrap(); + quote! { include_str!(#file) } + }); + (quote! { #[allow(non_upper_case_globals)] #[cfg(target_arch = "wasm32")] #[link_section = "__wasm_bindgen_unstable"] #[doc(hidden)] #[allow(clippy::all)] - pub static #generated_static_name: [u8; #generated_static_length] = - *#generated_static_value; + pub static #generated_static_name: [u8; #generated_static_length] = { + #[doc(hidden)] + static _INCLUDED_FILES: &[&str] = &[#(#file_dependencies),*]; + + *#generated_static_value + }; + }) .to_tokens(tokens); diff --git a/crates/backend/src/encode.rs b/crates/backend/src/encode.rs index adaec24d017..3b99f3f6df2 100644 --- a/crates/backend/src/encode.rs +++ b/crates/backend/src/encode.rs @@ -1,26 +1,50 @@ -use std::cell::RefCell; -use std::collections::HashMap; - use proc_macro2::{Ident, Span}; +use std::cell::RefCell; +use std::collections::{HashMap, HashSet}; +use std::env; +use std::fs; +use std::path::PathBuf; +use util::ShortHash; use ast; use Diagnostic; -pub fn encode(program: &ast::Program) -> Result, Diagnostic> { +pub struct EncodeResult { + pub custom_section: Vec, + pub included_files: Vec, +} + +pub fn encode(program: &ast::Program) -> Result { let mut e = Encoder::new(); let i = Interner::new(); shared_program(program, &i)?.encode(&mut e); - Ok(e.finish()) + let custom_section = e.finish(); + let included_files = i.files.borrow().values().map(|p| &p.path).cloned().collect(); + Ok(EncodeResult { custom_section, included_files }) } struct Interner { map: RefCell>, + strings: RefCell>, + files: RefCell>, + root: PathBuf, + crate_name: String, +} + +struct LocalFile { + path: PathBuf, + definition: Span, + new_identifier: String, } impl Interner { fn new() -> Interner { Interner { map: RefCell::new(HashMap::new()), + strings: RefCell::new(HashSet::new()), + files: RefCell::new(HashMap::new()), + root: env::current_dir().unwrap(), + crate_name: env::var("CARGO_PKG_NAME").unwrap(), } } @@ -34,7 +58,45 @@ impl Interner { } fn intern_str(&self, s: &str) -> &str { - self.intern(&Ident::new(s, Span::call_site())) + let mut strings = self.strings.borrow_mut(); + if let Some(s) = strings.get(s) { + return unsafe { &*(&**s as *const str) }; + } + strings.insert(s.to_string()); + drop(strings); + self.intern_str(s) + } + + /// Given an import to a local module `id` this generates a unique module id + /// to assign to the contents of `id`. + /// + /// Note that repeated invocations of this function will be memoized, so the + /// same `id` will always return the same resulting unique `id`. + fn resolve_import_module(&self, id: &str, span: Span) -> Result<&str, Diagnostic> { + let mut files = self.files.borrow_mut(); + if let Some(file) = files.get(id) { + return Ok(self.intern_str(&file.new_identifier)) + } + let path = if id.starts_with("/") { + self.root.join(&id[1..]) + } else if id.starts_with("./") || id.starts_with("../") { + let msg = "relative module paths aren't supported yet"; + return Err(Diagnostic::span_error(span, msg)) + } else { + return Ok(self.intern_str(&id)) + }; + + // Generate a unique ID which is somewhat readable as well, so mix in + // the crate name, hash to make it unique, and then the original path. + let new_identifier = format!("{}-{}{}", self.crate_name, ShortHash(0), id); + let file = LocalFile { + path, + definition: span, + new_identifier, + }; + files.insert(id.to_string(), file); + drop(files); + self.resolve_import_module(id, span) } } @@ -64,8 +126,29 @@ fn shared_program<'a>( .iter() .map(|x| -> &'a str { &x }) .collect(), - // version: shared::version(), - // schema_version: shared::SCHEMA_VERSION.to_string(), + local_modules: intern + .files + .borrow() + .values() + .map(|file| { + fs::read_to_string(&file.path) + .map(|s| { + LocalModule { + identifier: intern.intern_str(&file.new_identifier), + contents: intern.intern_str(&s), + } + }) + .map_err(|e| { + let msg = format!("failed to read file `{}`: {}", file.path.display(), e); + Diagnostic::span_error(file.definition, msg) + }) + }) + .collect::, _>>()?, + inline_js: prog + .inline_js + .iter() + .map(|js| intern.intern_str(js)) + .collect(), }) } @@ -111,7 +194,13 @@ fn shared_variant<'a>(v: &'a ast::Variant, intern: &'a Interner) -> EnumVariant< fn shared_import<'a>(i: &'a ast::Import, intern: &'a Interner) -> Result, Diagnostic> { Ok(Import { - module: i.module.as_ref().map(|s| &**s), + module: match &i.module { + ast::ImportModule::Named(m, span) => { + ImportModule::Named(intern.resolve_import_module(m, *span)?) + } + 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)), kind: shared_import_kind(&i.kind, intern)?, }) diff --git a/crates/backend/src/util.rs b/crates/backend/src/util.rs index 9c7eb06a245..2a9a9ce98b3 100644 --- a/crates/backend/src/util.rs +++ b/crates/backend/src/util.rs @@ -94,7 +94,7 @@ pub fn ident_ty(ident: Ident) -> syn::Type { pub fn wrap_import_function(function: ast::ImportFunction) -> ast::Import { ast::Import { - module: None, + module: ast::ImportModule::None, js_namespace: None, kind: ast::ImportKind::Function(function), } diff --git a/crates/cli-support/src/js/mod.rs b/crates/cli-support/src/js/mod.rs index b81cc609c9b..7e6efdea397 100644 --- a/crates/cli-support/src/js/mod.rs +++ b/crates/cli-support/src/js/mod.rs @@ -1,6 +1,6 @@ use crate::decode; use crate::descriptor::{Descriptor, VectorKind}; -use crate::{Bindgen, EncodeInto}; +use crate::{Bindgen, EncodeInto, OutputMode}; use failure::{bail, Error, ResultExt}; use std::collections::{HashMap, HashSet}; use walrus::{MemoryId, Module}; @@ -33,7 +33,7 @@ pub struct Context<'a> { /// from, `None` being the global module. The second key is a map of /// identifiers we've already imported from the module to what they're /// called locally. - pub imported_names: HashMap, HashMap<&'a str, String>>, + pub imported_names: HashMap, HashMap<&'a str, String>>, /// A set of all imported identifiers to the number of times they've been /// imported, used to generate new identifiers. @@ -53,6 +53,14 @@ pub struct Context<'a> { pub interpreter: &'a mut Interpreter, pub memory: MemoryId, + /// A map of all local modules we've found, from the identifier they're + /// known as to their actual JS contents. + pub local_modules: HashMap<&'a str, &'a str>, + + /// An integer offset of where to start assigning indexes to `inline_js` + /// snippets. This is incremented each time a `Program` is processed. + pub snippet_offset: usize, + pub anyref: wasm_bindgen_anyref_xform::Context, } @@ -100,6 +108,19 @@ enum Import<'a> { name: &'a str, field: Option<&'a str>, }, + /// Same as `Module`, except we're importing from a local module defined in + /// a local JS snippet. + LocalModule { + module: &'a str, + name: &'a str, + field: Option<&'a str>, + }, + /// Same as `Module`, except we're importing from an `inline_js` attribute + InlineJs { + idx: usize, + name: &'a str, + field: Option<&'a str>, + }, /// A global import which may have a number of vendor prefixes associated /// with it, like `webkitAudioPrefix`. The `name` is the name to test /// whether it's prefixed. @@ -124,28 +145,62 @@ impl<'a> Context<'a> { self.globals.push_str(c); self.typescript.push_str(c); } - let global = if self.use_node_require() { - if contents.starts_with("class") { - format!("{1}\nmodule.exports.{0} = {0};\n", name, contents) - } else { - format!("module.exports.{} = {};\n", name, contents) + let global = match self.config.mode { + OutputMode::Node { + experimental_modules: false, + } => { + if contents.starts_with("class") { + format!("{1}\nmodule.exports.{0} = {0};\n", name, contents) + } else { + format!("module.exports.{} = {};\n", name, contents) + } } - } else if self.config.no_modules { - if contents.starts_with("class") { - format!("{1}\n__exports.{0} = {0};\n", name, contents) - } else { - format!("__exports.{} = {};\n", name, contents) + OutputMode::NoModules { .. } => { + if contents.starts_with("class") { + format!("{1}\n__exports.{0} = {0};\n", name, contents) + } else { + format!("__exports.{} = {};\n", name, contents) + } } - } else { - if contents.starts_with("function") { - format!("export function {}{}\n", name, &contents[8..]) - } else if contents.starts_with("class") { - format!("export {}\n", contents) - } else { - format!("export const {} = {};\n", name, contents) + OutputMode::Bundler + | OutputMode::Node { + experimental_modules: true, + } => { + if contents.starts_with("function") { + format!("export function {}{}\n", name, &contents[8..]) + } else if contents.starts_with("class") { + format!("export {}\n", contents) + } else { + format!("export const {} = {};\n", name, contents) + } + } + OutputMode::Browser => { + // In browser mode there's no need to export the internals of + // wasm-bindgen as we're not using the module itself as the + // import object but rather the `__exports` map we'll be + // initializing below. + let export = if name.starts_with("__wbindgen") + || name.starts_with("__wbg_") + || name.starts_with("__widl_") + { + "" + } else { + "export " + }; + if contents.starts_with("function") { + format!("{}function {}{}\n", export, name, &contents[8..]) + } else if contents.starts_with("class") { + format!("{}{}\n", export, contents) + } else { + format!("{}const {} = {};\n", export, name, contents) + } } }; self.global(&global); + + if self.config.mode.browser() { + self.global(&format!("__exports.{} = {0};", name)); + } } fn require_internal_export(&mut self, name: &'static str) -> Result<(), Error> { @@ -529,7 +584,7 @@ impl<'a> Context<'a> { })?; self.bind("__wbindgen_module", &|me| { - if !me.config.no_modules { + if !me.config.mode.no_modules() && !me.config.mode.browser() { bail!( "`wasm_bindgen::module` is currently only supported with \ --no-modules" @@ -596,35 +651,14 @@ impl<'a> Context<'a> { // entirely. Otherwise we want to first add a start function to the // `start` section if one is specified. // - // Afterwards, we need to perform what's a bit of a hack. Right after we - // added the start function, we remove it again because no current - // strategy for bundlers and deployment works well enough with it. For - // `--no-modules` output we need to be sure to call the start function - // after our exports are wired up (or most imported functions won't - // work). - // - // For ESM outputs bundlers like webpack also don't work because - // currently they run the wasm initialization before the JS glue - // initialization, meaning that if the wasm start function calls - // imported functions the JS glue isn't ready to go just yet. - // - // To handle `--no-modules` we just unstart the start function and call - // it manually. To handle the ESM use case we switch the start function - // to calling an imported function which defers the start function via - // `Promise.resolve().then(...)` to execute on the next microtask tick. - let mut has_start_function = false; + // Note that once a start function is added, if any, we immediately + // un-start it. This is done because we require that the JS glue + // initializes first, so we execute wasm startup manually once the JS + // glue is all in place. + let mut needs_manual_start = false; if self.config.emit_start { self.add_start_function()?; - has_start_function = self.unstart_start_function(); - - // In the "we're pretending to be an ES module use case if we've got - // a start function then we use an injected shim to actually execute - // the real start function on the next tick of the microtask queue - // (explained above) - if !self.config.no_modules && has_start_function { - self.inject_start_shim(); - } - + needs_manual_start = self.unstart_start_function(); } self.export_table()?; @@ -653,20 +687,94 @@ impl<'a> Context<'a> { // we don't ask for items which we can no longer emit. drop(self.exposed_globals.take().unwrap()); - let mut js = if self.config.threads.is_some() { - // TODO: It's not clear right now how to best use threads with - // bundlers like webpack. We need a way to get the existing - // module/memory into web workers for now and we don't quite know - // idiomatically how to do that! In the meantime, always require - // `--no-modules` - if !self.config.no_modules { - bail!("most use `--no-modules` with threads for now") + let mut js = String::new(); + if self.config.mode.no_modules() { + js.push_str("(function() {\n"); + } + + // Depending on the output mode, generate necessary glue to actually + // import the wasm file in one way or another. + let mut init = String::new(); + match &self.config.mode { + // In `--no-modules` mode we need to both expose a name on the + // global object as well as generate our own custom start function. + OutputMode::NoModules { global } => { + js.push_str("const __exports = {};\n"); + js.push_str("let wasm;\n"); + init = self.gen_init(&module_name, needs_manual_start); + self.footer.push_str(&format!( + "self.{} = Object.assign(init, __exports);\n", + global + )); } - let mem = self.module.memories.get(self.memory); - if mem.import.is_none() { - bail!("must impot a shared memory with threads") + + // With normal CommonJS node we need to defer requiring the wasm + // until the end so most of our own exports are hooked up + OutputMode::Node { + experimental_modules: false, + } => { + self.footer + .push_str(&format!("wasm = require('./{}_bg');\n", module_name)); + if needs_manual_start { + self.footer.push_str("wasm.__wbindgen_start();\n"); + } + js.push_str("var wasm;\n"); + } + + // With Bundlers and modern ES6 support in Node we can simply import + // the wasm file as if it were an ES module and let the + // bundler/runtime take care of it. + OutputMode::Bundler + | OutputMode::Node { + experimental_modules: true, + } => { + js.push_str(&format!("import * as wasm from './{}_bg';\n", module_name)); + if needs_manual_start { + self.footer.push_str("wasm.__wbindgen_start();\n"); + } + } + + // With a browser-native output we're generating an ES module, but + // browsers don't support natively importing wasm right now so we + // expose the same initialization function as `--no-modules` as the + // default export of the module. + OutputMode::Browser => { + js.push_str("const __exports = {};\n"); + self.imports_post.push_str("let wasm;\n"); + init = self.gen_init(&module_name, needs_manual_start); + self.footer.push_str("export default init;\n"); } + } + + // Emit all the JS for importing all our functionality + js.push_str(&self.imports); + js.push_str("\n"); + js.push_str(&self.imports_post); + js.push_str("\n"); + // Emit all our exports from this module + js.push_str(&self.globals); + js.push_str("\n"); + + // Generate the initialization glue, if there was any + js.push_str(&init); + js.push_str("\n"); + js.push_str(&self.footer); + js.push_str("\n"); + if self.config.mode.no_modules() { + js.push_str("})();\n"); + } + + while js.contains("\n\n\n") { + js = js.replace("\n\n\n", "\n\n"); + } + + Ok((js, self.typescript.clone())) + } + + fn gen_init(&mut self, module_name: &str, needs_manual_start: bool) -> String { + let mem = self.module.memories.get(self.memory); + let (init_memory1, init_memory2) = if mem.import.is_some() { let mut memory = String::from("new WebAssembly.Memory({"); memory.push_str(&format!("initial:{}", mem.initial)); if let Some(max) = mem.maximum { @@ -676,158 +784,64 @@ impl<'a> Context<'a> { memory.push_str(",shared:true"); } memory.push_str("})"); - - format!( - "\ -(function() {{ - var wasm; - var memory; - const __exports = {{}}; - {globals} - function init(module_or_path, maybe_memory) {{ - let result; - const imports = {{ './{module}': __exports }}; - if (module_or_path instanceof WebAssembly.Module) {{ - memory = __exports.memory = maybe_memory; - result = WebAssembly.instantiate(module_or_path, imports) - .then(instance => {{ - return {{ instance, module: module_or_path }} - }}); - }} else {{ - memory = __exports.memory = {init_memory}; - const response = fetch(module_or_path); - if (typeof WebAssembly.instantiateStreaming === 'function') {{ - result = WebAssembly.instantiateStreaming(response, imports) - .catch(e => {{ - console.warn(\"`WebAssembly.instantiateStreaming` failed. Assuming this is \ - because your server does not serve wasm with \ - `application/wasm` MIME type. Falling back to \ - `WebAssembly.instantiate` which is slower. Original \ - error:\\n\", e); - return response - .then(r => r.arrayBuffer()) - .then(bytes => WebAssembly.instantiate(bytes, imports)); - }}); - }} else {{ - result = response - .then(r => r.arrayBuffer()) - .then(bytes => WebAssembly.instantiate(bytes, imports)); - }} - }} - return result.then(({{instance, module}}) => {{ - wasm = init.wasm = instance.exports; - init.__wbindgen_wasm_instance = instance; - init.__wbindgen_wasm_module = module; - init.__wbindgen_wasm_memory = __exports.memory; - {start} - }}); - }}; - self.{global_name} = Object.assign(init, __exports); -}})();", - globals = self.globals, - module = module_name, - global_name = self - .config - .no_modules_global - .as_ref() - .map(|s| &**s) - .unwrap_or("wasm_bindgen"), - init_memory = memory, - start = if has_start_function { - "wasm.__wbindgen_start();" - } else { - "" - }, - ) - } else if self.config.no_modules { - format!( - "\ -(function() {{ - var wasm; - const __exports = {{}}; - {globals} - function init(path_or_module) {{ - let instantiation; - const imports = {{ './{module}': __exports }}; - if (path_or_module instanceof WebAssembly.Module) {{ - instantiation = WebAssembly.instantiate(path_or_module, imports) - .then(instance => {{ - return {{ instance, module: path_or_module }} - }}); - }} else {{ - const data = fetch(path_or_module); - if (typeof WebAssembly.instantiateStreaming === 'function') {{ - instantiation = WebAssembly.instantiateStreaming(data, imports) - .catch(e => {{ - console.warn(\"`WebAssembly.instantiateStreaming` failed. Assuming this is \ - because your server does not serve wasm with \ - `application/wasm` MIME type. Falling back to \ - `WebAssembly.instantiate` which is slower. Original \ - error:\\n\", e); - return data - .then(r => r.arrayBuffer()) - .then(bytes => WebAssembly.instantiate(bytes, imports)); - }}); - }} else {{ - instantiation = data - .then(response => response.arrayBuffer()) - .then(buffer => WebAssembly.instantiate(buffer, imports)); - }} - }} - return instantiation.then(({{instance}}) => {{ - wasm = init.wasm = instance.exports; - {start} - }}); - }}; - self.{global_name} = Object.assign(init, __exports); -}})();", - globals = self.globals, - module = module_name, - global_name = self - .config - .no_modules_global - .as_ref() - .map(|s| &**s) - .unwrap_or("wasm_bindgen"), - start = if has_start_function { - "wasm.__wbindgen_start();" - } else { - "" - }, + self.imports_post.push_str("let memory;\n"); + ( + format!("memory = __exports.memory = maybe_memory;"), + format!("memory = __exports.memory = {};", memory), ) } else { - let import_wasm = if self.globals.len() == 0 { - String::new() - } else if self.use_node_require() { - self.footer - .push_str(&format!("wasm = require('./{}_bg');", module_name)); - format!("var wasm;") - } else { - format!("import * as wasm from './{}_bg';", module_name) - }; - - format!( - "\ - /* tslint:disable */\n\ - {import_wasm}\n\ - {imports}\n\ - {imports_post}\n\ - - {globals}\n\ - {footer}", - import_wasm = import_wasm, - globals = self.globals, - imports = self.imports, - imports_post = self.imports_post, - footer = self.footer, - ) + (String::new(), String::new()) }; - while js.contains("\n\n\n") { - js = js.replace("\n\n\n", "\n\n"); - } - - Ok((js, self.typescript.clone())) + format!( + "\ + function init(module_or_path, maybe_memory) {{ + let result; + const imports = {{ './{module}': __exports }}; + if (module_or_path instanceof WebAssembly.Module) {{ + {init_memory1} + result = WebAssembly.instantiate(module_or_path, imports) + .then(instance => {{ + return {{ instance, module: module_or_path }}; + }}); + }} else {{ + {init_memory2} + const response = fetch(module_or_path); + if (typeof WebAssembly.instantiateStreaming === 'function') {{ + result = WebAssembly.instantiateStreaming(response, imports) + .catch(e => {{ + console.warn(\"`WebAssembly.instantiateStreaming` failed. Assuming this is \ + because your server does not serve wasm with \ + `application/wasm` MIME type. Falling back to \ + `WebAssembly.instantiate` which is slower. Original \ + error:\\n\", e); + return response + .then(r => r.arrayBuffer()) + .then(bytes => WebAssembly.instantiate(bytes, imports)); + }}); + }} else {{ + result = response + .then(r => r.arrayBuffer()) + .then(bytes => WebAssembly.instantiate(bytes, imports)); + }} + }} + return result.then(({{instance, module}}) => {{ + wasm = instance.exports; + init.__wbindgen_wasm_module = module; + {start} + return wasm; + }}); + }} + ", + module = module_name, + init_memory1 = init_memory1, + init_memory2 = init_memory2, + start = if needs_manual_start { + "wasm.__wbindgen_start();" + } else { + "" + }, + ) } fn bind( @@ -1364,14 +1378,14 @@ impl<'a> Context<'a> { } fn expose_text_processor(&mut self, s: &str) { - if self.config.nodejs_experimental_modules { + if self.config.mode.nodejs_experimental_modules() { self.imports .push_str(&format!("import {{ {} }} from 'util';\n", s)); self.global(&format!("let cached{0} = new {0}('utf-8');", s)); - } else if self.config.nodejs { + } else if self.config.mode.nodejs() { self.global(&format!("const {0} = require('util').{0};", s)); self.global(&format!("let cached{0} = new {0}('utf-8');", s)); - } else if !(self.config.browser || self.config.no_modules) { + } else if !self.config.mode.always_run_in_browser() { self.global(&format!( " const l{0} = typeof {0} === 'undefined' ? \ @@ -2008,7 +2022,7 @@ impl<'a> Context<'a> { } fn use_node_require(&self) -> bool { - self.config.nodejs && !self.config.nodejs_experimental_modules + self.config.mode.nodejs() && !self.config.mode.nodejs_experimental_modules() } fn memory(&mut self) -> &'static str { @@ -2052,23 +2066,36 @@ impl<'a> Context<'a> { .or_insert_with(|| { let name = generate_identifier(import.name(), imported_identifiers); match &import { - Import::Module { module, .. } => { + Import::Module { .. } + | Import::LocalModule { .. } + | Import::InlineJs { .. } => { + // When doing a modular import local snippets (either + // inline or not) are routed to a local `./snippets` + // directory which the rest of `wasm-bindgen` will fill + // in. + let path = match import { + Import::Module { module, .. } => module.to_string(), + Import::LocalModule { module, .. } => format!("./snippets/{}", module), + Import::InlineJs { idx, .. } => { + format!("./snippets/wbg-inline{}.js", idx) + } + _ => unreachable!(), + }; if use_node_require { imports.push_str(&format!( "const {} = require(String.raw`{}`).{};\n", name, - module, + path, import.name() )); } else if import.name() == name { - imports - .push_str(&format!("import {{ {} }} from '{}';\n", name, module)); + imports.push_str(&format!("import {{ {} }} from '{}';\n", name, path)); } else { imports.push_str(&format!( "import {{ {} as {} }} from '{}';\n", import.name(), name, - module + path )); } name @@ -2290,24 +2317,6 @@ impl<'a> Context<'a> { true } - /// Injects a `start` function into the wasm module. This start function - /// calls a shim in the generated JS which defers the actual start function - /// to the next microtask tick of the event queue. - /// - /// See docs above at callsite for why this happens. - fn inject_start_shim(&mut self) { - let body = "function() { - Promise.resolve().then(() => wasm.__wbindgen_start()); - }"; - self.export("__wbindgen_defer_start", body, None); - let ty = self.module.types.add(&[], &[]); - let id = - self.module - .add_import_func("__wbindgen_placeholder__", "__wbindgen_defer_start", ty); - assert!(self.module.start.is_none()); - self.module.start = Some(id); - } - fn expose_anyref_table(&mut self) { assert!(self.config.anyref); if !self.should_write_global("anyref_table") { @@ -2368,6 +2377,14 @@ impl<'a> Context<'a> { impl<'a, 'b> SubContext<'a, 'b> { pub fn generate(&mut self) -> Result<(), Error> { + for m in self.program.local_modules.iter() { + // All local modules we find should be unique, so assert such. + assert!(self + .cx + .local_modules + .insert(m.identifier, m.contents) + .is_none()); + } for f in self.program.exports.iter() { self.generate_export(f).with_context(|_| { format!( @@ -2752,16 +2769,41 @@ impl<'a, 'b> SubContext<'a, 'b> { ) -> Result, Error> { // First up, imports don't work at all in `--no-modules` mode as we're // not sure how to import them. - if self.cx.config.no_modules { - if let Some(module) = &import.module { + let is_local_snippet = match import.module { + decode::ImportModule::Named(s) => self.cx.local_modules.contains_key(s), + decode::ImportModule::Inline(_) => true, + decode::ImportModule::None => false, + }; + if self.cx.config.mode.no_modules() { + if is_local_snippet { + bail!( + "local JS snippets are not supported with `--no-modules`; \ + use `--browser` or no flag instead", + ); + } + if let decode::ImportModule::Named(module) = &import.module { bail!( "import from `{}` module not allowed with `--no-modules`; \ - use `--nodejs` or `--browser` instead", + use `--nodejs`, `--browser`, or no flag instead", module ); } } + // FIXME: currently we require that local JS snippets are written in ES + // module syntax for imports/exports, but nodejs uses CommonJS to handle + // this meaning that local JS snippets are basically guaranteed to be + // incompatible. We need to implement a pass that translates the ES + // module syntax in the snippet to a CommonJS module, which is in theory + // not that hard but is a chunk of work to do. + if is_local_snippet && self.cx.config.mode.nodejs() { + bail!( + "local JS snippets are not supported with `--nodejs`; \ + see rustwasm/rfcs#6 for more details, but this restriction \ + will be lifted in the future" + ); + } + // Similar to `--no-modules`, only allow vendor prefixes basically for web // apis, shouldn't be necessary for things like npm packages or other // imported items. @@ -2769,7 +2811,15 @@ impl<'a, 'b> SubContext<'a, 'b> { if let Some(vendor_prefixes) = vendor_prefixes { assert!(vendor_prefixes.len() > 0); - if let Some(module) = &import.module { + if is_local_snippet { + bail!( + "local JS snippets do not support vendor prefixes for \ + the import of `{}` with a polyfill of `{}`", + item, + &vendor_prefixes[0] + ); + } + if let decode::ImportModule::Named(module) = &import.module { bail!( "import of `{}` from `{}` has a polyfill of `{}` listed, but vendor prefixes aren't supported when importing from modules", @@ -2792,20 +2842,28 @@ impl<'a, 'b> SubContext<'a, 'b> { }); } - let name = import.js_namespace.as_ref().map(|s| &**s).unwrap_or(item); - let field = if import.js_namespace.is_some() { - Some(item) - } else { - None + let (name, field) = match import.js_namespace { + Some(ns) => (ns, Some(item)), + None => (item, None), }; Ok(match import.module { - Some(module) => Import::Module { + decode::ImportModule::Named(module) if is_local_snippet => Import::LocalModule { + module, + name, + field, + }, + decode::ImportModule::Named(module) => Import::Module { module, name, field, }, - None => Import::Global { name, field }, + decode::ImportModule::Inline(idx) => Import::InlineJs { + idx: idx as usize + self.cx.snippet_offset, + name, + field, + }, + decode::ImportModule::None => Import::Global { name, field }, }) } @@ -2815,17 +2873,30 @@ impl<'a, 'b> SubContext<'a, 'b> { } } +#[derive(Hash, Eq, PartialEq)] +pub enum ImportModule<'a> { + Named(&'a str), + Inline(usize), + None, +} + impl<'a> Import<'a> { - fn module(&self) -> Option<&'a str> { + fn module(&self) -> ImportModule<'a> { match self { - Import::Module { module, .. } => Some(module), - _ => None, + Import::Module { module, .. } | Import::LocalModule { module, .. } => { + ImportModule::Named(module) + } + Import::InlineJs { idx, .. } => ImportModule::Inline(*idx), + Import::Global { .. } | Import::VendorPrefixed { .. } => ImportModule::None, } } fn field(&self) -> Option<&'a str> { match self { - Import::Module { field, .. } | Import::Global { field, .. } => *field, + Import::Module { field, .. } + | Import::LocalModule { field, .. } + | Import::InlineJs { field, .. } + | Import::Global { field, .. } => *field, Import::VendorPrefixed { .. } => None, } } @@ -2833,6 +2904,8 @@ impl<'a> Import<'a> { fn name(&self) -> &'a str { match self { Import::Module { name, .. } + | Import::LocalModule { name, .. } + | Import::InlineJs { name, .. } | Import::Global { name, .. } | Import::VendorPrefixed { name, .. } => *name, } diff --git a/crates/cli-support/src/lib.rs b/crates/cli-support/src/lib.rs index 6518c6fb0a3..9856880a248 100755 --- a/crates/cli-support/src/lib.rs +++ b/crates/cli-support/src/lib.rs @@ -17,11 +17,7 @@ pub mod wasm2es6js; pub struct Bindgen { input: Input, out_name: Option, - nodejs: bool, - nodejs_experimental_modules: bool, - browser: bool, - no_modules: bool, - no_modules_global: Option, + mode: OutputMode, debug: bool, typescript: bool, demangle: bool, @@ -39,6 +35,13 @@ pub struct Bindgen { encode_into: EncodeInto, } +enum OutputMode { + Bundler, + Browser, + NoModules { global: String }, + Node { experimental_modules: bool }, +} + enum Input { Path(PathBuf), Module(Module, String), @@ -56,11 +59,7 @@ impl Bindgen { Bindgen { input: Input::None, out_name: None, - nodejs: false, - nodejs_experimental_modules: false, - browser: false, - no_modules: false, - no_modules_global: None, + mode: OutputMode::Bundler, debug: false, typescript: false, demangle: true, @@ -92,29 +91,66 @@ impl Bindgen { return self; } - pub fn nodejs(&mut self, node: bool) -> &mut Bindgen { - self.nodejs = node; - self + fn switch_mode(&mut self, mode: OutputMode, flag: &str) -> Result<(), Error> { + match self.mode { + OutputMode::Bundler => self.mode = mode, + _ => bail!( + "cannot specify `{}` with another output mode already specified", + flag + ), + } + Ok(()) } - pub fn nodejs_experimental_modules(&mut self, node: bool) -> &mut Bindgen { - self.nodejs_experimental_modules = node; - self + pub fn nodejs(&mut self, node: bool) -> Result<&mut Bindgen, Error> { + if node { + self.switch_mode( + OutputMode::Node { + experimental_modules: false, + }, + "--nodejs", + )?; + } + Ok(self) } - pub fn browser(&mut self, browser: bool) -> &mut Bindgen { - self.browser = browser; - self + pub fn nodejs_experimental_modules(&mut self, node: bool) -> Result<&mut Bindgen, Error> { + if node { + self.switch_mode( + OutputMode::Node { + experimental_modules: true, + }, + "--nodejs-experimental-modules", + )?; + } + Ok(self) } - pub fn no_modules(&mut self, no_modules: bool) -> &mut Bindgen { - self.no_modules = no_modules; - self + pub fn browser(&mut self, browser: bool) -> Result<&mut Bindgen, Error> { + if browser { + self.switch_mode(OutputMode::Browser, "--browser")?; + } + Ok(self) } - pub fn no_modules_global(&mut self, name: &str) -> &mut Bindgen { - self.no_modules_global = Some(name.to_string()); - self + pub fn no_modules(&mut self, no_modules: bool) -> Result<&mut Bindgen, Error> { + if no_modules { + self.switch_mode( + OutputMode::NoModules { + global: "wasm_bindgen".to_string(), + }, + "--no-modules", + )?; + } + Ok(self) + } + + pub fn no_modules_global(&mut self, name: &str) -> Result<&mut Bindgen, Error> { + match &mut self.mode { + OutputMode::NoModules { global } => *global = name.to_string(), + _ => bail!("can only specify `--no-modules-global` with `--no-modules`"), + } + Ok(self) } pub fn debug(&mut self, debug: bool) -> &mut Bindgen { @@ -203,8 +239,10 @@ impl Bindgen { // a module's start function, if any, because we assume start functions // only show up when injected on behalf of wasm-bindgen's passes. if module.start.is_some() { - bail!("wasm-bindgen is currently incompatible with modules that \ - already have a start function"); + bail!( + "wasm-bindgen is currently incompatible with modules that \ + already have a start function" + ); } let mut program_storage = Vec::new(); @@ -263,8 +301,10 @@ impl Bindgen { imported_functions: Default::default(), imported_statics: Default::default(), direct_imports: Default::default(), + local_modules: Default::default(), start: None, anyref: Default::default(), + snippet_offset: 0, }; cx.anyref.enabled = self.anyref; cx.anyref.prepare(cx.module)?; @@ -275,11 +315,30 @@ impl Bindgen { vendor_prefixes: Default::default(), } .generate()?; + + for (i, js) in program.inline_js.iter().enumerate() { + let name = format!("wbg-inline{}.js", i + cx.snippet_offset); + let path = out_dir.join("snippets").join(name); + fs::create_dir_all(path.parent().unwrap())?; + fs::write(&path, js) + .with_context(|_| format!("failed to write `{}`", path.display()))?; + } + cx.snippet_offset += program.inline_js.len(); } + + // Write out all local JS snippets to the final destination now that + // we've collected them from all the programs. + for (path, contents) in cx.local_modules.iter() { + let path = out_dir.join("snippets").join(path); + fs::create_dir_all(path.parent().unwrap())?; + fs::write(&path, contents) + .with_context(|_| format!("failed to write `{}`", path.display()))?; + } + cx.finalize(stem)? }; - let extension = if self.nodejs_experimental_modules { + let extension = if self.mode.nodejs_experimental_modules() { "mjs" } else { "js" @@ -296,7 +355,7 @@ impl Bindgen { let wasm_path = out_dir.join(format!("{}_bg", stem)).with_extension("wasm"); - if self.nodejs { + if self.mode.nodejs() { let js_path = wasm_path.with_extension(extension); let shim = self.generate_node_wasm_import(&module, &wasm_path); fs::write(&js_path, shim) @@ -325,7 +384,7 @@ impl Bindgen { let mut shim = String::new(); - if self.nodejs_experimental_modules { + if self.mode.nodejs_experimental_modules() { for (i, module) in imports.iter().enumerate() { shim.push_str(&format!("import * as import{} from '{}';\n", i, module)); } @@ -357,7 +416,7 @@ impl Bindgen { } shim.push_str("let imports = {};\n"); for (i, module) in imports.iter().enumerate() { - if self.nodejs_experimental_modules { + if self.mode.nodejs_experimental_modules() { shim.push_str(&format!("imports['{}'] = import{};\n", module, i)); } else { shim.push_str(&format!("imports['{0}'] = require('{0}');\n", module)); @@ -371,7 +430,7 @@ impl Bindgen { ", )); - if self.nodejs_experimental_modules { + if self.mode.nodejs_experimental_modules() { for entry in m.exports.iter() { shim.push_str("export const "); shim.push_str(&entry.name); @@ -567,3 +626,41 @@ fn demangle(module: &mut Module) { } } } + +impl OutputMode { + fn nodejs_experimental_modules(&self) -> bool { + match self { + OutputMode::Node { experimental_modules } => *experimental_modules, + _ => false, + } + } + + fn nodejs(&self) -> bool { + match self { + OutputMode::Node { .. } => true, + _ => false, + } + } + + fn no_modules(&self) -> bool { + match self { + OutputMode::NoModules { .. } => true, + _ => false, + } + } + + fn always_run_in_browser(&self) -> bool { + match self { + OutputMode::Browser => true, + OutputMode::NoModules { .. } => true, + _ => false, + } + } + + fn browser(&self) -> bool { + match self { + OutputMode::Browser => true, + _ => false, + } + } +} diff --git a/crates/cli/src/bin/wasm-bindgen-test-runner/main.rs b/crates/cli/src/bin/wasm-bindgen-test-runner/main.rs index de840fd83c1..3865a4849ab 100644 --- a/crates/cli/src/bin/wasm-bindgen-test-runner/main.rs +++ b/crates/cli/src/bin/wasm-bindgen-test-runner/main.rs @@ -107,7 +107,8 @@ fn rmain() -> Result<(), Error> { shell.status("Executing bindgen..."); let mut b = Bindgen::new(); b.debug(debug) - .nodejs(node) + .nodejs(node)? + .browser(!node)? .input_module(module, wasm) .keep_debug(false) .emit_start(false) diff --git a/crates/cli/src/bin/wasm-bindgen-test-runner/server.rs b/crates/cli/src/bin/wasm-bindgen-test-runner/server.rs index e41959c8bfa..11dd124fd74 100644 --- a/crates/cli/src/bin/wasm-bindgen-test-runner/server.rs +++ b/crates/cli/src/bin/wasm-bindgen-test-runner/server.rs @@ -5,7 +5,6 @@ use std::path::Path; use failure::{format_err, Error, ResultExt}; use rouille::{Request, Response, Server}; -use wasm_bindgen_cli_support::wasm2es6js::Config; pub fn spawn( addr: &SocketAddr, @@ -23,9 +22,9 @@ pub fn spawn( __wbgtest_console_log, __wbgtest_console_info, __wbgtest_console_warn, - __wbgtest_console_error + __wbgtest_console_error, + default as init, }} from './{0}'; - import * as wasm from './{0}_bg'; // Now that we've gotten to the point where JS is executing, update our // status text as at this point we should be asynchronously fetching the @@ -33,9 +32,7 @@ pub fn spawn( document.getElementById('output').textContent = "Loading wasm module..."; async function main(test) {{ - // this is a facet of using wasm2es6js, a hack until browsers have - // native ESM support for wasm modules. - await wasm.booted; + const wasm = await init('./{0}_bg.wasm'); const cx = new Context(); window.on_console_debug = __wbgtest_console_debug; @@ -65,25 +62,6 @@ pub fn spawn( let js_path = tmpdir.join("run.js"); fs::write(&js_path, js_to_execute).context("failed to write JS file")?; - // No browser today supports a wasm file as ES modules natively, so we need - // to shim it. Use `wasm2es6js` here to fetch an appropriate URL and look - // like an ES module with the wasm module under the hood. - // - // TODO: don't reparse the wasm module here, should pass the - // `Module struct` directly from the output of - // `wasm-bindgen` previously here and avoid unnecessary - // parsing. - let wasm_name = format!("{}_bg.wasm", module); - let wasm = fs::read(tmpdir.join(&wasm_name))?; - let output = Config::new() - .fetch(Some(format!("/{}", wasm_name))) - .generate(&wasm)?; - let (js, wasm) = output.js_and_wasm()?; - let wasm = wasm.unwrap(); - fs::write(tmpdir.join(format!("{}_bg.js", module)), js).context("failed to write JS file")?; - fs::write(tmpdir.join(format!("{}_bg.wasm", module)), wasm) - .context("failed to write wasm file")?; - // For now, always run forever on this port. We may update this later! let tmpdir = tmpdir.to_path_buf(); let srv = Server::new(addr, move |request| { diff --git a/crates/cli/src/bin/wasm-bindgen.rs b/crates/cli/src/bin/wasm-bindgen.rs index be5e31d0ecf..4cea6086bac 100644 --- a/crates/cli/src/bin/wasm-bindgen.rs +++ b/crates/cli/src/bin/wasm-bindgen.rs @@ -88,9 +88,9 @@ fn rmain(args: &Args) -> Result<(), Error> { let mut b = Bindgen::new(); b.input_path(input) - .nodejs(args.flag_nodejs) - .browser(args.flag_browser) - .no_modules(args.flag_no_modules) + .nodejs(args.flag_nodejs)? + .browser(args.flag_browser)? + .no_modules(args.flag_no_modules)? .debug(args.flag_debug) .demangle(!args.flag_no_demangle) .keep_debug(args.flag_keep_debug) @@ -98,7 +98,7 @@ fn rmain(args: &Args) -> Result<(), Error> { .remove_producers_section(args.flag_remove_producers_section) .typescript(typescript); if let Some(ref name) = args.flag_no_modules_global { - b.no_modules_global(name); + b.no_modules_global(name)?; } if let Some(ref name) = args.flag_out_name { b.out_name(name); diff --git a/crates/macro-support/Cargo.toml b/crates/macro-support/Cargo.toml index 01f86a32bda..75117eb6aad 100644 --- a/crates/macro-support/Cargo.toml +++ b/crates/macro-support/Cargo.toml @@ -9,6 +9,7 @@ documentation = "https://docs.rs/wasm-bindgen" description = """ The part of the implementation of the `#[wasm_bindgen]` attribute that is not in the shared backend crate """ +edition = '2018' [features] spans = ["wasm-bindgen-backend/spans"] diff --git a/crates/macro-support/src/lib.rs b/crates/macro-support/src/lib.rs index cf807e86736..601a4731a48 100644 --- a/crates/macro-support/src/lib.rs +++ b/crates/macro-support/src/lib.rs @@ -12,8 +12,8 @@ extern crate wasm_bindgen_backend as backend; extern crate wasm_bindgen_shared as shared; use backend::{Diagnostic, TryToTokens}; -pub use parser::BindgenAttrs; -use parser::MacroParse; +pub use crate::parser::BindgenAttrs; +use crate::parser::MacroParse; use proc_macro2::TokenStream; use quote::ToTokens; use quote::TokenStreamExt; diff --git a/crates/macro-support/src/parser.rs b/crates/macro-support/src/parser.rs index b87f6d1a856..574af3c27ed 100644 --- a/crates/macro-support/src/parser.rs +++ b/crates/macro-support/src/parser.rs @@ -33,6 +33,7 @@ macro_rules! attrgen { (static_method_of, StaticMethodOf(Span, Ident)), (js_namespace, JsNamespace(Span, Ident)), (module, Module(Span, String, Span)), + (inline_js, InlineJs(Span, String, Span)), (getter, Getter(Span, Option)), (setter, Setter(Span, Option)), (indexing_getter, IndexingGetter(Span)), @@ -339,12 +340,12 @@ impl<'a> ConvertToAst for &'a mut syn::ItemStruct { } } -impl<'a> ConvertToAst<(BindgenAttrs, &'a Option)> for syn::ForeignItemFn { +impl<'a> ConvertToAst<(BindgenAttrs, &'a ast::ImportModule)> for syn::ForeignItemFn { type Target = ast::ImportKind; fn convert( self, - (opts, module): (BindgenAttrs, &'a Option), + (opts, module): (BindgenAttrs, &'a ast::ImportModule), ) -> Result { let wasm = function_from_decl( &self.ident, @@ -543,12 +544,12 @@ impl ConvertToAst for syn::ForeignItemType { } } -impl<'a> ConvertToAst<(BindgenAttrs, &'a Option)> for syn::ForeignItemStatic { +impl<'a> ConvertToAst<(BindgenAttrs, &'a ast::ImportModule)> for syn::ForeignItemStatic { type Target = ast::ImportKind; fn convert( self, - (opts, module): (BindgenAttrs, &'a Option), + (opts, module): (BindgenAttrs, &'a ast::ImportModule), ) -> Result { if self.mutability.is_some() { bail_span!(self.mutability, "cannot import mutable globals yet") @@ -1084,8 +1085,27 @@ impl MacroParse for syn::ItemForeignMod { )); } } - for mut item in self.items.into_iter() { - if let Err(e) = item.macro_parse(program, &opts) { + let module = match opts.module() { + Some((name, span)) => { + if opts.inline_js().is_some() { + let msg = "cannot specify both `module` and `inline_js`"; + errors.push(Diagnostic::span_error(span, msg)); + } + ast::ImportModule::Named(name.to_string(), span) + } + None => { + match opts.inline_js() { + Some((js, span)) => { + let i = program.inline_js.len(); + program.inline_js.push(js.to_string()); + ast::ImportModule::Inline(i, span) + } + None => ast::ImportModule::None + } + } + }; + for item in self.items.into_iter() { + if let Err(e) = item.macro_parse(program, module.clone()) { errors.push(e); } } @@ -1095,11 +1115,11 @@ impl MacroParse for syn::ItemForeignMod { } } -impl<'a> MacroParse<&'a BindgenAttrs> for syn::ForeignItem { +impl MacroParse for syn::ForeignItem { fn macro_parse( mut self, program: &mut ast::Program, - opts: &'a BindgenAttrs, + module: ast::ImportModule, ) -> Result<(), Diagnostic> { let item_opts = { let attrs = match self { @@ -1110,11 +1130,7 @@ impl<'a> MacroParse<&'a BindgenAttrs> for syn::ForeignItem { }; BindgenAttrs::find(attrs)? }; - let module = item_opts - .module() - .or(opts.module()) - .map(|s| s.0.to_string()); - let js_namespace = item_opts.js_namespace().or(opts.js_namespace()).cloned(); + let js_namespace = item_opts.js_namespace().cloned(); let kind = match self { syn::ForeignItem::Fn(f) => f.convert((item_opts, &module))?, syn::ForeignItem::Type(t) => t.convert(item_opts)?, diff --git a/crates/macro/ui-tests/import-local.rs b/crates/macro/ui-tests/import-local.rs new file mode 100644 index 00000000000..42613c3cadf --- /dev/null +++ b/crates/macro/ui-tests/import-local.rs @@ -0,0 +1,15 @@ +extern crate wasm_bindgen; + +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(module = "./foo.js")] +extern { + fn wut(); +} + +#[wasm_bindgen(module = "../foo.js")] +extern { + fn wut(); +} + +fn main() {} diff --git a/crates/macro/ui-tests/import-local.stderr b/crates/macro/ui-tests/import-local.stderr new file mode 100644 index 00000000000..8d0d0bc246f --- /dev/null +++ b/crates/macro/ui-tests/import-local.stderr @@ -0,0 +1,14 @@ +error: relative module paths aren't supported yet + --> $DIR/import-local.rs:5:25 + | +5 | #[wasm_bindgen(module = "./foo.js")] + | ^^^^^^^^^^ + +error: relative module paths aren't supported yet + --> $DIR/import-local.rs:10:25 + | +10 | #[wasm_bindgen(module = "../foo.js")] + | ^^^^^^^^^^^ + +error: aborting due to 2 previous errors + diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index 546620b5fc9..40084dafee7 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -14,16 +14,22 @@ macro_rules! shared_api { imports: Vec>, structs: Vec>, typescript_custom_sections: Vec<&'a str>, - // version: &'a str, - // schema_version: &'a str, + local_modules: Vec>, + inline_js: Vec<&'a str>, } struct Import<'a> { - module: Option<&'a str>, + module: ImportModule<'a>, js_namespace: Option<&'a str>, kind: ImportKind<'a>, } + enum ImportModule<'a> { + None, + Named(&'a str), + Inline(u32), + } + enum ImportKind<'a> { Function(ImportFunction<'a>), Static(ImportStatic<'a>), @@ -113,6 +119,11 @@ macro_rules! shared_api { readonly: bool, comments: Vec<&'a str>, } + + struct LocalModule<'a> { + identifier: &'a str, + contents: &'a str, + } } }; // end of mac case } // end of mac definition diff --git a/crates/webidl/src/lib.rs b/crates/webidl/src/lib.rs index eea08f7caee..1574ecb28d0 100644 --- a/crates/webidl/src/lib.rs +++ b/crates/webidl/src/lib.rs @@ -272,7 +272,7 @@ impl<'src> FirstPassRecord<'src> { ) { let variants = &enum_.values.body.list; program.imports.push(backend::ast::Import { - module: None, + module: backend::ast::ImportModule::None, js_namespace: None, kind: backend::ast::ImportKind::Enum(backend::ast::ImportEnum { vis: public(), @@ -463,7 +463,7 @@ impl<'src> FirstPassRecord<'src> { self.append_required_features_doc(&import_function, &mut doc, extra); import_function.doc_comment = doc; module.imports.push(backend::ast::Import { - module: None, + module: backend::ast::ImportModule::None, js_namespace: Some(raw_ident(self_name)), kind: backend::ast::ImportKind::Function(import_function), }); @@ -539,7 +539,7 @@ impl<'src> FirstPassRecord<'src> { import_type.doc_comment = doc_comment; program.imports.push(backend::ast::Import { - module: None, + module: backend::ast::ImportModule::None, js_namespace: None, kind: backend::ast::ImportKind::Type(import_type), }); diff --git a/examples/raytrace-parallel/.gitignore b/examples/raytrace-parallel/.gitignore new file mode 100644 index 00000000000..8b5555eb0cf --- /dev/null +++ b/examples/raytrace-parallel/.gitignore @@ -0,0 +1,2 @@ +raytrace_parallel.js +raytrace_parallel_bg.wasm diff --git a/examples/without-a-bundler-no-modules/Cargo.toml b/examples/without-a-bundler-no-modules/Cargo.toml new file mode 100644 index 00000000000..9c0bf1cbe70 --- /dev/null +++ b/examples/without-a-bundler-no-modules/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "without-a-bundler-no-modules" +version = "0.1.0" +authors = ["The wasm-bindgen Developers"] +edition = "2018" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wasm-bindgen = "0.2.37" + +[dependencies.web-sys] +version = "0.3.4" +features = [ + 'Document', + 'Element', + 'HtmlElement', + 'Node', + 'Window', +] diff --git a/examples/without-a-bundler-no-modules/README.md b/examples/without-a-bundler-no-modules/README.md new file mode 100644 index 00000000000..e986e752714 --- /dev/null +++ b/examples/without-a-bundler-no-modules/README.md @@ -0,0 +1,13 @@ +# Without a Bundler + +[View documentation for this example online][dox] + +[dox]: https://rustwasm.github.io/wasm-bindgen/examples/without-a-bundler.html + +You can build the example locally with: + +``` +$ wasm-pack build --target no-modules +``` + +and then opening `index.html` in a browser should run the example! diff --git a/examples/without-a-bundler-no-modules/index.html b/examples/without-a-bundler-no-modules/index.html new file mode 100644 index 00000000000..7d4e194abd1 --- /dev/null +++ b/examples/without-a-bundler-no-modules/index.html @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/examples/without-a-bundler-no-modules/src/lib.rs b/examples/without-a-bundler-no-modules/src/lib.rs new file mode 100644 index 00000000000..017c6615e9d --- /dev/null +++ b/examples/without-a-bundler-no-modules/src/lib.rs @@ -0,0 +1,24 @@ +use wasm_bindgen::prelude::*; + +// Called when the wasm module is instantiated +#[wasm_bindgen(start)] +pub fn main() -> Result<(), JsValue> { + // Use `web_sys`'s global `window` function to get a handle on the global + // window object. + let window = web_sys::window().expect("no global `window` exists"); + let document = window.document().expect("should have a document on window"); + let body = document.body().expect("document should have a body"); + + // Manufacture the element we're gonna append + let val = document.create_element("p")?; + val.set_inner_html("Hello from Rust!"); + + body.append_child(&val)?; + + Ok(()) +} + +#[wasm_bindgen] +pub fn add(a: u32, b: u32) -> u32 { + a + b +} diff --git a/examples/without-a-bundler/README.md b/examples/without-a-bundler/README.md index e986e752714..3bd88d71a87 100644 --- a/examples/without-a-bundler/README.md +++ b/examples/without-a-bundler/README.md @@ -7,7 +7,7 @@ You can build the example locally with: ``` -$ wasm-pack build --target no-modules +$ ./build.sh ``` and then opening `index.html` in a browser should run the example! diff --git a/examples/without-a-bundler/build.sh b/examples/without-a-bundler/build.sh new file mode 100755 index 00000000000..c79ec6145ef --- /dev/null +++ b/examples/without-a-bundler/build.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +set -ex + +# Note that typically we'd use `wasm-pack` to build the crate, but the +# `--browser` flag is very new to `wasm-bindgen` and as such doesn't have +# support in `wasm-pack` yet. Support will be added soon though! + +cargo build --target wasm32-unknown-unknown --release +cargo run --manifest-path ../../crates/cli/Cargo.toml \ + --bin wasm-bindgen -- \ + ../../target/wasm32-unknown-unknown/release/without_a_bundler.wasm --out-dir pkg \ + --browser + +python3 -m http.server diff --git a/examples/without-a-bundler/index.html b/examples/without-a-bundler/index.html index 1379e8b8205..08803b19c5d 100644 --- a/examples/without-a-bundler/index.html +++ b/examples/without-a-bundler/index.html @@ -3,33 +3,29 @@ - - - - "); } + +pub mod snippets; diff --git a/tests/headless/snippets.rs b/tests/headless/snippets.rs new file mode 100644 index 00000000000..f52b55046c1 --- /dev/null +++ b/tests/headless/snippets.rs @@ -0,0 +1,42 @@ +use wasm_bindgen::prelude::*; +use wasm_bindgen_test::*; + +#[wasm_bindgen(module = "/tests/headless/snippets1.js")] +extern { + fn get_two() -> u32; +} + +#[wasm_bindgen_test] +fn test_get_two() { + assert_eq!(get_two(), 2); +} + +#[wasm_bindgen(inline_js = "export function get_three() { return 3; }")] +extern { + fn get_three() -> u32; +} + +#[wasm_bindgen_test] +fn test_get_three() { + assert_eq!(get_three(), 3); +} + +#[wasm_bindgen(inline_js = "let a = 0; export function get() { a += 1; return a; }")] +extern { + #[wasm_bindgen(js_name = get)] + fn duplicate1() -> u32; +} + +#[wasm_bindgen(inline_js = "let a = 0; export function get() { a += 1; return a; }")] +extern { + #[wasm_bindgen(js_name = get)] + fn duplicate2() -> u32; +} + +#[wasm_bindgen_test] +fn duplicate_inline_not_unified() { + assert_eq!(duplicate1(), 1); + assert_eq!(duplicate2(), 1); + assert_eq!(duplicate1(), 2); + assert_eq!(duplicate2(), 2); +} diff --git a/tests/headless/snippets1.js b/tests/headless/snippets1.js new file mode 100644 index 00000000000..88975843d94 --- /dev/null +++ b/tests/headless/snippets1.js @@ -0,0 +1,3 @@ +export function get_two() { + return 2; +}