Skip to content

Commit b762948

Browse files
committed
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]: rustwasm/rfcs#6 Closes #1311
1 parent f161717 commit b762948

File tree

32 files changed

+984
-379
lines changed

32 files changed

+984
-379
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ members = [
8181
"examples/webaudio",
8282
"examples/webgl",
8383
"examples/without-a-bundler",
84+
"examples/without-a-bundler-no-modules",
8485
"tests/no-std",
8586
]
8687
exclude = ['crates/typescript']

crates/backend/src/ast.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use proc_macro2::{Ident, Span};
22
use shared;
33
use syn;
44
use Diagnostic;
5+
use std::hash::{Hash, Hasher};
56

67
/// An abstract syntax tree representing a rust program. Contains
78
/// extra information for joining up this rust code with javascript.
@@ -24,6 +25,8 @@ pub struct Program {
2425
pub dictionaries: Vec<Dictionary>,
2526
/// custom typescript sections to be included in the definition file
2627
pub typescript_custom_sections: Vec<String>,
28+
/// Inline JS snippets
29+
pub inline_js: Vec<String>,
2730
}
2831

2932
/// A rust to js interface. Allows interaction with rust objects/functions
@@ -66,11 +69,37 @@ pub enum MethodSelf {
6669
#[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))]
6770
#[derive(Clone)]
6871
pub struct Import {
69-
pub module: Option<String>,
72+
pub module: ImportModule,
7073
pub js_namespace: Option<Ident>,
7174
pub kind: ImportKind,
7275
}
7376

77+
#[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))]
78+
#[derive(Clone)]
79+
pub enum ImportModule {
80+
None,
81+
Named(String, Span),
82+
Inline(usize, Span),
83+
}
84+
85+
impl Hash for ImportModule {
86+
fn hash<H: Hasher>(&self, h: &mut H) {
87+
match self {
88+
ImportModule::None => {
89+
0u8.hash(h);
90+
}
91+
ImportModule::Named(name, _) => {
92+
1u8.hash(h);
93+
name.hash(h);
94+
}
95+
ImportModule::Inline(idx, _) => {
96+
2u8.hash(h);
97+
idx.hash(h);
98+
}
99+
}
100+
}
101+
}
102+
74103
#[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))]
75104
#[derive(Clone)]
76105
pub enum ImportKind {

crates/backend/src/codegen.rs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,25 +94,46 @@ impl TryToTokens for ast::Program {
9494
shared::SCHEMA_VERSION,
9595
shared::version()
9696
);
97+
let encoded = encode::encode(self)?;
9798
let mut bytes = Vec::new();
9899
bytes.push((prefix_json.len() >> 0) as u8);
99100
bytes.push((prefix_json.len() >> 8) as u8);
100101
bytes.push((prefix_json.len() >> 16) as u8);
101102
bytes.push((prefix_json.len() >> 24) as u8);
102103
bytes.extend_from_slice(prefix_json.as_bytes());
103-
bytes.extend_from_slice(&encode::encode(self)?);
104+
bytes.extend_from_slice(&encoded.custom_section);
104105

105106
let generated_static_length = bytes.len();
106107
let generated_static_value = syn::LitByteStr::new(&bytes, Span::call_site());
107108

109+
// We already consumed the contents of included files when generating
110+
// the custom section, but we want to make sure that updates to the
111+
// generated files will cause this macro to rerun incrementally. To do
112+
// that we use `include_str!` to force rustc to think it has a
113+
// dependency on these files. That way when the file changes Cargo will
114+
// automatically rerun rustc which will rerun this macro. Other than
115+
// this we don't actually need the results of the `include_str!`, so
116+
// it's just shoved into an anonymous static.
117+
let file_dependencies = encoded.included_files
118+
.iter()
119+
.map(|file| {
120+
let file = file.to_str().unwrap();
121+
quote! { include_str!(#file) }
122+
});
123+
108124
(quote! {
109125
#[allow(non_upper_case_globals)]
110126
#[cfg(target_arch = "wasm32")]
111127
#[link_section = "__wasm_bindgen_unstable"]
112128
#[doc(hidden)]
113129
#[allow(clippy::all)]
114-
pub static #generated_static_name: [u8; #generated_static_length] =
115-
*#generated_static_value;
130+
pub static #generated_static_name: [u8; #generated_static_length] = {
131+
#[doc(hidden)]
132+
static _INCLUDED_FILES: &[&str] = &[#(#file_dependencies),*];
133+
134+
*#generated_static_value
135+
};
136+
116137
})
117138
.to_tokens(tokens);
118139

crates/backend/src/encode.rs

Lines changed: 98 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,50 @@
1-
use std::cell::RefCell;
2-
use std::collections::HashMap;
3-
41
use proc_macro2::{Ident, Span};
2+
use std::cell::RefCell;
3+
use std::collections::{HashMap, HashSet};
4+
use std::env;
5+
use std::fs;
6+
use std::path::PathBuf;
7+
use util::ShortHash;
58

69
use ast;
710
use Diagnostic;
811

9-
pub fn encode(program: &ast::Program) -> Result<Vec<u8>, Diagnostic> {
12+
pub struct EncodeResult {
13+
pub custom_section: Vec<u8>,
14+
pub included_files: Vec<PathBuf>,
15+
}
16+
17+
pub fn encode(program: &ast::Program) -> Result<EncodeResult, Diagnostic> {
1018
let mut e = Encoder::new();
1119
let i = Interner::new();
1220
shared_program(program, &i)?.encode(&mut e);
13-
Ok(e.finish())
21+
let custom_section = e.finish();
22+
let included_files = i.files.borrow().values().map(|p| &p.path).cloned().collect();
23+
Ok(EncodeResult { custom_section, included_files })
1424
}
1525

1626
struct Interner {
1727
map: RefCell<HashMap<Ident, String>>,
28+
strings: RefCell<HashSet<String>>,
29+
files: RefCell<HashMap<String, LocalFile>>,
30+
root: PathBuf,
31+
crate_name: String,
32+
}
33+
34+
struct LocalFile {
35+
path: PathBuf,
36+
definition: Span,
37+
new_identifier: String,
1838
}
1939

2040
impl Interner {
2141
fn new() -> Interner {
2242
Interner {
2343
map: RefCell::new(HashMap::new()),
44+
strings: RefCell::new(HashSet::new()),
45+
files: RefCell::new(HashMap::new()),
46+
root: env::current_dir().unwrap(),
47+
crate_name: env::var("CARGO_PKG_NAME").unwrap(),
2448
}
2549
}
2650

@@ -34,7 +58,45 @@ impl Interner {
3458
}
3559

3660
fn intern_str(&self, s: &str) -> &str {
37-
self.intern(&Ident::new(s, Span::call_site()))
61+
let mut strings = self.strings.borrow_mut();
62+
if let Some(s) = strings.get(s) {
63+
return unsafe { &*(&**s as *const str) };
64+
}
65+
strings.insert(s.to_string());
66+
drop(strings);
67+
self.intern_str(s)
68+
}
69+
70+
/// Given an import to a local module `id` this generates a unique module id
71+
/// to assign to the contents of `id`.
72+
///
73+
/// Note that repeated invocations of this function will be memoized, so the
74+
/// same `id` will always return the same resulting unique `id`.
75+
fn resolve_import_module(&self, id: &str, span: Span) -> Result<&str, Diagnostic> {
76+
let mut files = self.files.borrow_mut();
77+
if let Some(file) = files.get(id) {
78+
return Ok(self.intern_str(&file.new_identifier))
79+
}
80+
let path = if id.starts_with("/") {
81+
self.root.join(&id[1..])
82+
} else if id.starts_with("./") || id.starts_with("../") {
83+
let msg = "relative module paths aren't supported yet";
84+
return Err(Diagnostic::span_error(span, msg))
85+
} else {
86+
return Ok(self.intern_str(&id))
87+
};
88+
89+
// Generate a unique ID which is somewhat readable as well, so mix in
90+
// the crate name, hash to make it unique, and then the original path.
91+
let new_identifier = format!("{}-{}{}", self.crate_name, ShortHash(0), id);
92+
let file = LocalFile {
93+
path,
94+
definition: span,
95+
new_identifier,
96+
};
97+
files.insert(id.to_string(), file);
98+
drop(files);
99+
self.resolve_import_module(id, span)
38100
}
39101
}
40102

@@ -64,8 +126,29 @@ fn shared_program<'a>(
64126
.iter()
65127
.map(|x| -> &'a str { &x })
66128
.collect(),
67-
// version: shared::version(),
68-
// schema_version: shared::SCHEMA_VERSION.to_string(),
129+
local_modules: intern
130+
.files
131+
.borrow()
132+
.values()
133+
.map(|file| {
134+
fs::read_to_string(&file.path)
135+
.map(|s| {
136+
LocalModule {
137+
identifier: intern.intern_str(&file.new_identifier),
138+
contents: intern.intern_str(&s),
139+
}
140+
})
141+
.map_err(|e| {
142+
let msg = format!("failed to read file `{}`: {}", file.path.display(), e);
143+
Diagnostic::span_error(file.definition, msg)
144+
})
145+
})
146+
.collect::<Result<Vec<_>, _>>()?,
147+
inline_js: prog
148+
.inline_js
149+
.iter()
150+
.map(|js| intern.intern_str(js))
151+
.collect(),
69152
})
70153
}
71154

@@ -111,7 +194,13 @@ fn shared_variant<'a>(v: &'a ast::Variant, intern: &'a Interner) -> EnumVariant<
111194

112195
fn shared_import<'a>(i: &'a ast::Import, intern: &'a Interner) -> Result<Import<'a>, Diagnostic> {
113196
Ok(Import {
114-
module: i.module.as_ref().map(|s| &**s),
197+
module: match &i.module {
198+
ast::ImportModule::Named(m, span) => {
199+
ImportModule::Named(intern.resolve_import_module(m, *span)?)
200+
}
201+
ast::ImportModule::Inline(idx, _) => ImportModule::Inline(*idx as u32),
202+
ast::ImportModule::None => ImportModule::None,
203+
},
115204
js_namespace: i.js_namespace.as_ref().map(|s| intern.intern(s)),
116205
kind: shared_import_kind(&i.kind, intern)?,
117206
})

crates/backend/src/util.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ pub fn ident_ty(ident: Ident) -> syn::Type {
9494

9595
pub fn wrap_import_function(function: ast::ImportFunction) -> ast::Import {
9696
ast::Import {
97-
module: None,
97+
module: ast::ImportModule::None,
9898
js_namespace: None,
9999
kind: ast::ImportKind::Function(function),
100100
}

0 commit comments

Comments
 (0)