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&mutwhose referent mentions a type parameter), and a reference to a type parameter nested inside another type (e.g.Option<&T>). A bare&Tis 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 thevariadicargument, since it may monomorphise to a scalar, which is not spreadable. - A type parameter in the error position of a
catchimport (Result<T, E>with genericE): only theOktype is monomorphised, and the error type is alwaysJsValue. slice_to_arrayon a slice whose element type mentions a type parameter (&[T],&[Vec<T>],Option<&[T]>).VectorRefIntoWasmAbiis implemented per concrete ABI shape, so no bound the caller can write makes an arbitraryTsatisfy it; the element type must be concrete. Seeslice_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 asfn f<T>((a, b): (u32, u32), x: T)), reported asunsupported 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.