JsRef typed JsValue implementation
This implements a new JsRef<T> typed version of JsValue, aliasing JsValue to JsRef<AnyType>, while maintaining full backwards compatibility with the existing JsValue semantics.
After various paths attempted, this should form a new foundation for the generics work.
There are two ways to obtain a JsRef<T> - by cast_unchecked or cast_ref_unchecked from another JsRef<T> or by JsRef::to_js(t) which takes T: Into<JsValue> and treats the inner T as the same as the input type. There is also a trait sugar for to_js applied for all Into<JsValue> traits allowing e.g. 25i32.to_js().
When unwrapping, wbg_cast is used to convert from the extern ref to the inner type, with the assumption of FromWasmAbi for a .from_js() call. For a fallible unwrap, TryFromJsValue can be used instead via try_from_js().
A JsValue can be obtained from a JsRef<T> via v.into_value() to use all the original JsValue methods. There are also as_value and to_value variants for referencing and cloning respectively.
The benefit of the typed value is that we can update existing functions that take &JsValue or JsValue to take a generic JsRef<T> while retaining backwards compatibility in that the function still accepts the untyped JsValue form. This effectively forms a dynamic runtime type system where types can be created and extracted very naturally. For example, creating a typed value from a string and then extracting the string again:
let str_val = JsRef::to_js(String::from("test"));
let str = str_val.from_js(); // or try_from_js() for Result
assert_eq!(&str, "test");
A shorthand trait ToJs is provided to allow more ergonomic usage as well:
let str_val = String::from("test").to_js();
let num_val = i32.to_js();
JsRef<T> can also be created from any type with explicit type annotation using to_js_as:
let val: JsRef<JsString> = JsRef::to_js_as("hello");
Which, despite effectively being a double conversion, can allow more efficient construction like the above.
By moving the JsValue generic casting down into the value type itself, this drastically simplifies the design of backwards-compatible import generics via type erasure, see https://github.com/wasm-bindgen/wasm-bindgen/pull/4756 for the draft approach.
It would be possible to call JsRef JsValue, but I hit an error in serde-wasm-bindgen which is using RefFromWasmAbi which would then require an explicit concrete typing using AnyType to build under the new version (v.ref_from_abi() becoming JsValue::<AnyType>::ref_from_abi(v) or even via a reverse alias like JsValueAny::ref_from_abi(v)). The aliased approach is slightly safer so that's what I've done for now, although this can still be discussed - since these are marked as unstable we could just have a comment noting the trivial upgrade path for those consumers too.
The important point here though is that a function that takes JsValue can be updated to take a JsRef<T> on T being generic, while still supporting the JsValue in a backwards compatible way, making this a foundation for generics.
Hm not too sure about this change to be honest.
It feels somewhat awkward and inconsistent to have to wrap all values into JsRef just to support generics, when rest of wasm-bindgen has always allowed to use native Rust types as-is.
The mental model gets even weirder in examples like JsRef<JsString>, when JsString already "inherits" (via deref-coercion) from JsValue and now we wrap it into another type that is also like JsValue.
What were the final blockers for getting generics with auto-conversion over the line? I'd much prefer if we could support Rust built-in types in generics like we do in all other places.
@RReverser the reason for splitting this out is that box types allow flexible import type generics when we don't have any other option, but because the boxed generic is now fully abstracted - it is entirely explicit and not part of the core generics semantics anymore. That is, the generics PR has no knowledge of JsRef<T> at all. Rather, this solves the problem of generic promise construction, since we need Promise::resolve(&JsRef<T>) for backwards compat. When possible we can then lean into generalized generics including on exported interfaces.
So the answer to your question is, this takes the parts you don't like about the existing generics PR, and moves them into an optional box type here, used only when we need a JsValue<T> flexible box.
I'll be posting up the generics update to this PR soon, but in the mean time please try to evaluate this PR on its own merits as enabling a box type for deeper generics support, the sooner we can get this landed the sooner we can get generics landed.
The counter argument effectively being that this is just an externref box with type information just because JsRef<T> exists doesn't mean every T is a JsRef<T>, and doesn't mean JsRef<T> isn't useful when you need a singular any type without type erasure.
since we need
Promise::resolve(&JsRef<T>)for backwards compat
To me it's unclear why we need this. Last time we discussed generics, we arrived at the solution where any T: IntoWasmAbi will be converted to JsValue implicitly so that we could still get the ideal Promise::resolve(T) interface.
That's why I'm asking if there were any new issues that didn't allow that approach, and why we need an explicit wrapper now instead.
JsRef<T>exists doesn't mean everyTis aJsRef<T>, and doesn't meanJsRef<T>isn't useful when you need a singular any type without type erasure.
That's why I raised this concern:
The mental model gets even weirder in examples like
JsRef<JsString>, whenJsStringalready "inherits" (via deref-coercion) fromJsValueand now we wrap it into another type that is also likeJsValue.
Not counting Rust primitives, in wasm-bindgen ecosystem all the types (e.g. all imported types, all user-defined exported structs and so on) are already typed externrefs as they all inherit from JsValue. That's why the API of having to wrap them yet again into another type just to indicate they're typed externrefs feels awkward as that's what the nested type already represented. It seems it only adds mental overhead to the API user.
I personally like having a concise rust representation of a typed externref.
I feel some of @RReverser's confusion around the mental model getting a bit fuzzy when it comes to understanding JsRef<JsString>'s relationship with JsValue when JsString already has an javascript inheritance relation via coercion.
I was wondering if it made sense to alias all the existing js_sys types such that JsString = JsRefinternal::JsString, etc? Then users wouldn't necessarily have to directly use JsRef if they didn't want to.
Anyway, backwards comparibility is hard, and I think this pr sets us up well for incremental migration.
I was wondering if it made sense to alias all the existing js_sys types such that JsString = JsRefinternal::JsString, etc? Then users wouldn't necessarily have to directly use JsRef if they didn't want to.
Do you mean type JsString = JsRef<String>;? Yeah that would definitely solve my double-wrapping concern.
The other one was that it doesn't feel great to have to pass explicit JsRef everywhere in public APIs - I'd still like us to support native Rust types like we support in other places. But that's best to discuss on a design doc for generics which @guybedford said he can write up, as we've had too many disconnected conversations in different places by now and it's hard to keep track.
To give some more context here as to why Promise<T> is hard for primitive types is because there is no IntoWasmAbi and FromWasmAbi concrete trait implementation that works for all values of T. This is a requirement for generics on extern C blocks which must have concrete implementations independent of T. Thus JsRef<T> allows unification on the Promise<JsValue> path without incurring additional extra wrapping.
With the latest design, this PR is entirely only a usability improvement to generics, and IS NOT REQUIRED FOR THE GENERICS PR. It's now just a primitive value convenience. Perhaps that was the confusion here.
CodSpeed Performance Report
Merging #4746 will not alter performance
Comparing jsvalue-generics (63b7467) with main (96f3e1e)
Summary
✅ 4 untouched