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 for ThisThing, so also implement SomeTrait for RelatedThing").

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 to text/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:

  1. A From implementation for self-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)
        }
    }
    #}
  2. 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)
        }
    }
    #}
  3. 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 standard From and As{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 the std traits.

  • Upcasting using the From and As{Ref,Mut} traits does not provide chainable, turbofishing methods on self 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 on self, at the cost of using non-standard traits.

  • We could use TryFrom for dynamically-checked casts instead of JsCast::dyn_into et al. This would introduce a new nightly feature requirement when using wasm-bindgen. We leave the possibility open for when TryFrom is stabilized by not naming our dynamically-checked cast methods JsCast::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 for MyBase and MyDerived? Also emit a MyDerivedMethods trait that requires MyBase 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 what stdweb does with the IHTMLElement trait for HTMLElement'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:

    1. Only implement its methods on JsValue and require that conversions like ImportedJsClassUno -> ImportedJsClassDos go to JsValue in between: ImportedJsClassUno -> JsValue -> ImpiortedJsClassDos.

    2. 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 using unsafe as a general-purpose "you probably shouldn't use this" warning, which is not unsafe'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, and AsMut and doing dynamically checks for other types of casts.

Unresolved Questions

  • Should the JsCast trait be re-exported in wasm_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 implement Deref<Target=JsValue>.
  • Otherwise, the first extends attribute is used to implement Deref<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 /. When proc_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 to wasm-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 to wasm-bindgen indicates that the output should be tailored for Node.js, notably using CommonJS module conventions. In this mode wasm-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 that TextEncoder 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?

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

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:

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

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

  3. 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 polish wasm-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.

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 and wasm-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:

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

  2. 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 of package.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 for wasm-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 of package.json. The downside of this approach is the same as Cargo.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 in package.json?