Skip to content

Commit 1194a8e

Browse files
authored
Merge pull request #6 from alexcrichton/local-js-dependencies
RFC: Add support for local JS snippets in wasm-bindgen
2 parents 57b1dbf + 02c4e1e commit 1194a8e

File tree

1 file changed

+374
-0
lines changed

1 file changed

+374
-0
lines changed

text/006-local-js-dependencies.md

Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
1+
- Start Date: 2018-01-08
2+
- RFC PR: (leave this empty)
3+
- Tracking Issue: (leave this empty)
4+
5+
# Summary
6+
[summary]: #summary
7+
8+
Add the ability for `#[wasm_bindgen]` to process, load, and handle dependencies
9+
on local JS files.
10+
11+
* The `module` attribute can now be used to import files explicitly:
12+
13+
```rust
14+
#[wasm_bindgen(module = "/js/foo.js")]
15+
extern "C" {
16+
// ...
17+
}
18+
```
19+
20+
* The `inline_js` attribute can now be used to import JS modules inline:
21+
22+
```rust
23+
#[wasm_bindgen(inline_js = "export function foo() {}")]
24+
extern "C" {
25+
fn foo();
26+
}
27+
```
28+
29+
* The `--browser` flag is repurposed to generate an ES module for the browser
30+
and `--no-modules` is deprecated in favor of this flag.
31+
32+
* The `--nodejs` will not immediately support local JS snippets, but will do so
33+
in the future.
34+
35+
# Motivation
36+
[motivation]: #motivation
37+
38+
The goal of `wasm-bindgen` is to enable easy interoperation between Rust and JS.
39+
While it's very easy to write custom Rust code, it's actually pretty difficult
40+
to write custom JS and hook it up with `#[wasm_bindgen]` (see
41+
[rustwasm/wasm-bindgen#224][issue]). The `#[wasm_bindgen]`
42+
attribute currently only supports importing functions from ES modules, but even
43+
then the support is limited and simply assumes that the ES module string exists
44+
in the final application build step.
45+
46+
[issue]: https://github.com/rustwasm/wasm-bindgen/issues/224
47+
48+
Currently there is no composable way for a crate to have some auxiliary JS that
49+
it is built with which ends up seamlessly being included into a final built
50+
application. For example the `rand` crate can't easily include local JS (perhaps
51+
to detect what API for randomness it's supposed to use) without imposing strong
52+
requirements on the final artifact.
53+
54+
Ergonomically support imports from custom JS files also looks to be required by
55+
frameworks like `stdweb` to build a macro like `js!`. This involves generating
56+
snippets of JS at compile time which need to be included into the final bundle,
57+
which is intended to be powered by this new attribute.
58+
59+
# Stakeholders
60+
[stakeholders]: #stakeholders
61+
62+
Some major stakeholders in this RFC are:
63+
64+
* Users of `#[wasm_bindgen]`
65+
* Crate authors wishing to add wasm support to their crate.
66+
* The `stdweb` authors
67+
* Bundler (webpack) and `wasm-bindgen` integration folks.
68+
69+
Most of the various folks here will be cc'd onto the RFC, and reaching out to
70+
more is always welcome!
71+
72+
# Detailed Explanation
73+
[detailed-explanation]: #detailed-explanation
74+
75+
This proposal involves a number of moving pieces, all of which are intended to
76+
work in concert to provide a streamlined story to including local JS files into
77+
a final `#[wasm_bindgen]` artifact. We'll take a look at each piece at a time
78+
here.
79+
80+
### New Syntactical Features
81+
82+
The most user-facing change proposed here is the reinterpretation of the
83+
`module` attribute inside of `#[wasm_bindgen]` and the addition of an
84+
`inline_js` attribute. They can now be used to import local files and define
85+
local imports like so:
86+
87+
```rust
88+
#[wasm_bindgen(module = "/js/foo.js")]
89+
extern "C" {
90+
// ... definitions
91+
}
92+
93+
#[wasm_bindgen(inline_js = "export function foo() {}")]
94+
extern "C" {
95+
fn foo();
96+
}
97+
```
98+
99+
The first declaration says that the block of functions and types and such are
100+
all imported from the `/js/foo.js` file, relative to the current file and rooted
101+
at the crate root. The second declaration lists the JS inline as a string
102+
literal and the `extern` block describes the exports of the inline module.
103+
104+
The following rules are proposed for interpreting a `module` attribute.
105+
106+
* If the strings starts with the platform-specific representation of an absolute
107+
path to the cargo build directory (identified by `$OUT_DIR`) then the string
108+
is interpreted as a file path in the output directory. This is intended for
109+
build scripts which generate JS files as part of the build.
110+
111+
* If the string starts with `/`, `./`, or `../` then it's considered a path to a
112+
local file. If not, then it's passed through verbatim as the ES module import.
113+
114+
* All paths are resolved relative to the current file, like Rust's own
115+
`#[path]`, `include_str!`, etc. At this time, however, it's unknown how we'd
116+
actually do this for relative files. As a result all paths will be required to
117+
start with `/`. When `proc_macro` has a stable API (or we otherwise figure
118+
out how) we can start allowing `./` and `../`-prefixed paths.
119+
120+
This will hopefully roughly match what programmers expect as well as preexisting
121+
conventions in browsers and bundlers.
122+
123+
The `inline_js` attribute isn't really intended to be used for general-purpose
124+
development, but rather a way for procedural macros which can't currently today
125+
rely on the presence of `$OUT_DIR` to generate JS to import.
126+
127+
### Format of imported JS
128+
129+
All imported JS is required to written with ES module syntax. Initially the JS
130+
must be hand-written and cannot be postprocessed. For example the JS cannot be
131+
written with TypeScript, nor can it be compiled by Babel or similar.
132+
133+
As an example, a library may contain:
134+
135+
```rust
136+
// src/lib.rs
137+
#[wasm_bindgen(module = "/js/foo.js")]
138+
extern "C" {
139+
fn call_js();
140+
}
141+
```
142+
143+
accompanied with:
144+
145+
```js
146+
// js/foo.js
147+
148+
export function call_js() {
149+
// ...
150+
}
151+
```
152+
153+
Note that `js/foo.js` uses ES module syntax to export the function `call_js`.
154+
When `call_js` is called from Rust it will call the `call_js` function in
155+
`foo.js`.
156+
157+
### Propagation Through Dependencies
158+
159+
The purpose of the `file` attribute is to work seamlessly with dependencies.
160+
When building a project with `#[wasm_bindgen]` you shouldn't be required to know
161+
whether your dependencies are using local JS snippets or not!
162+
163+
The `#[wasm_bindgen]` macro, at compile time, will read the contents of the file
164+
provided, if any. This file will be serialized into the wasm-bindgen custom
165+
sections in a wasm-bindgen specific format. The final wasm artifact produced by
166+
rustc will contain all referenced JS file contents in its custom sections.
167+
168+
The `wasm-bindgen` CLI tool will extract all this JS and write it out to the
169+
filesystem. The wasm file (or the wasm-bindgen-generated shim JS file) emitted
170+
will import all the emitted JS files with relative imports.
171+
172+
### Updating `wasm-bindgen` output modes
173+
174+
The `wasm-bindgen` has a few modes of output generation today. These output
175+
modes are largely centered around modules vs no modules and how modules are
176+
defined. This RFC proposes that we move away from this moreso towards
177+
*environments*, such as node.js-compatible vs browser-compatible code (which
178+
involves more than only module format). This means that in cases where an
179+
environment supports multiple module systems, or the module system is optional
180+
(browsers support es modules and also no modules) `wasm-bindgen` will choose
181+
what module system it thinks is best as long as it is compatible with that
182+
environment.
183+
184+
The current output modes of `wasm-bindgen` are:
185+
186+
* **Default** - by default `wasm-bindgen` emits output that assumes the wasm
187+
module itself is an ES module. This will naturally work with custom JS
188+
snippets that are themselves ES modules, as they'll just be more modules in
189+
the graph all found in the local output directory. This output mode is
190+
currently only consumable by bundlers like Webpack, the default output cannot
191+
be loaded in either a web browser or Node.js.
192+
193+
* **`--no-modules`** - the `--no-modules` flag to `wasm-bindgen` is incompatible
194+
with ES modules because it's intended to be included via a `<script>` tag
195+
which is not a module. This mode, like today, will fail to work if upstream
196+
crates contain local JS snippets.
197+
198+
* **`--nodejs`** - this flag to `wasm-bindgen` indicates that the output should
199+
be tailored for Node.js, notably using CommonJS module conventions. In this
200+
mode `wasm-bindgen` will eventually use a JS parser in Rust to rewrite ES
201+
syntax of locally imported JS modules into CommonJS syntax.
202+
203+
* **`--browser`** - currently this flag is the same as the default output mode
204+
except that the output is tailored slightly for a browser environment (such as
205+
assuming that `TextEncoder` is ambiently available).
206+
207+
This RFC proposes
208+
repurposing this flag (breaking it) to instead generate an ES module natively
209+
loadable inside the web browser, but otherwise having a similar interface to
210+
`--no-modules` today, detailed below.
211+
212+
This RFC proposes rethinking these output modes as follows:
213+
214+
| Target Environment | CLI Flag | Module Format | User Experience | How are Local JS Snippets Loaded? |
215+
|-------------------------|-------------|---------------|------------------------------------------|----------------------------------------------------------------------------------------------|
216+
| Node.js without bundler | `--nodejs` | Common.js | `require()` the main JS glue file | Main JS glue file `require()`s crates' local JS snippets. |
217+
| Web without bundler | `--browser` | ES Modules | `<script>` pointing to main JS glue file, using `type=module` | `import` statements cause additional network requests for crates' local snippets. |
218+
| Web with bundler | none | ES Modules | `<script>` pointing to main JS glue file | Bundler links crates' local snippets into main JS glue file. No additional network requests except for the `wasm` module itself. |
219+
220+
It is notable that browser with and without bundler is almost the same as far
221+
as `wasm-bindgen` is concerned: the only difference is that if we assume a
222+
bundler, we can rely on the bundler polyfilling wasm-as-ES-module for us.
223+
Note the `--browser` here is relatively radically different today and as such
224+
would be a breaking change. It's thought that the usage of `--browser` is small
225+
enough that we can get away with this, but feedback is always welcome on this
226+
point!
227+
228+
The `--no-modules` flag doesn't really fit any more as the `--browser` use case
229+
is intended to subsume that. Note that the this RFC proposes only having the
230+
bundler-oriented and browser-oriented modes supporting local JS snippets for
231+
now, while paving a way forward to eventually support local JS snippets in
232+
Node.js. The `--no-modules` could eventually also be supported in the same
233+
manner as Node.js is (once we're parsing the JS file and rewriting the exports),
234+
but it's proposed here to generally move away from `--no-modules` towards
235+
`--browser`.
236+
237+
238+
The `--browser` output is currently considered to export an initialization
239+
function which, after called and the returned promise is resolved (like
240+
`--no-modules` today) will cause all exports to work when called. Before the
241+
promise resolves all exports will throw an error when called.
242+
243+
### JS files depending on other JS files
244+
245+
One tricky point about this RFC is when a local JS snippet depends on other JS
246+
files. For example your JS might look like:
247+
248+
```js
249+
// js/foo.js
250+
251+
import { foo } from '@some/npm-package';
252+
import { bar } from './bar.js'
253+
254+
// ...
255+
```
256+
257+
As designed above, these imports would not work. It's intended that we
258+
explicitly say this is an initial limitation of this design. We won't support
259+
imports between JS snippets just yet, but we should eventually be able to do so.
260+
261+
In the long run to support `--nodejs` we'll need some level of ES module parser
262+
for JS. Once we can parse the imports themselves it would be relatively
263+
straightforward for `#[wasm_bindgen]`, during expansion, to load transitively
264+
included files. For example in the file above we'd include `./bar.js` into the
265+
wasm custom section. In this future world we'd just rewrite `./bar.js` (if
266+
necessary) when the final output artifact is emitted. Additionally with NPM
267+
package support in `wasm-pack` and `wasm-bindgen` (a future goal) we could
268+
validate entries in `package.json` are present for imports found.
269+
270+
### Accessing wasm Memory/Table
271+
272+
JS snippets interacting with the wasm module may commonly need to work with the
273+
`WebAssembly.Memory` and `WebAssembly.Table` instances associated with the wasm
274+
module. This RFC proposes using the wasm itself to pass along these objects,
275+
like so:
276+
277+
```rust
278+
// lib.rs
279+
280+
#[wasm_bindgen(module = "/js/local-snippet.js")]
281+
extern {
282+
fn take_u8_slice(memory: &JsValue, ptr: u32, len: u32);
283+
}
284+
285+
#[wasm_bindgen]
286+
pub fn call_local_snippet() {
287+
let vec = vec![0,1,2,3,4];
288+
let mem = wasm_bindgen::memory();
289+
take_u8_slice(&mem, vec.as_ptr() as usize as u32, vec.len() as u32);
290+
}
291+
```
292+
293+
```js
294+
// js/local-snippet.js
295+
296+
export function take_u8_slice(memory, ptr, len) {
297+
let slice = new UInt8Array(memory.arrayBuffer, ptr, len);
298+
// ...
299+
}
300+
```
301+
302+
Here the `wasm_bindgen::memory()` existing intrinsic is used to pass along the
303+
memory object to the imported JS snippet. To mirror this we'll add
304+
`wasm_bindgen::function_table()` as well to the `wasm-bindgen` crate as an
305+
intrinsic to access the function table and return it as a `JsValue`.
306+
307+
Eventually we may want a more explicit way to import the memory/table, but for
308+
now this should be sufficient for expressiveness.
309+
310+
# Drawbacks
311+
[drawbacks]: #drawbacks
312+
313+
* The initial RFC is fairly conservative. It doesn't work with `--nodejs` out of
314+
the gate nor `--no-modules`. Additionally it doesn't support JS snippets
315+
importing other JS initially. Note that all of these are intended to be
316+
supported in the future, it's just thought that it may take more design than
317+
we need at the get-go for now.
318+
319+
* JS snippets must be written in vanilla ES module JS syntax. Common
320+
preprocessors like TypeScript can't be used. It's unclear how such
321+
preprocessed JS would be imported. It's hoped that JS snippets are small
322+
enough that this isn't too much of a problem. Larger JS snippets can always be
323+
extracted to an NPM package and postprocessed there. Note that it's always
324+
possible for authors to manually run the TypeScript compiler by hand for these
325+
use cases, though.
326+
327+
* The relatively popular `--no-modules` flag is proposed to be deprecated in
328+
favor of a `--browser` flag, which itself will have a breaking change relative
329+
to today. It's thought though that `--browser` is only very rarely used so is
330+
safe to break, and it's also thought that we'll want to avoid breaking
331+
`--no-modules` as-is today.
332+
333+
* Local JS snippets are required to be written in ES module syntax. This may be
334+
a somewhat opinionated stance, but it's intended to make it easier to add
335+
future features to `wasm-bindgen` while continuing to work with JS. The ES
336+
module system, however, is the only known official standard throughout the
337+
ecosystem, so it's hoped that this is a clear choice for writing local JS
338+
snippets.
339+
340+
# Rationale and Alternatives
341+
[alternatives]: #rationale-and-alternatives
342+
343+
The primary alternative to this system is a macro like `js!` from stdweb. This
344+
allows written small snippets of JS code directly in Rust code, and then
345+
`wasm-bindgen` would have the knowledge to generate appropriate shims. This RFC
346+
proposes recognizing `module` paths instead of this approach as it's thought to
347+
be a more general approach. Additionally it's intended that the `js!` macro can
348+
be built on the `module` directive including local file paths. The
349+
`wasm-bindgen` crate may grow a `js!`-like macro one day, but it's thought that
350+
it's best to start with a more conservative approach.
351+
352+
One alternative for ES modules is to simply concatenate all JS together. This
353+
way we wouldn't have to parse anything but we'd instead just throw everything
354+
into one file. The downside of this approach, however, is that it can easily
355+
lead to namespacing conflicts and it also forces everyone to agree on module
356+
formats and runs the risk of forcing the module format of the final product.
357+
358+
Another alternative to emitting small files at wasm-bindgen time is to instead
359+
unpack all files at *runtime* by leaving them in custom sections of the wasm
360+
executable. This in turn, however, may violate some CSP settings (particularly
361+
strict ones).
362+
363+
# Unresolved Questions
364+
[unresolved]: #unresolved-questions
365+
366+
- Is it necessary to support `--nodejs` initially?
367+
368+
- Is it necessary to support local JS imports in local JS snippets initially?
369+
370+
- Are there known parsers of JS ES modules today? Are we forced to include a
371+
full JS parser or can we have a minimal one which only deals with ES syntax?
372+
373+
- How would we handle other assets like CSS, HTML, or images that want to be
374+
referenced by the final wasm file?

0 commit comments

Comments
 (0)