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
This commit is contained in:
Alex Crichton
2019-02-25 11:11:30 -08:00
parent f161717afe
commit b762948456
32 changed files with 985 additions and 380 deletions

View File

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

View File

@ -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<Dictionary>,
/// custom typescript sections to be included in the definition file
pub typescript_custom_sections: Vec<String>,
/// Inline JS snippets
pub inline_js: Vec<String>,
}
/// 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<String>,
pub module: ImportModule,
pub js_namespace: Option<Ident>,
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<H: Hasher>(&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 {

View File

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

View File

@ -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<Vec<u8>, Diagnostic> {
pub struct EncodeResult {
pub custom_section: Vec<u8>,
pub included_files: Vec<PathBuf>,
}
pub fn encode(program: &ast::Program) -> Result<EncodeResult, Diagnostic> {
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<HashMap<Ident, String>>,
strings: RefCell<HashSet<String>>,
files: RefCell<HashMap<String, LocalFile>>,
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::<Result<Vec<_>, _>>()?,
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<Import<'a>, 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)?,
})

View File

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

View File

@ -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<Option<&'a str>, HashMap<&'a str, String>>,
pub imported_names: HashMap<ImportModule<'a>, 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,19 +145,27 @@ impl<'a> Context<'a> {
self.globals.push_str(c);
self.typescript.push_str(c);
}
let global = if self.use_node_require() {
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 {
}
OutputMode::NoModules { .. } => {
if contents.starts_with("class") {
format!("{1}\n__exports.{0} = {0};\n", name, contents)
} else {
format!("__exports.{} = {};\n", name, contents)
}
} else {
}
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") {
@ -144,8 +173,34 @@ impl<'a> Context<'a> {
} 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 mem = self.module.memories.get(self.memory);
if mem.import.is_none() {
bail!("must impot a shared memory with threads")
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
));
}
// 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,25 +784,28 @@ impl<'a> Context<'a> {
memory.push_str(",shared:true");
}
memory.push_str("})");
self.imports_post.push_str("let memory;\n");
(
format!("memory = __exports.memory = maybe_memory;"),
format!("memory = __exports.memory = {};", memory),
)
} else {
(String::new(), String::new())
};
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;
{init_memory1}
result = WebAssembly.instantiate(module_or_path, imports)
.then(instance => {{
return {{ instance, module: module_or_path }}
return {{ instance, module: module_or_path }};
}});
}} else {{
memory = __exports.memory = {init_memory};
{init_memory2}
const response = fetch(module_or_path);
if (typeof WebAssembly.instantiateStreaming === 'function') {{
result = WebAssembly.instantiateStreaming(response, imports)
@ -715,119 +826,22 @@ impl<'a> Context<'a> {
}}
}}
return result.then(({{instance, module}}) => {{
wasm = init.wasm = instance.exports;
init.__wbindgen_wasm_instance = instance;
wasm = instance.exports;
init.__wbindgen_wasm_module = module;
init.__wbindgen_wasm_memory = __exports.memory;
{start}
return wasm;
}});
}};
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 {
init_memory1 = init_memory1,
init_memory2 = init_memory2,
start = if needs_manual_start {
"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 {
""
},
)
} 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,
)
};
while js.contains("\n\n\n") {
js = js.replace("\n\n\n", "\n\n");
}
Ok((js, self.typescript.clone()))
}
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<Import<'b>, 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,
},
None => Import::Global { name, field },
decode::ImportModule::Named(module) => Import::Module {
module,
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,
}

View File

@ -17,11 +17,7 @@ pub mod wasm2es6js;
pub struct Bindgen {
input: Input,
out_name: Option<String>,
nodejs: bool,
nodejs_experimental_modules: bool,
browser: bool,
no_modules: bool,
no_modules_global: Option<String>,
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,
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Ident>)),
(setter, Setter(Span, Option<Ident>)),
(indexing_getter, IndexingGetter(Span)),
@ -339,12 +340,12 @@ impl<'a> ConvertToAst<BindgenAttrs> for &'a mut syn::ItemStruct {
}
}
impl<'a> ConvertToAst<(BindgenAttrs, &'a Option<String>)> 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<String>),
(opts, module): (BindgenAttrs, &'a ast::ImportModule),
) -> Result<Self::Target, Diagnostic> {
let wasm = function_from_decl(
&self.ident,
@ -543,12 +544,12 @@ impl ConvertToAst<BindgenAttrs> for syn::ForeignItemType {
}
}
impl<'a> ConvertToAst<(BindgenAttrs, &'a Option<String>)> 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<String>),
(opts, module): (BindgenAttrs, &'a ast::ImportModule),
) -> Result<Self::Target, Diagnostic> {
if self.mutability.is_some() {
bail_span!(self.mutability, "cannot import mutable globals yet")
@ -1084,8 +1085,27 @@ impl MacroParse<BindgenAttrs> 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<BindgenAttrs> for syn::ItemForeignMod {
}
}
impl<'a> MacroParse<&'a BindgenAttrs> for syn::ForeignItem {
impl MacroParse<ast::ImportModule> 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)?,

View File

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

View File

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

View File

@ -14,16 +14,22 @@ macro_rules! shared_api {
imports: Vec<Import<'a>>,
structs: Vec<Struct<'a>>,
typescript_custom_sections: Vec<&'a str>,
// version: &'a str,
// schema_version: &'a str,
local_modules: Vec<LocalModule<'a>>,
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

View File

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

2
examples/raytrace-parallel/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
raytrace_parallel.js
raytrace_parallel_bg.wasm

View File

@ -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',
]

View File

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

View File

@ -0,0 +1,30 @@
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
</head>
<body>
<!-- Include the JS generated by `wasm-pack build` -->
<script src='pkg/without_a_bundler_no_modules.js'></script>
<script type=module>
// Like with the `--browser` output the exports are immediately available
// but they won't work until we initialize the module. Unlike `--browser`,
// however, the globals are all stored on a `wasm_bindgen` global. The
// global itself is the initialization function and then the properties of
// the global are all the exported functions.
//
// Note that the name `wasm_bindgen` can be configured with the
// `--no-modules-global` CLI flag
const { add } = wasm_bindgen;
async function run() {
await wasm_bindgen('./pkg/without_a_bundler_no_modules_bg.wasm');
const result = add(1, 2);
console.log(`1 + 2 = ${result}`);
}
run();
</script>
</body>
</html>

View File

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

View File

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

View File

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

View File

@ -3,33 +3,29 @@
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
</head>
<body>
<!--
This is the JS generated by the `wasm-pack` build command
The script here will define a `wasm_bindgen` global where all
functionality can be accessed such as instantiation and the actual
functions (examples below).
You can customize the name of the file here with the `out-name` CLI flag
to `wasm-bindgen`. You can also customize the name of the global exported
here with the `no-modules-global` flag.
-->
<script src='./pkg/without_a_bundler.js'></script>
<script>
// Import functionality from the wasm module, but note that it's not quite
// ready to be used just yet.
const { add } = wasm_bindgen;
<!-- Note the usage of `type=module` here as this is an ES6 module -->
<script type=module>
// Use ES module import syntax to import functionality from the module
// that we have compiled.
//
// Note that the `default` import is an initialization function which
// will "boot" the module and make it ready to use. Currently browsers
// don't support natively imported WebAssembly as an ES module, but
// eventually the manual initialization won't be required!
import { add, default as init } from './pkg/without_a_bundler.js';
async function run() {
// First up we need to actually load the wasm file, so we use the
// exported global to inform it where the wasm file is located on the
// server, and then we wait on the returned promies to wait for the
// default export to inform it where the wasm file is located on the
// server, and then we wait on the returned promise to wait for the
// wasm to be loaded.
//
// Note that instead of a string here you can also pass in an instance
// of `WebAssembly.Module` which allows you to compile your own module.
await wasm_bindgen('./pkg/without_a_bundler_bg.wasm');
// Also note that the promise, when resolved, yields the wasm module's
// exports which is the same as importing the `*_bg` module in other
// modes
await init('./pkg/without_a_bundler_bg.wasm');
// And afterwards we can use all the functionality defined in wasm.
const result = add(1, 2);

View File

@ -27,6 +27,7 @@
- [web-sys: A TODO MVC App](./examples/todomvc.md)
- [Reference](./reference/index.md)
- [Deployment](./reference/deployment.md)
- [JS snippets](./reference/js-snippets.md)
- [Passing Rust Closures to JS](./reference/passing-rust-closures-to-js.md)
- [Receiving JS Closures in Rust](./reference/receiving-js-closures-in-rust.md)
- [`Promise`s and `Future`s](./reference/js-promises-and-rust-futures.md)

View File

@ -4,12 +4,15 @@
[code]: https://github.com/rustwasm/wasm-bindgen/tree/master/examples/without-a-bundler
This example shows how the `--no-modules` flag can be used load code in a
This example shows how the `--browser` flag can be used load code in a
browser directly. For this deployment strategy bundlers like Webpack are not
required. For more information on deployment see the [dedicated
documentation][deployment].
First let's take a look at the code and see how when we're using `--no-modules`
> **Note**: the `--browser` flag is quite new to `wasm-bindgen`, and does not
> currently have support in `wasm-pack` yet. Support will be added soon though!
First let's take a look at the code and see how when we're using `--browser`
we're not actually losing any functionality!
```rust
@ -22,7 +25,33 @@ Otherwise the rest of the deployment magic happens in `index.html`:
{{#include ../../../examples/without-a-bundler/index.html}}
```
And that's it! Be sure to read up on the [deployment options][deployment] to see what it
means to deploy without a bundler.
And that's it! Be sure to read up on the [deployment options][deployment] to see
what it means to deploy without a bundler.
[deployment]: ../reference/deployment.html
## Using the older `--no-modules`
[View full source code][code]
[code]: https://github.com/rustwasm/wasm-bindgen/tree/master/examples/without-a-bundler-no-modules
The older version of using `wasm-bindgen` without a bundler is to use the
`--no-modules` flag to the `wasm-bindgen` CLI. This corresponds to `--target
no-modules` in `wasm-pack`.
While similar to the newer `--browser`, the `--no-modules` flag has a few
caveats:
* It does not support [local JS snippets][snippets]
* It does not generate an ES module
With that in mind the main difference is how the wasm/JS code is loaded, and
here's an example of loading the output of `wasm-pack` for the same module as
above.
```html
{{#include ../../../examples/without-a-bundler-no-modules/index.html}}
```
[snippets]: ../reference/js-snippets.html

View File

@ -29,26 +29,29 @@ necessary.
If you're not using a bundler but you're still running code in a web browser,
`wasm-bindgen` still supports this! For this use case you'll want to use the
`--no-modules` flag. You can check out a [full example][nomex] in the
`--browser` flag. You can check out a [full example][nomex] in the
documentation, but the highlights of this output are:
* When using `wasm-pack` you'll pass `--target no-modules`, and when using
`wasm-bindgen` directly you'll pass `--no-modules`.
* When using `wasm-bindgen` directly you'll pass `--browser`.
* The output can natively be included on a web page, and doesn't require any
further postprocessing.
* The `--no-modules` mode is not able to use NPM dependencies nor local JS
snippets (both currently [proposed][rfc1] [features][rfc2])
further postprocessing. The output is included as an ES module.
* The `--browser` mode is not able to use NPM dependencies.
* You'll want to review the [browser requirements] for `wasm-bindgen` because
no polyfills will be available.
> **Note**: currently `--browser` is not supported in `wasm-pack` because it is
> a very recent addition to `wasm-bindgen`, but support will be added soon!
[nomex]: ../examples/without-a-bundler.html
[rfc1]: https://github.com/rustwasm/rfcs/pull/6
[rfc2]: https://github.com/rustwasm/rfcs/pull/8
[browser requirements]: browser-support.html
Despite these limitations almost all code today is compatible with
`--no-modules`, but this area is actively being worked on to improve the
experience so the experience here may be tweaked over time!
The `wasm-bindgen` CLI also supports an output mode called `--no-modules` which
is similar to `--browser` in that it requires manual initialization of the wasm
and is intended to be included in web pages without any further postprocessing.
See the [without a bundler example][nomex] for some more information about
`--no-modules`, which corresponds to `--target no-modules` in `wasm-pack`.
## Node.js

View File

@ -0,0 +1,78 @@
# JS Snippets
Often when developing a crate you want to run on the web you'll want to include
some JS code here and there. While [`js-sys`](https://docs.rs/js-sys) and
[`web-sys`](https://docs.rs/web-sys) cover many needs they don't cover
everything, so `wasm-bindgen` supports the ability to write JS code next to your
Rust code and have it included in the final output artifact.
To include a local JS file, you'll use the `#[wasm_bindgen(module)]` macro:
```rust
#[wasm_bindgen(module = "/js/foo.js")]
extern "C" {
fn add(a: u32, b: u32) -> u32;
}
```
This declaration indicates that all the functions contained in the `extern`
block are imported from the file `/js/foo.js`, where the root is relative to the
crate root (where `Cargo.toml` is located).
The `/js/foo.js` file will make its way to the final output when `wasm-bindgen`
executes, so you can use the `module` annotation in a library without having to
worry users of your library!
The JS file itself must be written with ES module syntax:
```js
export function add(a, b) {
return a + b;
}
```
A full design of this feature can be found in [RFC 6] as well if you're
interested!
[RFC 6]: https://github.com/rustwasm/rfcs/pull/6
### Using `inline_js`
In addition to `module = "..."` if you're a macro author you also have the
ability to use the `inline_js` attribute:
```rust
#[wasm_bindgen(inline_js = "export function add(a, b) { return a + b; }")]
extern "C" {
fn add(a: u32, b: u32) -> u32;
}
```
Using `inline_js` indicates that the JS module is specified inline in the
attribute itself, and no files are loaded from the filesystem. They have the
same limitations and caveats as when using `module`, but can sometimes be easier
to generate for macros themselves. It's not recommended for hand-written code to
make use of `inline_js` but instead to leverage `module` where possible.
### Caveats
While quite useful local JS snippets currently suffer from a few caveats which
are important to be aware of. Many of these are temporary though!
* Currently `import` statements are not supported in the JS file. This is a
restriction we may lift in the future once we settle on a good way to support
this. For now, though, js snippets must be standalone modules and can't import
from anything else.
* Only `--browser` and the default bundler output mode are supported. To support
`--nodejs` we'd need to translate ES module syntax to CommonJS (this is
planned to be done, just hasn't been done yet). Additionally to support
`--no-modules` we'd have to similarly translate from ES modules to something
else.
* Paths in `module = "..."` must currently start with `/`, or be rooted at the
crate root. It is intended to eventually support relative paths like `./` and
`../`, but it's currently believed that this requires more support in
the Rust `proc_macro` crate.
As above, more detail about caveats can be found in [RFC 6].

2
tests/headless.rs → tests/headless/main.rs Executable file → Normal file
View File

@ -37,3 +37,5 @@ extern "C" {
fn can_log_html_strings() {
log("<script>alert('lol')</script>");
}
pub mod snippets;

View File

@ -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);
}

View File

@ -0,0 +1,3 @@
export function get_two() {
return 2;
}