Rust and WebAssembly RFCs
Many changes, including bug fixes and documentation improvements can be implemented and reviewed via the normal GitHub pull request workflow.
Some changes though are "substantial", and we ask that these be put through a bit of a design process and produce a consensus among the Rust and WebAssembly community.
The "RFC" (request for comments) process is intended to provide a consistent and controlled path for substantial changes and additions to enter the Rust and WebAssembly ecosystem, so that all stakeholders can be confident about the direction the ecosystem is evolving in.
The RFC Process
When does a change require an RFC? How does an RFC get approved or rejected? What is the RFC life cycle?
These questions are answered in RFC 001.
License
This repository is currently in the process of being licensed under either of
- Apache License, Version 2.0, (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option. Some parts of the repository are already licensed according to those terms. For more see RFC 2044 and its tracking issue.
Contributions
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
- Start Date: 2018-06-28
- RFC PR: (leave this empty)
- Tracking Issue: (leave this empty)
Summary
Adopt a simplified version of the Rust RFC process to the Rust and WebAssembly domain working group. The RFC process will provide a single place to decide on substantial changes and additions across the ecosystem for all stakeholders.
Motivation
There are some decisions which have broad impact across the Rust and WebAssembly ecosystem, and therefore have many stakeholders who deserve to have a say in the decision and provide feedback on proposals and designs. Right now, these decisions tend to be made in whatever local repository pull request or issue tracker. This makes it difficult for stakeholders to stay on top of these decisions, because they need to watch many different places. For a repository owner or team, it is also difficult to determine whether the ecosystem is in favor of a feature or not.
After adopting this RFC process, stakeholders should have an easier time staying on top of substantial changes and features within the ecosystem. Additionally, the maintainers of a particular repository within the Rust and WebAssembly ecosystem should feel confident that they've solicited feedback from everyone involved after going through an RFC, and won't get angry bug reports from users who felt that they were not consulted. Everyone should have shared confidence in the direction that the ecosystem evolves in.
Detailed Explanation
Right now, governance for repositories within the rustwasm
organization
follow these rules describing policy for merging pull
requests:
Unless otherwise noted, each
rustwasm/*
repository has the following general policies:
All pull requests must be reviewed and approved of by at least one relevant team member or repository collaborator before merging.
Larger, more nuanced decisions about design, architecture, breaking changes, trade offs, etc are made by the relevant team and/or repository collaborators consensus. In other words, decisions on things that aren't straightforward improvements to or bug fixes for things that already exist in the project.
This policy categorizes pull requests as either "larger, more nuanced ..." changes or not (we will use "substantial" from now on). When a change is not substantial, it requires only a single team member approve of it. When a change is larger and more substantial, then the relevant team comes to consensus on how to proceed.
This RFC intends to further sub-categorize substantial changes into those that affect only maintenance of the repository itself, and are therefore only substantial internally to the maintainers, versus those substantial changes that have an impact on external users and the larger Rust and WebAssembly community. For internally substantial changes, we do not intend to change the current policy at all. For substantial changes that have external impact, we will adopt a lightweight version of Rust's RFC process.
When does a change need an RFC?
You need to follow the RFC process if you intend to make externally substantial
changes to any repository within the rustwasm
organization, or the RFC
process itself. What constitutes a "substantial" change is evolving based on
community norms and varies depending on what part of the ecosystem you are
proposing to change, but may include the following:
- The removal of or breaking changes to public APIs in widespread use.
- Public API additions that extend the public API in new ways (i.e. more than
"we implement
SomeTrait
forThisThing
, so also implementSomeTrait
forRelatedThing
").
Some changes do not require an RFC:
- Rephrasing, reorganizing, refactoring, or otherwise "changing shape does not change meaning".
- Additions that strictly improve objective, numerical quality criteria (warning removal, speedup, better platform coverage, more parallelism, trap more errors, etc.)
- Additions only likely to be noticed by other maintainers, and remain invisible to users.
If you submit a pull request to implement a new feature without going through the RFC process, it may be closed with a polite request to submit an RFC first.
The RFC process step by step
- Fork the RFC repository.
- Copy
000-template.md
totext/000-my-feature.md
(where "my-feature" is descriptive. Don't assign an RFC number yet). - Fill in the RFC. Put care into the details: RFCs that do not present convincing motivation, demonstrate understanding of the impact of the design, or are disingenuous about the drawbacks or alternatives tend to be poorly-received.
- Submit a pull request. As a pull request, the RFC will receive design feedback from the larger community, and the author should be prepared to revise it in response.
- Each new RFC pull request will be triaged in the next Rust and WebAssembly
domain working group meeting and assigned to one or more of the
@rustwasm/*
teams. - Build consensus and integrate feedback. RFCs that have broad support are much more likely to make progress than those that don't receive any comments. Feel free to reach out to the RFC assignee in particular to get help identifying stakeholders and obstacles.
- The team(s) will discuss the RFC pull request, as much as possible in the comment thread of the pull request itself. Offline discussion will be summarized on the pull request comment thread.
- RFCs rarely go through this process unchanged, especially as alternatives and drawbacks are shown. You can make edits, big and small, to the RFC to clarify or change the design, but make changes as new commits to the pull request, and leave a comment on the pull request explaining your changes. Specifically, do not squash or rebase commits after they are visible on the pull request.
- At some point, a member of the subteam will propose a "motion for final
comment period" (FCP), along with a disposition for the RFC (merge, close,
or postpone).
- This step is taken when enough of the tradeoffs have been discussed that the team(s) are in a position to make a decision. That does not require consensus amongst all participants in the RFC thread (which may be impossible). However, the argument supporting the disposition on the RFC needs to have already been clearly articulated, and there should not be a strong consensus against that position outside of the team(s). Team members use their best judgment in taking this step, and the FCP itself ensures there is ample time and notification for stakeholders to push back if it is made prematurely.
- For RFCs with lengthy discussion, the motion to FCP should be preceded by a summary comment trying to lay out the current state of the discussion and major tradeoffs/points of disagreement.
- Before actually entering FCP, all members of the team(s) must sign off; this is often the point at which many team members first review the RFC in full depth.
- The FCP lasts seven calendar days. It is also advertised widely, e.g. in an issue of "This Week in Rust and WebAssembly" on the Rust and WebAssembly blog. This way all stakeholders have a chance to lodge any final objections before a decision is reached.
- In most cases, the FCP period is quiet, and the RFC is either merged or closed. However, sometimes substantial new arguments or ideas are raised, the FCP is canceled, and the RFC goes back into development mode.
From RFC to implementation
Once an RFC is merged it becomes "active" then authors may implement it and submit the feature as a pull request to the relevant repositories. Being "active" is not a rubber stamp, and in particular still does not mean the feature will ultimately be merged; it does mean that in principle all the major stakeholders have agreed to the feature and are amenable to merging it.
Furthermore, the fact that a given RFC has been accepted and is "active" implies nothing about what priority is assigned to its implementation, nor does it imply anything about whether a developer has been assigned the task of implementing the feature. While it is not necessary that the author of the RFC also write the implementation, it is by far the most effective way to see an RFC through to completion: authors should not expect that other project developers will take on responsibility for implementing their accepted feature.
Modifications to "active" RFCs can be done in follow-up pull requests. We strive to write each RFC in a manner that it will reflect the final design of the feature; but the nature of the process means that we cannot expect every merged RFC to actually reflect what the end result will be at the time of the next major release.
In general, once accepted, RFCs should not be substantially changed. Only very minor changes should be submitted as amendments. More substantial changes should be new RFCs, with a note added to the original RFC.
Rationale and Alternatives
The design space for decision making is very large, from democratic to autocratic and more.
Forking and simplifying Rust's RFC process is practical. Rather than designing a decision making process from scratch, we take an existing one that works well and tailor it to our needs. Many Rust and WebAssembly stakeholders are already familiar with it.
The main differences from the Rust RFC process are:
- FCP lasts seven calendar days rather than ten. This reflects our desire for a lighter-weight process that moves more quickly than Rust's RFC process.
- The RFC template is shorter and merges together into single sections what were distinct sections in the Rust RFC template. Again, this reflects our desire for a lighter-weight process where we do not need to go into quite as much painstaking detail as Rust RFCs sometimes do (perhaps excluding this RFC).
The phases of RFC development and post-RFC implementation are largely the same as the Rust RFC process. We found that the motivations for nearly every phase of Rust's RFC process are equally motivating for the Rust and WebAssembly domain. We expected to simplify phases a lot, for example, we initially considered removing FCP and going straight to signing off on accepting an RFC or not. However, FCP exists as a way to (1) allow stakeholders to voice any final concerns that hadn't been brought up yet, and (2) help enforce the "no new rationale" rule. Both points apply equally well to the Rust and WebAssembly domain working group and ecosystem as they apply to Rust itself.
We can also avoid adopting an RFC process, and move more quickly by allowing each repository's team or owner to be dictators of their corner of the ecosystem. However, this will result in valuable feedback, opinions, and insight not getting voiced, and narrow decisions being made.
Unresolved Questions
-
Will we use
@rfcbot
? If we can, we probably should, but this can be decided separately from whether to accept this RFC. -
How to best advertise new RFCs and FCP? Should we make "This Week in Rust and WebAssembly" actually be weekly rather than every other week? The interaction between FCP length and frequency of TWiRaWA posting seems important.
- Start Date: 2018-07-10
- RFC PR: https://github.com/rustwasm/rfcs/pull/2
- Tracking Issue: https://github.com/rustwasm/wasm-bindgen/pull/640
Summary
Support defining single inheritance relationships in wasm-bindgen
's imported
types. Specifically, we define static upcasts from a derived type to one of its
base types, checked dynamic casts from a type to any other type using
JavaScript's instanceof
operator, and finally unchecked casts between any
JavaScript types as an escape hatch for developers. For the proc-macro frontend,
this is done by adding the #[wasm_bindgen(extends = Base)]
attribute to the
derived type. For the WebIDL frontend, WebIDL's existing interface inheritance
syntax is used.
Motivation
Prototype chains and ECMAScript classes allow JavaScript developers to define
single inheritance relationships between types. WebIDL interfaces can inherit
from one another, and Web APIs make widespread use of this
feature. We want to support calling base methods on an imported derived type and
passing an imported derived type to imported functions that expect a base type
in wasm-bindgen
. We want to support dynamically checking whether some JS value
is an instance of a JS class, and dynamically-checked casts. Finally, the same
way that unsafe
provides an encapsulatable escape hatch for Rust's ownership
and borrowing, we want to provide unchecked (but safe!) conversions between JS
classes and values.
Stakeholders
Anyone who is using wasm-bindgen
directly or transitively through the
web-sys
crate is affected. This does not affect the larger wasm ecosystem
outside of Rust (eg Webpack). Therefore, the usual advertisement of this RFC on
This Week in Rust and WebAssembly and at our working group meetings should
suffice in soliciting feedback.
Detailed Explanation
Example Usage
Consider the following JavaScript class definitions:
class MyBase { }
class MyDerived extends MyBase { }
We translate this into wasm-bindgen
proc-macro imports like this:
# #![allow(unused_variables)] #fn main() { #[wasm_bindgen] extern { pub extern type MyBase; #[wasm_bindgen(extends = MyBase)] pub extern type MyDerived; } #}
Note the #[wasm_bindgen(extends = MyBase)]
annotation on extern type MyDerived
. This tells wasm-bindgen
that MyDerived
inherits from MyBase
.
Alternatively, we could describe these same classes as WebIDL interfaces:
interface MyBase {}
interface MyDerived : MyBase {}
Example Upcasting
We can upcast into a MyBase
from a MyDerived
type using the normal From
,
AsRef
, AsMut
, and Into
conversions:
# #![allow(unused_variables)] #fn main() { let derived: MyDerived = get_derived_from_somewhere(); let base: MyBase = derived.into(); #}
Example Dynamically-Checked Casting
We can do dynamically-checked (checked with JavaScript's instanceof
operator)
downcasts from a MyBase
into a MyDerived
using the dyn_{into,ref,mut}
methods:
let base: MyBase = get_base_from_somewhere();
match base.dyn_into::<MyDerived>() {
Ok(derived) => {
// It was an instance of `MyDerived`!
}
Err(base) => {
// It was some other kind of instance of `MyBase`.
}
}
Example Unchecked Casting
If we really know that a MyBase
is an instance of MyDerived
and we don't
want to pay the cost of a dynamic check, we can also use unchecked conversions:
# #![allow(unused_variables)] #fn main() { let derived: MyDerived = get_derived_from_somewhere(); let base: MyBase = derived.into(); // We know that this is a `MyDerived` since we *just* converted it into `MyBase` // from `MyDerived` above. let derived: MyDerived = base.unchecked_into(); #}
Unchecked casting serves as an escape hatch for developers, and while it can lead to JavaScript exceptions, it cannot create memory unsafety.
The JsCast
Trait
For dynamically-checked and unchecked casting between arbitrary JavaScript
types, we introduce the JsCast
trait. It requires implementations provide a
boolean predicate that consults JavaScript's instanceof
operator, as well as
unchecked conversions from JavaScript values:
# #![allow(unused_variables)] #fn main() { pub trait JsCast { fn instanceof(val: &JsValue) -> bool; fn unchecked_from_js(val: JsValue) -> Self; fn unchecked_from_js_ref(val: &JsValue) -> &Self; fn unchecked_from_js_mut(val: &mut JsValue) -> &mut Self; // ... provided methods elided ... } #}
JsCast
's required trait methods are not intended to be used to directly, but
instead are leveraged by its provided methods. Users of wasm-bindgen
will be
able to ignore JsCast
's required trait methods for the most part, since the
implementations will be mechanically generated, and they will only be using the
required trait methods indirectly through the more ergonomic provided methods.
For every extern { type Illmatic; }
imported with wasm-bindgen
, we emit an
implementation of JsCast
similar to this:
# #![allow(unused_variables)] #fn main() { impl JsCast for Illmatic { fn instanceof(val: &JsValue) -> bool { #[cfg(all(target_arch = "wasm32", not(target_os = "emscripten")))] #[wasm_import_module = "__wbindgen_placeholder__"] extern { fn __wbindgen_instanceof_Illmatic(idx: u32) -> u32; } #[cfg(not(all(target_arch = "wasm32", not(target_os = "emscripten"))))] unsafe extern fn __wbindgen_instanceof_Illmatic(_: u32) -> u32 { panic!("function not implemented on non-wasm32 targets") } __wbindgen_instance_of_MyDerived(val.idx) == 1 } fn unchecked_from_js(val: JsValue) -> Illmatic { Illmatic { obj: val, } } fn unchecked_from_js_ref(val: &JsValue) -> &Illmatic { unsafe { &*(val as *const JsValue as *const Illmatic) } } fn unchecked_from_js_mut(val: &mut JsValue) -> &mut Illmatic { unsafe { &mut *(val as *mut JsValue as *mut Illmatic) } } } #}
Additionally, wasm-bindgen
will emit this JavaScript definition of
__wbindgen_instanceof_Illmatic
that simply wraps the JS instanceof
operator:
const __wbindgen_instanceof_Illmatic = function (idx) {
return getObject(idx) instanceof Illmatic;
};
JsCast
's Provided Trait Methods
The JsCast
trait's provided methods wrap the unergonomic required static trait
methods and provide ergonomic, chainable versions that operate on self
and
another T: JsCast
. For example, the JsCast::is_instance_of
method asks if
&self
is an instance of some other T
that also implements JsCast
.
# #![allow(unused_variables)] #fn main() { pub trait JsCast where Self: AsRef<JsValue> + AsMut<JsValue> + Into<JsValue>, { // ... required trait methods elided ... // Unchecked conversions from `Self` into some other `T: JsCast`. fn unchecked_into<T>(self) -> T where T: JsCast, { T::unchecked_from_js(self.into()) } fn unchecked_ref<T>(&self) -> &T where T: JsCast, { T::unchecked_from_js_ref(self.as_ref()) } fn unchecked_mut<T>(&mut self) -> &mut T where T: JsCast, { T::unchecked_from_js_mut(self.as_mut()) } // Predicate method to check whether `self` is an instance of `T` or not. fn is_instance_of<T>(&self) -> bool where T: JsCast, { T::instanceof(self.as_ref()) } // Dynamically-checked conversions from `Self` into some other `T: JsCast`. fn dyn_into<T>(self) -> Result<T, Self> where T: JsCast, { if self.is_instance_of::<T>() { Ok(self.unchecked_into()) } else { Err(self) } } fn dyn_ref<T>(&self) -> Option<&T> where T: JsCast, { if self.is_instance_of::<T>() { Some(self.unchecked_ref()) } else { None } } fn dyn_mut<T>(&mut self) -> Option<&mut T> where T: JsCast, { if self.is_instance_of::<T>() { Some(self.unchecked_mut()) } else { None } } } #}
Using these methods provides better turbo-fishing syntax than using JsCast
's
required trait methods directly.
# #![allow(unused_variables)] #fn main() { fn get_it() -> JsValue { ... } // Tired -_- SomeJsThing::unchecked_from_js(get_it()).method(); // Wired ^_^ get_it() .unchecked_into::<SomeJsThing>() .method(); #}
JsCast
Implementation for JsValue
We also trivially implement JsCast
for JsValue
with no-ops, and add
AsRef<JsValue>
and AsMut<JsValue>
implementations for JsValue
itself, so
that the JsCast
super trait bounds are satisfied:
# #![allow(unused_variables)] #fn main() { impl AsRef<JsValue> for JsValue { fn as_ref(&self) -> &JsValue { self } } impl AsMut<JsValue> for JsValue { fn as_mut(&mut self) -> &mut JsValue { self } } impl JsCast for JsValue { fn instanceof(_: &JsValue) -> bool { true } fn unchecked_from_js(val: JsValue) -> Self { val } fn unchecked_from_js_ref(val: &JsValue) -> &Self { val } fn unchecked_from_js_mut(val: &mut JsValue) -> &mut Self { val } } #}
Upcasting Implementation
For every extends = MyBase
on a type imported with extern type MyDerived
,
and for every base and derived interface in a WebIDL interface inheritance
chain, wasm-bindgen
will emit these trait implementations that wrap unchecked
conversions methods from JsCast
that we know are valid due to the inheritance
relationship:
-
A
From
implementation forself
-consuming conversions:# #![allow(unused_variables)] #fn main() { impl From<MyDerived> for MyBase { fn from(my_derived: MyDerived) -> MyBase { let val: JsValue = my_derived.into(); <MyDerived as JsCast>::unchecked_from_js(val) } } #}
-
An
AsRef
implementation for shared reference conversions:# #![allow(unused_variables)] #fn main() { impl AsRef<MyBase> for MyDerived { fn as_ref(&self) -> &MyDerived { let val: &JsValue = self.as_ref(); <MyDerived as JsCast>::uncheck_from_js_ref(val) } } #}
-
An
AsMut
implementation for exclusive reference conversions:# #![allow(unused_variables)] #fn main() { impl AsMut<MyBase> for MyDerived { fn as_mut(&mut self) -> &mut MyDerived { let val: &mut JsValue = self.as_mut(); <MyDerived as JsCast>::uncheck_from_js_mut(val) } } #}
Deep Inheritance Chains Example
For deeper inheritance chain, like this example:
class MyBase {}
class MyDerived extends MyBase {}
class MyDoubleDerived extends MyDerived {}
the proc-macro imports require an extends
attribute for every transitive base:
# #![allow(unused_variables)] #fn main() { #[wasm_bindgen] extern { pub extern type MyBase; #[wasm_bindgen(extends = MyBase)] pub extern type MyDerived; #[wasm_bindgen(extends = MyBase, extends = MyDerived)] pub extern type MyDoubleDerived; } #}
On the other hand, the WebIDL frontend can understand the full inheritance chain and nothing more than the usual interface inheritance syntax is required:
interface MyBase {}
interface MyDerived : MyBase {}
interface MyDoubleDerived : MyDerived {}
Given these definitions, we can upcast a MyDoubleDerived
all the way to a
MyBase
:
# #![allow(unused_variables)] #fn main() { let dub_derived: MyDoubleDerived = get_it_from_somewhere(); let base: MyBase = dub_derived.into(); #}
Drawbacks
- We might accidentally encourage using this inheritance instead of the more Rust-idiomatic usage of traits.
Rationale and Alternatives
-
We could define an
Upcast
trait instead of using the standardFrom
andAs{Ref,Mut}
traits. This would make it more clear that we are doing inheritance-related casts, but would also be a new trait that folks would have to understand vs pretty much every Rust programmer's familiarity with thestd
traits. -
Upcasting using the
From
andAs{Ref,Mut}
traits does not provide chainable, turbofishing methods onself
that one could use when type inference needs a helping hand. Instead, one must create a local variable with an explicit type.# #![allow(unused_variables)] #fn main() { // Can't do this with upcasting. get_some_js_type() .into::<AnotherJsType>() .method(); // Have to do this: let another: AnotherJsType = get_some_js_type().into(); another.method(); #}
If we used a custom
Upcast
trait, we could provide turbofishable methods onself
, at the cost of using non-standard traits. -
We could use
TryFrom
for dynamically-checked casts instead ofJsCast::dyn_into
et al. This would introduce a new nightly feature requirement when usingwasm-bindgen
. We leave the possibility open for whenTryFrom
is stabilized by not naming our dynamically-checked cast methodsJsCast::try_into
to be future compatible. -
Explicit upcasting still does not provide very good ergonomics. There are a couple things we could do here:
-
Use the
Deref
trait to hide upcasting. This is generally considered an anti-pattern. -
Automatically create a
MyBaseMethods
trait for base types that contain all the base type's methods and implement that trait forMyBase
andMyDerived
? Also emit aMyDerivedMethods
trait that requiresMyBase
as a super trait, representing the inheritance at the trait level? This is the Rust-y thing to do and allows us to write generic functions with trait bounds. This is whatstdweb
does with theIHTMLElement
trait forHTMLElement
's methods.Whether we do this or not also happens to be orthogonal to casting between base and derived types. We leave exploring this design space to follow up RFCs, and hope to land just the casting in an incremental fashion.
-
-
Traits sometimes get in the way of learning what one can do with a thing. They aren't as up-front in the generated documentation, and can lead people to thinking they must write code that is generic over a trait when it isn't necessary. There are two ways we could get rid of the
JsCast
trait:-
Only implement its methods on
JsValue
and require that conversions likeImportedJsClassUno
->ImportedJsClassDos
go toJsValue
in between:ImportedJsClassUno
->JsValue
->ImpiortedJsClassDos
. -
We could redundantly implement all its methods on
JsValue
and imported JS classes directly.
-
-
Unchecked casting could be marked
unsafe
to reflect that correctness relies on the programmer in these cases. However, misusing unchecked JS casts cannot introduce memory unsafety in the Rust sense, so this would be usingunsafe
as a general-purpose "you probably shouldn't use this" warning, which is notunsafe
's intended purpose. -
We could only implement unchecked casts for everything all the time. This would encourage a loose, shoot-from-the-hip programming style. We would prefer leveraging types when possible. We realize that escape hatches are still required at times, and we do provide arbitrary unchecked casts, but guide folks towards upcasting with
From
,AsRef
, andAsMut
and doing dynamically checks for other types of casts.
Unresolved Questions
- Should the
JsCast
trait be re-exported inwasm_bindgen::prelude
? We do not specify that it should be in this RFC, and we can initially ship without re-exporting it in prelude and see what it feels like. Based on experience, we may decide in the future to add it to the prelude.
- Start Date: 2018-10-05
- RFC PR: https://github.com/rustwasm/rfcs/pull/5
- Tracking Issue: (leave this empty)
Summary
Change #[wasm_bindgen]
to use structural
by default, and add a new
attribute final
for an opt-in to today's behavior. Once implemented then use
Deref
to model the class inheritance hierarchy in web-sys
and js-sys
to
enable ergonomic usage of superclass methods of web types.
Motivation
The initial motivation for this is outlined RFC 3, namely that the web-sys
crate provides bindings for many APIs found on the web but accessing the
functionality of parent classes is quite cumbersome.
The web makes extensive use of class inheritance hierarchies, and in web-sys
right now each class gets its own struct
type with inherent methods. These
types implement AsRef
between one another for subclass relationships, but it's
quite unergonomic to actually reference the functionality! For example:
# #![allow(unused_variables)] #fn main() { let x: &Element = ...; let y: &Node = x.as_ref(); y.append_child(...); #}
or...
# #![allow(unused_variables)] #fn main() { let x: &Element = ...; <Element as AsRef<Node>>::as_ref(x) .append_child(...); #}
It's be much nicer if we could support this in a more first-class fashion and make it more ergonomic!
Note: While this RFC has the same motivation as RFC 3 it's proposing an alternative solution, specifically enabled by switching by
structural
by default, which is discussed in RFC 3 but is hopefully formally outlined here.
Detailed Explanation
This RFC proposes using the built-in Deref
trait to model the class hierarchy
found on the web in web-sys
. This also proposes changes to #[wasm_bindgen]
to make using Deref
feasible for binding arbitrary JS apis (such as those on
NPM) with Deref
as well.
For example, web-sys
will contain:
# #![allow(unused_variables)] #fn main() { impl Deref for Element { type Target = Node; fn deref(&self) -> &Node { /* ... */ } } #}
allowing us to write our example above as:
# #![allow(unused_variables)] #fn main() { let x: &Element = ...; x.append_child(...); // implicit deref to `Node`! #}
All JS types in web-sys
and in general have at most one superclass. Currently,
however, the #[wasm_bindgen]
attribute allows specifying multiple extends
attributes to indicate superclasses:
# #![allow(unused_variables)] #fn main() { #[wasm_bindgen] extern { #[wasm_bindgen(extends = Node, extends = Object)] type Element; // ... } #}
The web-sys
API generator currently lists an extends
for all superclasses,
transitively. This is then used in the code generator to generate AsRef
implementatiosn for Element
.
The code generation of #[wasm_bindgen]
will be updated with the following
rules:
- If no
extends
attribute is present, defined types will implementDeref<Target=JsValue>
. - Otherwise, the first
extends
attribute is used to implementDeref<Target=ListedType>
. - (long term, currently require a breaking change) reject multiple
extends
attributes, requiring there's only one.
This means that web-sys
may need to be updated to ensure that the immediate
superclass is listed first in extends
. Manual bindings will continue to work
and will have the old AsRef
implementations as well as a new Deref
implementation.
The Deref
implementation will concretely be implemented as:
# #![allow(unused_variables)] #fn main() { impl Deref for #imported_type { type Target = #target_type; #[inline] fn deref(&self) -> &#target_type { ::wasm_bindgen::JsCast::unchecked_ref(self) } } #}
Switching to structural
by default
If we were to implement the above Deref
proposal as-is today in
wasm-bindgen
, it would have a crucial drawback. It may not handle inheritance
correctly! Let's explore this with an example. Say we have some JS we'd like to
import:
class Parent {
constructor() {}
method() { console.log('parent'); }
}
class Child extends Parent {
constructor() {}
method() { console.log('child'); }
}
we would then bind this in Rust with:
# #![allow(unused_variables)] #fn main() { #[wasm_bindgen] extern { type Parent; #[wasm_bindgen(constructor)] fn new() -> Parent; #[wasm_bindgen(method)] fn method(this: &Parent); #[wasm_bindgen(extends = Parent)] type Child; #[wasm_bindgen(constructor)] fn new() -> Child; #[wasm_bindgen(method)] fn method(this: &Child); } #}
and we could then use it like so:
# #![allow(unused_variables)] #fn main() { #[wasm_bindgen] pub fn run() { let parent = Parent::new(); parent.method(); let child = Child::new(); child.method(); } #}
and we would today see parent
and child
logged to the console. Ok everything
is working as expected so far! We know we've got Deref<Target=Parent> for Child
, though, so let's say we tweak this example a bit:
# #![allow(unused_variables)] #fn main() { #[wasm_bindgen] pub fn run() { call_method(&Parent::new()); call_method(&Child::new()); } fn call_method(object: &Parent) { object.method(); } #}
Here we'd naively (and correctly) expect parent
and child
to be output like
before, but much to our surprise this actually prints out parent
twice!
The issue with this is how #[wasm_bindgen]
treats method calls today. When you
say:
# #![allow(unused_variables)] #fn main() { #[wasm_bindgen(method)] fn method(this: &Parent); #}
then wasm-bindgen
(the CLI tool) generates JS that looks like this:
const Parent_method_target = Parent.prototype.method;
export function __wasm_bindgen_Parent_method(obj) {
Parent_method_target.call(getObject(obj));
}
Here we can see that, by default, wasm-bindgen
is reaching into the
prototype
of each class to figure out what method to call. This in turn
means that when Parent::method
is called in Rust, it unconditionally uses the
method defined on Parent
rather than walking the protype chain (that JS
usually does) to find the right method
method.
To improve the situation there's a structural
attribute to wasm-bindgen to fix
this, which when applied like so:
# #![allow(unused_variables)] #fn main() { #[wasm_bindgen(method, structural)] fn method(this: &Parent); #}
means that the following JS code is generated:
const Parent_method_target = function() { this.method(); };
// ...
Here we can see that a JS function shim is generated instead of using the raw
function value in the prototype. This, however, means that our example above
will indeed print parent
and then child
because JS is using prototype
lookups to find the method
method.
Phew! Ok with all that information, we can see that if structural
is omitted
then JS class hierarchies can be subtly incorrect when methods taking parent
classes are passed child classes which override methods.
An easy solution to this problem is to simply use structural
everywhere, so...
let's propose that! Consequently, this RFC proposes changing #[wasm_bindgen]
to act as if all bindings are labeled as structural
. While technically a
breaking change it's believed that we don't have any usage which would actually
run into the breakage here.
Adding #[wasm_bindgen(final)]
Since structural
is not the default today we don't actually have a name for
the default behavior of #[wasm_bindgen]
today. This RFC proposes adding a new
attribute to #[wasm_bindgen]
, final
, which indicates that it should have
today's behavior.
When attached to an attribute or method, the final
attribute indicates that
the method or attribute should be processed through the prototype
of a class
rather than looked up structurally via the prototype chain.
You can think of this as "everything today is final
by default".
Why is it ok to make structural
the default?
One pretty reasonable question you might have at this point is "why, if
structural
is the default today, is it ok to switch?" To answer this, let's
first explore why final
is the default today!
From its inception wasm-bindgen
has been designed with the future host
bindings proposal for WebAssembly. The host bindings proposal promises
faster-than-JS DOM access by removing many of the dynamic checks necessary when
calling DOM methods. This proposal, however, is still in relatively early stages
and hasn't been implemented in any browser yet (as far as we know).
In WebAssembly on the web all imported functions must be plain old JS functions.
They're all currently invoked with undefined
as the this
parameter. With
host bindings, however, there's a way to say that an imported function uses the
first argument to the function as the this
parameter (like Function.call
in
JS). This in turn brings the promise of eliminating any shim functions
necessary when calling imported functionality.
As an example, today for #[wasm_bindgen(method)] fn parent(this: &Parent);
we
generate JS that looks like:
# #![allow(unused_variables)] #fn main() { #[wasm_bindgen(method)] fn method(this: &Parent); #}
means that the following JS code is generated:
const Parent_method_target = Parent.prototype.method;
export function __wasm_bindgen_Parent_method(idx) {
Parent_method_target.call(getObject(idx));
}
If we assume for a moment that anyref
is implemented we
could instead change this to:
const Parent_method_target = Parent.prototype.method;
export function __wasm_bindgen_Parent_method(obj) {
Parent_method_target.call(obj);
}
(note the lack of need for getObject
). And finally, with host bindings we
can say that the wasm module's import of __wasm_bindgen_Parent_method
uses the
first parameter as this
, meaning we can transform this to:
export const __wasm_bindgen_Parent_method = Parent.prototype.method;
and voila, no JS function shims necessary! With structural
we'll still need
a function shim in this future world:
export const __wasm_bindgen_Parent_method = function() { this.method(); };
Alright, with some of those basics out of the way, let's get back to
why-final
-by-default. The promise of host bindings is that by eliminating
all these JS function shims necessary we can be faster than we would otherwise
be, providing a feeling that final
is faster than structural
. This future,
however, relies on a number of unimplemented features in wasm engines today.
Let's consequently get an idea of what the performance looks like today!
I've been slowly over time preparing a microbenchmark suite for measuring
JS/wasm/wasm-bindgen performance. The interesting one here is the benchmark
"structural
vs not". If you click "Run test" in a browser after awhile you'll
see two bars show up. The left-hand one is a method call with final
and the
right-hand one is a method call with structural
. The results I see on my
computer are:
- Firefox 62,
structural
is 3% faster - Firefox 64,
structural
is 3% slower - Chrome 69,
structural
is 5% slower - Edge 42,
structural
is 22% slower - Safari 12,
strutural
is 17% slower
So it looks like for Firefox/Chrome it's not really making much of a difference
but in Edge/Safari it's much faster to use final
! It turns out, however, that
we're not optimizing structural
as much as we can. Let's change our generated
code from:
const Parent_method_target = function() { this.method(); };
export function __wasm_bindgen_Parent_method(obj) {
Parent_method_target.call(getObject(obj));
}
to...
export function __wasm_bindgen_Parent_method(obj) {
getObject(obj).method();
}
(manually editing the JS today)
and if we rerun the benchmarks (sorry no online demo) we get:
- Firefox 62,
structural
is 22% faster - Firefox 64,
structural
is 10% faster - Chrome 69,
structural
is 0.3% slower - Edge 42,
structural
is 15% faster - Safai 12,
structural
is 8% slower
and these numbers look quite different! There's some strong data here showing
that final
is not universally faster today and is actually almost
universally slower (when we optimize structural
slightly).
Ok! That's all basically a very long winded way of saying final
was the
historical default because we thought it was faster, but it turns out that in JS
engines today it isn't always faster. As a result, this RFC proposes that it's
ok to make structural
the default.
Drawbacks
Deref
is a somewhat quiet trait with disproportionately large ramifications.
It affects method resolution (the .
operator) as well as coercions (&T
to
&U
). Discovering this in web-sys
and/or JS apis in the ecosystem isn't
always the easiest thing to do. It's thought, though, that this aspect of
Deref
won't come up very often when using JS apis in practice. Instead most
APIs will work "as-is" as you might expect in JS in Rust as well, with Deref
being an unobtrusive solution for developers to mostly ignore it an just call
methods.
Additionally Deref
has the drawback that it's not explicitly designed for
class inheritance hierarchies. For example *element
produces a Node
,
**element
produces an Object
, etc. This is expected to not really come up
that much in practice, though, and instead automatic coercions will cover almost
all type conversions.
Rationale and Alternatives
The primary alternative to this design is RFC 3, using traits to model the inheritance hierarchy. The pros/cons of that proposal are well listed in RFC 3.
Unresolved Questions
None right now!
- Start Date: 2018-01-08
- RFC PR: (leave this empty)
- Tracking Issue: (leave this empty)
Summary
Add the ability for #[wasm_bindgen]
to process, load, and handle dependencies
on local JS files.
-
The
module
attribute can now be used to import files explicitly:# #![allow(unused_variables)] #fn main() { #[wasm_bindgen(module = "/js/foo.js")] extern "C" { // ... } #}
-
The
inline_js
attribute can now be used to import JS modules inline:# #![allow(unused_variables)] #fn main() { #[wasm_bindgen(inline_js = "export function foo() {}")] extern "C" { fn foo(); } #}
-
The
--browser
flag is repurposed to generate an ES module for the browser and--no-modules
is deprecated in favor of this flag. -
The
--nodejs
will not immediately support local JS snippets, but will do so in the future.
Motivation
The goal of wasm-bindgen
is to enable easy interoperation between Rust and JS.
While it's very easy to write custom Rust code, it's actually pretty difficult
to write custom JS and hook it up with #[wasm_bindgen]
(see
rustwasm/wasm-bindgen#224). The #[wasm_bindgen]
attribute currently only supports importing functions from ES modules, but even
then the support is limited and simply assumes that the ES module string exists
in the final application build step.
Currently there is no composable way for a crate to have some auxiliary JS that
it is built with which ends up seamlessly being included into a final built
application. For example the rand
crate can't easily include local JS (perhaps
to detect what API for randomness it's supposed to use) without imposing strong
requirements on the final artifact.
Ergonomically support imports from custom JS files also looks to be required by
frameworks like stdweb
to build a macro like js!
. This involves generating
snippets of JS at compile time which need to be included into the final bundle,
which is intended to be powered by this new attribute.
Stakeholders
Some major stakeholders in this RFC are:
- Users of
#[wasm_bindgen]
- Crate authors wishing to add wasm support to their crate.
- The
stdweb
authors - Bundler (webpack) and
wasm-bindgen
integration folks.
Most of the various folks here will be cc'd onto the RFC, and reaching out to more is always welcome!
Detailed Explanation
This proposal involves a number of moving pieces, all of which are intended to
work in concert to provide a streamlined story to including local JS files into
a final #[wasm_bindgen]
artifact. We'll take a look at each piece at a time
here.
New Syntactical Features
The most user-facing change proposed here is the reinterpretation of the
module
attribute inside of #[wasm_bindgen]
and the addition of an
inline_js
attribute. They can now be used to import local files and define
local imports like so:
# #![allow(unused_variables)] #fn main() { #[wasm_bindgen(module = "/js/foo.js")] extern "C" { // ... definitions } #[wasm_bindgen(inline_js = "export function foo() {}")] extern "C" { fn foo(); } #}
The first declaration says that the block of functions and types and such are
all imported from the /js/foo.js
file, relative to the current file and rooted
at the crate root. The second declaration lists the JS inline as a string
literal and the extern
block describes the exports of the inline module.
The following rules are proposed for interpreting a module
attribute.
-
If the strings starts with the platform-specific representation of an absolute path to the cargo build directory (identified by
$OUT_DIR
) then the string is interpreted as a file path in the output directory. This is intended for build scripts which generate JS files as part of the build. -
If the string starts with
/
,./
, or../
then it's considered a path to a local file. If not, then it's passed through verbatim as the ES module import. -
All paths are resolved relative to the current file, like Rust's own
#[path]
,include_str!
, etc. At this time, however, it's unknown how we'd actually do this for relative files. As a result all paths will be required to start with/
. Whenproc_macro
has a stable API (or we otherwise figure out how) we can start allowing./
and../
-prefixed paths.
This will hopefully roughly match what programmers expect as well as preexisting conventions in browsers and bundlers.
The inline_js
attribute isn't really intended to be used for general-purpose
development, but rather a way for procedural macros which can't currently today
rely on the presence of $OUT_DIR
to generate JS to import.
Format of imported JS
All imported JS is required to written with ES module syntax. Initially the JS must be hand-written and cannot be postprocessed. For example the JS cannot be written with TypeScript, nor can it be compiled by Babel or similar.
As an example, a library may contain:
# #![allow(unused_variables)] #fn main() { // src/lib.rs #[wasm_bindgen(module = "/js/foo.js")] extern "C" { fn call_js(); } #}
accompanied with:
// js/foo.js
export function call_js() {
// ...
}
Note that js/foo.js
uses ES module syntax to export the function call_js
.
When call_js
is called from Rust it will call the call_js
function in
foo.js
.
Propagation Through Dependencies
The purpose of the file
attribute is to work seamlessly with dependencies.
When building a project with #[wasm_bindgen]
you shouldn't be required to know
whether your dependencies are using local JS snippets or not!
The #[wasm_bindgen]
macro, at compile time, will read the contents of the file
provided, if any. This file will be serialized into the wasm-bindgen custom
sections in a wasm-bindgen specific format. The final wasm artifact produced by
rustc will contain all referenced JS file contents in its custom sections.
The wasm-bindgen
CLI tool will extract all this JS and write it out to the
filesystem. The wasm file (or the wasm-bindgen-generated shim JS file) emitted
will import all the emitted JS files with relative imports.
Updating wasm-bindgen
output modes
The wasm-bindgen
has a few modes of output generation today. These output
modes are largely centered around modules vs no modules and how modules are
defined. This RFC proposes that we move away from this moreso towards
environments, such as node.js-compatible vs browser-compatible code (which
involves more than only module format). This means that in cases where an
environment supports multiple module systems, or the module system is optional
(browsers support es modules and also no modules) wasm-bindgen
will choose
what module system it thinks is best as long as it is compatible with that
environment.
The current output modes of wasm-bindgen
are:
-
Default - by default
wasm-bindgen
emits output that assumes the wasm module itself is an ES module. This will naturally work with custom JS snippets that are themselves ES modules, as they'll just be more modules in the graph all found in the local output directory. This output mode is currently only consumable by bundlers like Webpack, the default output cannot be loaded in either a web browser or Node.js. -
--no-modules
- the--no-modules
flag towasm-bindgen
is incompatible with ES modules because it's intended to be included via a<script>
tag which is not a module. This mode, like today, will fail to work if upstream crates contain local JS snippets. -
--nodejs
- this flag towasm-bindgen
indicates that the output should be tailored for Node.js, notably using CommonJS module conventions. In this modewasm-bindgen
will eventually use a JS parser in Rust to rewrite ES syntax of locally imported JS modules into CommonJS syntax. -
--browser
- currently this flag is the same as the default output mode except that the output is tailored slightly for a browser environment (such as assuming thatTextEncoder
is ambiently available).This RFC proposes repurposing this flag (breaking it) to instead generate an ES module natively loadable inside the web browser, but otherwise having a similar interface to
--no-modules
today, detailed below.
This RFC proposes rethinking these output modes as follows:
Target Environment | CLI Flag | Module Format | User Experience | How are Local JS Snippets Loaded? |
---|---|---|---|---|
Node.js without bundler | --nodejs | Common.js | require() the main JS glue file | Main JS glue file require() s crates' local JS snippets. |
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. |
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. |
It is notable that browser with and without bundler is almost the same as far
as wasm-bindgen
is concerned: the only difference is that if we assume a
bundler, we can rely on the bundler polyfilling wasm-as-ES-module for us.
Note the --browser
here is relatively radically different today and as such
would be a breaking change. It's thought that the usage of --browser
is small
enough that we can get away with this, but feedback is always welcome on this
point!
The --no-modules
flag doesn't really fit any more as the --browser
use case
is intended to subsume that. Note that the this RFC proposes only having the
bundler-oriented and browser-oriented modes supporting local JS snippets for
now, while paving a way forward to eventually support local JS snippets in
Node.js. The --no-modules
could eventually also be supported in the same
manner as Node.js is (once we're parsing the JS file and rewriting the exports),
but it's proposed here to generally move away from --no-modules
towards
--browser
.
The --browser
output is currently considered to export an initialization
function which, after called and the returned promise is resolved (like
--no-modules
today) will cause all exports to work when called. Before the
promise resolves all exports will throw an error when called.
JS files depending on other JS files
One tricky point about this RFC is when a local JS snippet depends on other JS files. For example your JS might look like:
// js/foo.js
import { foo } from '@some/npm-package';
import { bar } from './bar.js'
// ...
As designed above, these imports would not work. It's intended that we explicitly say this is an initial limitation of this design. We won't support imports between JS snippets just yet, but we should eventually be able to do so.
In the long run to support --nodejs
we'll need some level of ES module parser
for JS. Once we can parse the imports themselves it would be relatively
straightforward for #[wasm_bindgen]
, during expansion, to load transitively
included files. For example in the file above we'd include ./bar.js
into the
wasm custom section. In this future world we'd just rewrite ./bar.js
(if
necessary) when the final output artifact is emitted. Additionally with NPM
package support in wasm-pack
and wasm-bindgen
(a future goal) we could
validate entries in package.json
are present for imports found.
Accessing wasm Memory/Table
JS snippets interacting with the wasm module may commonly need to work with the
WebAssembly.Memory
and WebAssembly.Table
instances associated with the wasm
module. This RFC proposes using the wasm itself to pass along these objects,
like so:
# #![allow(unused_variables)] #fn main() { // lib.rs #[wasm_bindgen(module = "/js/local-snippet.js")] extern { fn take_u8_slice(memory: &JsValue, ptr: u32, len: u32); } #[wasm_bindgen] pub fn call_local_snippet() { let vec = vec![0,1,2,3,4]; let mem = wasm_bindgen::memory(); take_u8_slice(&mem, vec.as_ptr() as usize as u32, vec.len() as u32); } #}
// js/local-snippet.js
export function take_u8_slice(memory, ptr, len) {
let slice = new UInt8Array(memory.arrayBuffer, ptr, len);
// ...
}
Here the wasm_bindgen::memory()
existing intrinsic is used to pass along the
memory object to the imported JS snippet. To mirror this we'll add
wasm_bindgen::function_table()
as well to the wasm-bindgen
crate as an
intrinsic to access the function table and return it as a JsValue
.
Eventually we may want a more explicit way to import the memory/table, but for now this should be sufficient for expressiveness.
Drawbacks
-
The initial RFC is fairly conservative. It doesn't work with
--nodejs
out of the gate nor--no-modules
. Additionally it doesn't support JS snippets importing other JS initially. Note that all of these are intended to be supported in the future, it's just thought that it may take more design than we need at the get-go for now. -
JS snippets must be written in vanilla ES module JS syntax. Common preprocessors like TypeScript can't be used. It's unclear how such preprocessed JS would be imported. It's hoped that JS snippets are small enough that this isn't too much of a problem. Larger JS snippets can always be extracted to an NPM package and postprocessed there. Note that it's always possible for authors to manually run the TypeScript compiler by hand for these use cases, though.
-
The relatively popular
--no-modules
flag is proposed to be deprecated in favor of a--browser
flag, which itself will have a breaking change relative to today. It's thought though that--browser
is only very rarely used so is safe to break, and it's also thought that we'll want to avoid breaking--no-modules
as-is today. -
Local JS snippets are required to be written in ES module syntax. This may be a somewhat opinionated stance, but it's intended to make it easier to add future features to
wasm-bindgen
while continuing to work with JS. The ES module system, however, is the only known official standard throughout the ecosystem, so it's hoped that this is a clear choice for writing local JS snippets.
Rationale and Alternatives
The primary alternative to this system is a macro like js!
from stdweb. This
allows written small snippets of JS code directly in Rust code, and then
wasm-bindgen
would have the knowledge to generate appropriate shims. This RFC
proposes recognizing module
paths instead of this approach as it's thought to
be a more general approach. Additionally it's intended that the js!
macro can
be built on the module
directive including local file paths. The
wasm-bindgen
crate may grow a js!
-like macro one day, but it's thought that
it's best to start with a more conservative approach.
One alternative for ES modules is to simply concatenate all JS together. This way we wouldn't have to parse anything but we'd instead just throw everything into one file. The downside of this approach, however, is that it can easily lead to namespacing conflicts and it also forces everyone to agree on module formats and runs the risk of forcing the module format of the final product.
Another alternative to emitting small files at wasm-bindgen time is to instead unpack all files at runtime by leaving them in custom sections of the wasm executable. This in turn, however, may violate some CSP settings (particularly strict ones).
Unresolved Questions
-
Is it necessary to support
--nodejs
initially? -
Is it necessary to support local JS imports in local JS snippets initially?
-
Are there known parsers of JS ES modules today? Are we forced to include a full JS parser or can we have a minimal one which only deals with ES syntax?
-
How would we handle other assets like CSS, HTML, or images that want to be referenced by the final wasm file?
- Start Date: 2019-01-23
- RFC PR: https://github.com/rustwasm/rfcs/pull/7
Summary
2019 is the year WebAssembly with Rust goes from "usable" to "stable, batteries-available, and production-ready."
To realize this goal, the Rust and WebAssembly domain working group will:
-
Cultivate a library ecosystem by collaborating on a modular toolkit
-
Bring multithreading to Rust-generated Wasm
-
Integrate best-in-class debugging support into our toolchain
-
Polish our toolchain and developer workflow, culminating in a 1.0 version of
wasm-pack
-
Invest in monitoring, testing, and profiling infrastructure to keep our tools and libraries snappy, stable and production-ready.
Motivation
This proposed roadmap draws upon
-
the community's blog posts in response to the working group's call for roadmap suggestions,
-
and the working group's core team's intuition and experience.
Detailed Explanation
Collaborating on a Modular Toolkit
The idea of building [high-level libraries] in a modular way that will allow others in the community to put the components together in a different way is very exciting to me. This hopefully will make the ecosystem as a whole much stronger.
In particular I’d love to see a modular effort towards implementing a virtual DOM library with JSX like syntax. There have been several efforts on this front but all have seemed relatively monolithic and “batteries included”. I hope this will change in 2019.
— Ryan Levick in Rust WebAssembly 2019
Don't create branded silos. Branding might perhaps be useful to achieve fame. But if we truly want Rust's Wasm story to succeed we should think of ways to collaborate instead of carving out territory.
— Yoshua Wuyts in Wasm 2019
In 2018, we created foundational libraries like js-sys
and
web-sys
. In 2019, we should build modular, high-level
libraries on top of them, and collect the libraries under an umbrella toolkit
crate for a holistic experience. This toolkit and its libraries will make
available all the batteries you want when targeting Wasm.
Building a greenfield Web application? Use the whole toolkit to hit the ground running. Carefully crafting a tiny Wasm module and integrating it back into an existing JavaScript project? Grab that one targeted library you need out from the toolkit and use it by itself.
-
Modular: Take or leave any individual component. Prefer interfaces over implementations.
-
Cultivate collaboration: We've already seen an ecosystem sprouting up in the Rust and WebAssembly domain, and lots of great experiments, but we haven't seen a lot of collaboration between projects. By deliberately creating a space for collaboration, we can reduce effort duplication, multiply impact, and help the ecosystem stay healthy.
Multithreading for Wasm
We must bring Rust’s fearless concurrency to the Web!
— Nick Fitzgerald in Rust and WebAssembly in 2019
Be the absolute cutting edge when it comes to WebAssembly, we should be thinking about being the first to satisfy [threads and atomics].
— richardanaya in My Rust 2019 Dream
Our toolchain already has experimental support for multithreading in
Wasm. Browsers are currently shipping SharedArrayBuffer
and
atomics (the primitives of multithreading for Wasm) behind feature flags, and
they expect to start shipping them enabled by default in 2019.
One of WebAssembly's selling points is the ability to effectively utilize available hardware. Multithreading extends that story from a single core to many. While multithreading will be literally possible for both JavaScript and any compiled-to-Wasm language, Rust's unique ownership system makes it economically realistic.
There are some technical snags (see the link above for details) that mean we
can't get Rust's standard library's std::thread::*
working on Wasm. But it is
still crucial that we have shared implementations of core multithreading
building blocks like thread pools and locks across the ecosystem. In 2019, we
should transform our experimental multithreading support into a production-ready
foundation for multithreading on Wasm, get popular crates like rayon
working
on the Web, and cash in on Rust's fearless concurrency.
Debugging
Before [debugging] is working properly (including variable inspection, which doesn't work with wasm at all right now), everything else is just toying around.
— anlumo in a comment on r/rust
Having [source maps] would be excellent for debugging.
— Yoshua Wuyts in Wasm 2019
Debugging is tricky because much of the story is out of this working group's hands, and depends on both the WebAssembly standardization bodies and the folks implementing browser developer tools instead. However, there are some concrete steps we can take to improve debugging:
-
Get
println!
,dbg!
, and friends working out of the box with Wasm. To achieve this, we will build support for the WebAssembly reference sysroot and standard system calls for Wasm that are in the standardization pipeline. -
Create the ability to compile our Rust-generated Wasm to JavaScript with source maps when debugging. Source maps are a limited debug info format for JavaScript that enable stepping through source locations in a debugger, instead of stepping through compiler-generated JavaScript code.
-
Add debugging-focused tracing and instrumentation features to our toolchain. For example, it is currently difficult to debug a JavaScript array buffer view of Wasm memory getting detached because Wasm memory was resized. We can make debugging easier by optionally instrumenting
mem.grow
instructions with logging.
In addition to that, we should work with the WebAssembly standardization bodies and browser developer tools makers, and actively participate in the WebAssembly debugging subcharter to create some movement in the debugging space. By keeping up the environmental and social pressure and lending a hand where we can, we will eventually have rich, source-level debugging for Wasm.
Toolchain and Workflow Polish
Setting up a Wasm project requires quite some boilerplate. It'd be nice if we could find ways to reduce this.
— Yoshua Wuyts in Wasm 2019
There are a few things that we intended to include in
wasm-pack
in 2018 that didn’t quite make the cut. [...] We should finish these tasks and polishwasm-pack
into a 1.0 tool.
— Nick Fitzgerald in Rust and WebAssembly in 2019
In 2019, our toolchain and workflow should be feature complete and
polished. wasm-pack
, being the entry point to our toolchain, will bear the
brunt of this work, but much of it will also be in tools that are invoked by
wasm-pack
rather than work in wasm-pack
itself.
-
Generate JavaScript API documentation from the Rust doc comments on
#[wasm_bindgen]
exports. -
Finish and implement the RFC for library crates depending on external NPM packages.
-
Finish and implement the RFC for local JavaScript snippets.
-
Integrate
cargo generate
intowasm-pack
for new project scaffolding. This would smooth the developer on ramp, by making one less tool required to get up and running. -
RFC and implementation for generating portable, universal NPM packages that work on the Web, with Node.js, and in any minimal JavaScript environment.
-
Define a philosophy for
wasm-pack
's user experience, interaction, and display. Once defined and agreed upon, we should triage eachwasm-pack
subcommand and ensure that it is consistent with our philosophy.
Given that this work is largely about plugging missing holes and improving user
experience, it is a bit of a laundry list. But that is also good sign: it means
that wasm-pack
is actually fairly close to being feature complete.
After we've finished all these tasks, we should publish a 1.0 release of
wasm-pack
.
Monitoring, Profiling, and Testing Infrastructure
The main objection I've experienced when proposing rust/wasm is compile times, but the end-to-end latency actually looks pretty competitive so far [...] Having a couple of benchmarks in CI and a graph online somewhere would go a long way towards keeping it that way.
— @jamii in an RFC comment
If I want to run the tests of a library using both libtest and wasm-bindgen-test I need to write:
# #![allow(unused_variables)] #fn main() { #[cfg_attr(not(target_arch = "wasm32"), test)] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] fn my_test() { ... } #}
instead of just
# #![allow(unused_variables)] #fn main() { #[test]` fn my_test() { ... } #}
— @gnzlbg in an RFC comment
We should build perf.rust-lang.org-style infrastructure0 to keep an eye on
-
code size of popular and foundational wasm crates (such as those crates in our modular toolkit), and
-
our
wasm-bindgen
andwasm-pack
build times.
By continually tracking this data over time, and just at once at a particular, we will hold ourselves accountable to delivering on our promises of a lightweight toolkit and "stable, production-ready" toolchain.
0 Or perhaps integrate our monitoring into perf.rust-lang.org if it makes sense and the maintainers are willing.
That is the infrastructure story at the macro-level, but we also need to support the needs of crates within the ecosystem at the micro-level. That means continuing to invest in unit testing and profiling Rust-generated Wasm binaries. Concretely, we should
-
add benchmarking support to
wasm-bindgen-test
, and -
make
wasm-bindgen-test
future-compatible with the eRFC for custom test frameworks, paving the way forward for making regular#[test]
and#[bench]
Just Work™ with Wasm instead of requiring the use of#[wasm_bindgen_test]
instead.
Rationale, Drawbacks, and Alternatives
We choose to focus our efforts in 2019 where:
-
We — the Rust and WebAssembly working group — can build and ship features. Areas where we aren't potentially blocked by external factors, such as still-in-progress standards.
-
We can leverage advantages that are unique to Rust in the WebAssembly domain.
Things We can Build and Ship
We don't want our fate in anyone's hands but our own.
The toolkit and toolchain polish work don't involve any external entities that could slow our progress to a halt. For debugging, where the larger story involves significant consensus with external groups and standards work, we explicitly choose to focus on what we can do ourselves to improve our own debugging story. We do not set ourselves up to block on anything produced by the WebAssembly community group's debugging subcharter, and we won't wait on browser vendors to implement new Wasm debugging support in developer tools.
Of the roadmap items, the multithreading story has the most risk: our success in this domain relies on browsers enabling Wasm's multithreading primitives by default. However, this seems like a relatively safe bet, since the multithreading primitives have moved past their most experimental phase, Chrome is already shipping them enabled by default, and all other major browsers have implementations that just aren't enabled by default yet.
Leveraging Unique Advantages
We want to focus our efforts where we get the biggest effort to impact efficiency, and establish ourselves as leaders in WebAssembly in ways that no one else even has a route towards catching up.
The multithreading story is perhaps the biggest example of unique advantage: multithreading is infamously bug prone (to say the least!) and Rust's ownership system eliminates data races at compile time.
By building a modular toolkit of libraries, we bolster our ability to target the full spectrum from tiny module surgically inserted into an existing JavaScript application, to building a complete Web application in Rust. Any language that relies on a garbage collector, fat runtime, or is overly opinionated about FFI and interaction with the outside world can't reach the tiny module end of that spectrum.
The toolchain polish and debugging work have less clearly unique advantages. But both are table stakes for a good development experience, and the par for the course for these things in the Wasm domain is currently so low that we can and should stand out from the crowd.
Considered Alternative Roadmap Items
Here are a few alternative items that were considered for the roadmap, perhaps
because they were called out in #RustWasm2019
posts, but ultimately were not
included.
Pushing anyref
Integration into the Rust Language
We've already been well positioned to take advantage of host bindings and GC
reference types once they ship in Wasm via wasm-bindgen
. We could take it even
further and imagine a future where the Rust language was able to pass around
opaque references to objects in alternative memory spaces (some of which might
be GC'd) in a first class way: structs that are split across memory spaces, fat
pointers into multiple memory spaces, etc.
However, it isn't clear that pushing this all the way into the language will
bring that much utility over the existing "anyref
at the edges"
implementation that wasm-bindgen
already has.
Additionally, cashing in on this work could easily be blocked in a couple ways:
anyref
isn't shipping in any mainstream wasm engine yet, and getting this
language-level integration through the larger Rust RFC process with all of its
stakeholders would happen at a glacial pace (if it even happened!)
A Focus Only on Pure-Rust Web Applications
We prefer to answer "yes and" to pure-Rust Web applications via the modular toolkit that can service the full spectrum of tiny module to whole Web app, than to focus only on the whole Web app end of the spectrum. Our hope with the toolkit is that a rising tide will lift all boats, regardless where your project lands on that spectrum.
Additionally, full Web apps are not a unique advantage for Rust. JavaScript has been doing it for a while, and as far as Wasm goes, there are better-funded "competitors" in the space that will be able to provide a more compelling monolithic Web app development experience more quickly (via integration with tooling, existing ecosystems, or throwing money and developers at the problem). Microsoft and Blazor, Google and Go, bringing existing native applications to the Web with Emscripten, etc. We should compete where we are best positioned to do so, and monolithic Web applications is not that.
All that said, if you want to build a whole Web application with Rust-generated
Wasm and don't want to write any JavaScript at all, you should be able to do
so. In fact, you already can with #[wasm_bindgen(start)]
and the no-modules
target. We will never remove this ability, and the new toolkit will
only make developing a whole Web app easier.
Non-JavaScript and Non-Web Embeddings
While the whole of every non-Web and non-JavaScript WebAssembly embeddings looks very exciting, each embedding is a unique environment, and there is not yet a standard set of capabilities available. We don't want to block on waiting for a full set of standard capabilities to emerge, nor do we want to choose one particular embedding environment.
We do intend to support the reference sysroot work, and any follow up work that comes after it, but we will take advantage of these things on a opportunistic basis rather than making it a roadmap item.
We encourage anyone interested in non-JavaScript and non-Web embeddings to collaborate with the WebAssembly community group to push this story forward by defining standard Wasm capabilities!
Unresolved Questions
To be determined.
- Start Date: 2018-02-14
- RFC PR: (leave this empty)
- Tracking Issue: (leave this empty)
Summary
Enable Rust crates to transparently depend on packages in the npm ecosystem. These dependencies will, like normal Rust dependencies through Cargo, work seamlessly when consumed by other crates.
Motivation
The primary goal of wasm-bindgen
and wasm-pack
is to enable seamless
integration of Rust with JS. A massive portion of the JS ecosystem, npm, however
currently has little support in wasm-bindgen
and wasm-pack
, making it
difficult to access this rich resource that JS offers!
The goal of this RFC is to enable these dependencies to exist. Rust crates should be able to require functionality from NPM, just like how NPM can require Rust crates compiled to wasm. Any workflow which currently uses NPM packages (such as packaging WebAssembly with a bundler) should continue to work but also allow pulling in "custom" NPM packages as well as requested by Rust dependencies.
Stakeholders
This RFC primarily affects uses of wasm-pack
and wasm-bindgen
who are also
currently using bundlers like Webpack. This also affects, however, developers of
core foundational crates in the Rust ecosystem who want to be concious of the
ability to pull in NPM dependencies and such.
Detailed Explanation
Adding an NPM dependency to a Rust project will look very similar to adding an
NPM dependency to a normal JS project. First the dependency, and its version
requirement, need to be declare. This RFC proposes doing this in a
package.json
file adjacent to the crate's Cargo.toml
file:
{
"dependencies": {
"foo": "^1.0.1"
}
}
The package.json
file will initially be a subset of NPM's package.json
,
only supporting one dependencies
top-level key which internally has key/value
pairs with strings. Beyond this validation though no validation will be
performed of either key or value pairs within dependencies
. In the future
it's intended that more keys of package.json
in NPM will be supported, but
this RFC is intended to be an MVP for now to enable dependencies on NPM at all.
After this package.json
file is created, the package next needs to be
imported in the Rust crate. Like with other Rust dependencies on JS, this will
be done with the #[wasm_bindgen]
attribute:
# #![allow(unused_variables)] #fn main() { #[wasm_bindgen(module = "foo")] extern "C" { fn function_in_foo_package(); } #}
Note: in JS the above import would be similar to:
import { function_in_foo_package } from "foo";
The exiting module
key in the #[wasm_bindgen]
attribute can be used to
indicate which ES module the import is coming from. This affects the module
key in the final output wasm binary, and corresponds to the name of the package
in package.json
. This is intended to match how bundler conventions already
interpret NPM packages as ES modules.
After these two tools are in place, all that's needed is a wasm-pack build
and
you should be good to go! The final package.json
will have the foo
dependency listed in our package.json
above and be ready for consumption via a
bundler.
Technical Implementation
Under the hood there's a few moving parts which enables all of this to happen.
Let's first take a look at the pieces in wasm-bindgen
.
The primary goal of this RFC is to enable tranparent and transitive
dependencies on NPM. The #[wasm_bindgen]
macro is the only aspect of a crate's
build which has access to all transitive dependencies, so this is what we'll be
using to slurp up package.json
. When #[wasm_bindgen]
with a module
key is
specified it will look for package.json
inside the cwd of the procedural macro
(note that the cwd is set by Cargo to be the directory with the crate's
Cargo.toml
that is being compiled, or the crate in which #[wasm_bindgen]
is
written). This package.json
, if found, will have an absolute path to it
encoded into the custom section that wasm-bindgen
already emits.
Later, when the wasm-bindgen
CLI tool executes, it will parse an interpret all
items in the wasm-bindgen custom section. All package.json
files listed will
be loaded, parsed, and validated (aka only dependencies
allowed for now). If
any package.json
is loaded then a package.json
file will be emitted next to
the output JS file inside of --out-dir
.
After wasm-bindgen
executes, then wasm-pack
will read the package.json
output, if any, and augment it with metadata and other items which are already
emitted.
If more than one crate in a dependency graph depends on an NPM package then in
this MVP proposal an error will be generated. In the future we can implement
some degree of merging version requirements, but for now to remain simple
wasm-bindgen
will emit an error.
Interaction with --no-modules
Depending on NPM packages fundamentally requires, well, NPM, in one way or
another. The wasm-bindgen
and wasm-pack
CLI tools have modes of output
(notably wasm-bindgen
's --no-modules
and wasm-pack
's --target no-modules
flags) which are intended to not require NPM and other JS tooling. In these
situations if a package.json
in any Rust crate is detected an error will be
emitted indicating so.
Note that this means that core crates which are intended to work with
--no-modules
will not be able add NPM dependencies. Instead they'll have to
either import Rust dependencies from crates.io or use a feature like local JS
snippets to import custom JS code.
Drawbacks
One of the primary drawbacks of this RFC is that it's fundamentally incompatible
with a major use case of wasm-bindgen
and wasm-pack
, the --no-modules
and
--target no-modules
flags. As a short-term band-aid this RFC proposes making
it a hard error which would hinder the adoption of this feature in crates that
want to be usable in this mode.
In the long-term, however, it may be possible to get this working. For example
many NPM packages are available on unpkg.com
or in other locations. It may be
possible, if all packages in these locations adhere to well-known conventions,
to generate code that's compatible with these locations of hosting NPM packages.
In these situations it may then be possible to "just drop a script tag" in a few
locations to get --no-modules
working with NPM packages. It's unclear how
viable this is, though.
Rationale and Alternatives
When developing this RFC, some guiding values for its design have been articulated:
-
Development on Rust-generated WebAssembly projects should allow developers to use the development environment they are most comfortable with. Developers writing Rust should get to use Rust, and developers using JavaScript should get to use a JS based runtime environment (Node.js, Chakra, etc).
-
JavaScript tooling and workflows should be usable with Rust-generated WebAssembly projects. For example, bundlers like WebPack and Parcel, or dependency management tools such as
npm audit
and GreenKeeper. -
When possible, decisions should be made that allow the solution to be available to developers of not just Rust, but also C, and C++.
-
Decisions should be focused on creating workflows that allow developers an easy learning curve and productive development experience.
These principles lead to the above proposal of using package.json
to declare
NPM dependencies which is then grouped together by wasm-bindgen
to be
published by wasm-pack
. By using package.json
we get inherent compatibility
with existing workflows like GreenKeeper and npm install
. Additionally
package.json
is very well documented and supported throughout the JS ecosystem
making it very familiar.
Some other alternatives to this RFC which have been ruled out are:
-
Using
Cargo.toml
instead ofpackage.json
to declare NPM dependencies. For example we could use:[package.metadata.npm.dependencies] foo = "0.1"
This has the drawback though of being incompatible with all existing workflows around
package.json
. Additionally it also highlights a discrepancy between NPM and Cargo and how"0.1"
as a version requirement is interpreted (e.g.^0.1
or~0.1
). -
Adding a separate manifest file instead of using
package.json
is also possibility and might be easier forwasm-bindgen
to read and later parse/include. This has a possible benefit of being scoped to exactly our use case and not being misleading by disallowing otherwise-valid fields ofpackage.json
. The downside of this approach is the same asCargo.toml
, however, in that it's an unfamiliar format to most and is incompatible with existing tooling without bringing too much benefit. -
Annotating version dependencies inline could be used rather than
package.json
as well, such as:# #![allow(unused_variables)] #fn main() { #[wasm_bindgen(module = "foo", version = "0.1")] extern "C" { // ... } #}
As with all other alternatives this is incompatible with existing tooling, but it's also not aligned with Rust's own mechanism for declaring dependencies which separates the location for version information and the code iteslf.
Unresolved Questions
- Is the MVP restriction of only using
dependencies
too limiting? Should more fields be supported inpackage.json
?