Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

generic_per_mono

⚠️ Experimental. This attribute is experimental and its behaviour may change, or it may be removed, in any release. The set of supported signature shapes in particular is expected to grow, and the internal names of the generated JS bindings (__wbindgen_generic_N) are not stable. The type-erasure path described in Working with wasm-bindgen Generics is the supported way to write a generic import.

By default a generic imported function has its type parameters erased: every T is passed across the ABI as a JsValue, and the single JS binding that is generated works for all instantiations. That is described in Working with wasm-bindgen Generics, and it is why T normally has to be a JS type (JsGeneric) rather than a Rust one.

generic_per_mono opts a single import out of erasure. Instead of one erased binding, wasm-bindgen generates one binding per monomorphisation, each with its own descriptor, so arguments and return values are marshalled at their concrete types:

#![allow(unused)]
fn main() {
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console, generic_per_mono)]
    fn log<T>(value: T);
}

log(42u32);        // crosses as a number
log("hello");      // crosses as a string
log(true);         // crosses as a boolean
}

Each of those three calls gets its own JS shim. Because nothing is boxed into a JsValue, T can be an ordinary Rust type — u32, f64, bool, String — which the erasure path does not allow.

When to use it

Reach for generic_per_mono when you want one Rust signature to serve several Rust types and you care about how they marshal. Reach for the default erasure path when you are modelling JS generics (Array<T>, Promise<T>) and want a single binding for all of them.

The trade-off is code size: one JS shim and one descriptor per instantiation. A generic import instantiated at a dozen types produces a dozen shims, so prefer erasure when the concrete marshalling does not matter.

Trait bounds

Bounds you declare are part of the import’s contract. They are carried through to the generated wrapper, so callers must satisfy them, and they also reach the generated shim — which means a shim signature may project an associated type off a bounded parameter. Inline bounds, where predicates, and higher-ranked predicates all work:

#![allow(unused)]
fn main() {
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(generic_per_mono)]
    fn sum_items<T>(items: T) -> f64
    where
        T: IntoIterator<Item = u32>;

    #[wasm_bindgen(generic_per_mono)]
    fn double<T>(value: T) -> T
    where
        for<'a> &'a T: core::ops::Add<&'a T, Output = T>;
}
}

Note that a bound only constrains which types the import can be called with; it cannot make a type marshallable. Combining a higher-ranked bound with a &T argument is a common way to write a declaration that compiles but can never be called: &T additionally requires an IntoWasmAbi impl for the reference, which exists only for JsValue and imported JS types, so a bound such as for<'a> &'a T: IntoIterator<Item = &'a u32> leaves no type that satisfies both. See Note on &T arguments.

Relaxed bounds are the exception: T: ?Sized is not supported on any wasm-bindgen generic — erased or per-monomorphisation — and is reported as unsupported in wasm-bindgen generics.

Lifetime parameters

Lifetime parameters on the function are supported, including lifetime bounds (T: 'a) and lifetime-outlives predicates ('a: 'b):

#![allow(unused)]
fn main() {
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(generic_per_mono)]
    fn log_ref<'a, T>(value: &'a T);
}
}

A lifetime on a method’s receiver works too, and ties the borrow of the receiver to the rest of the call:

#![allow(unused)]
fn main() {
#[wasm_bindgen]
extern "C" {
    type Widget;

    #[wasm_bindgen(method, generic_per_mono)]
    fn set<'a, T>(this: &'a Widget, value: &'a T);
}
}

Lifetimes carry no runtime information — they are erased before values cross the wasm ABI — so this imposes no restriction beyond what plain Rust already requires of the signature. The one shape that is not supported is a lifetime belonging to the class itself, i.e. an imported type declared with its own lifetime parameter (type Holder<'a>, used as this: &Holder<'a>), since that needs the same hoisting machinery class-level type parameters do; see Unsupported shapes.

Other attributes

generic_per_mono composes with the usual import attributes — method, static_method_of, constructor, getter, setter, structural, final, indexing_getter, indexing_setter, indexing_deleter, js_namespace, js_name, catch, variadic, and slice_to_array — and the resulting JS binding is shaped exactly as it would be for the equivalent non-generic import.

The two that do not compose are assert_no_shim and reexport; both are rejected, see Unsupported shapes.

async is supported, and returns a future in the usual way:

#![allow(unused)]
fn main() {
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(generic_per_mono)]
    async fn round_trip<T>(value: T) -> T;
}
}

Applying it to a whole block

generic_per_mono can also go on the extern "C" block, which every function in the block then inherits:

#![allow(unused)]
fn main() {
#[wasm_bindgen(generic_per_mono)]
extern "C" {
    fn log_one<T>(x: T);
    fn log_two<T>(a: u32, b: T) -> T;
}
}

The block flag only applies where it can: the per-monomorphisation path needs at least one type parameter, so a non-generic import in the block is left alone and binds through the ordinary single shim. A block can therefore mix the two freely:

#![allow(unused)]
fn main() {
#[wasm_bindgen(generic_per_mono)]
extern "C" {
    fn log_generic<T>(x: T); // per-monomorphisation
    fn log_u32(x: u32);      // ordinary import, unaffected
}
}

Writing generic_per_mono directly on a non-generic function is still an error, since there you asked for something that cannot be done.

Unsupported shapes

These are rejected at compile time with a diagnostic pointing at the offending declaration. Each generally keeps working on the type-erasure path, so the fix is usually to drop generic_per_mono:

  • Generic parameters on the imported type (class-level generics), whether a type parameter (this: &Holder<T>) or a lifetime (this: &Holder<'a>). Lifetime parameters on the function itself — including on a method’s receiver, this: &'a Holder — are supported; see Lifetime parameters.
  • A mutable reference to a type parameter (&mut T, or &mut Vec<T>, or any other &mut whose referent mentions a type parameter), and a reference to a type parameter nested inside another type (e.g. Option<&T>). A bare &T is supported, and mutable references to concrete types (e.g. &mut [u16], &mut dyn FnMut(u32)) bind exactly as they do on the non-generic import path — the restriction is only about references to type parameters.
  • Returning a reference.
  • A bare type parameter, or a reference to one (&T), as the variadic argument, since it may monomorphise to a scalar, which is not spreadable.
  • A type parameter in the error position of a catch import (Result<T, E> with generic E): only the Ok type is monomorphised, and the error type is always JsValue.
  • slice_to_array on a slice whose element type mentions a type parameter (&[T], &[Vec<T>], Option<&[T]>). VectorRefIntoWasmAbi is implemented per concrete ABI shape, so no bound the caller can write makes an arbitrary T satisfy it; the element type must be concrete. See slice_to_array.
  • reexport, which has no well-defined target when one binding is manufactured per monomorphisation.
  • assert_no_shim, which asserts that no shim function is generated for the import. Per-monomorphisation codegen can never satisfy that, because it manufactures one shim per instantiation by construction, so the combination is rejected rather than silently ignored.
  • An argument whose pattern is not a plain name or _ (for example a tuple pattern such as fn f<T>((a, b): (u32, u32), x: T)), reported as unsupported pattern in generic_per_mono imported function. The generated per-monomorphisation shim has to forward each argument by name, so it needs a binding it can name. Give the argument a single identifier instead.

Const generic parameters are also rejected, but not by generic_per_mono: wasm-bindgen does not support them on any generic import, erased or not, and reports unsupported in wasm-bindgen generics. Dropping generic_per_mono will not help.

Colliding imports

Everything above is reported by the macro, at compile time. One failure is reported later, by the wasm-bindgen CLI, because it cannot be detected until the whole module is linked: two generic_per_mono imports that agree on every input to the shim key and differ only in an attribute that the key does not hash.

A monomorphisation records only a shim key, which is a hash over the Rust function name, the js_name (when one is given), the js_namespace, the signature tokens, the module, and any cfg attributes. It does not cover catch, variadic, slice_to_array, structural/final, or the getter/setter accessor kind. Two imports differing only in one of those claim the same key, and the CLI cannot tell which binding a given instantiation meant. Rather than silently binding one of them, which would mis-bind every monomorphisation of the other, it fails with an error naming both JS targets.

To fix it, make the two distinguishable on the Rust side: rename one of the Rust functions, adding js_name to keep the JS-visible name unchanged, or give them different signatures.

Note on &T arguments

A bare &T argument is supported, and requires the referent to satisfy the bound wasm-bindgen needs to marshal it — JsValue or a JS handle type. Passing &SomeStruct for a plain Rust struct, or &u32 for a scalar, is rejected, since there is no &T ABI representation for them; take the value by value, or pass a JS type.