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