Skip to content

Commit 4d4851d

Browse files
Allow creating Vecs of and implement TryFrom<JsValue> for strings and exported Rust types (#3554)
* Enable passing String vectors and boxed slices across ABI This is accomplished via conversion of the Strings to/from JsValues. * Enable passing custom struct vectors over ABI This was done by converting the structs to/from JsValues. It was necessary to change the way relevant traits (e.g. WasmDescribe, IntoWasmAbi etc.) are implemented. It's impossible to implement these for `Box<[#name]>` in codegen.rs because implementing traits on generic types is only allowed in the crate in which the trait is defined. Naively adding a blanket implementation on the wasm-bindgen side doesn't work either because it conflicts with the implementation for JsObjects. The solution was to create traits like VectorIntoWasmAbi etc. that are defined on the concrete type and contain the implementation for IntoWasmAbi etc. for vectors of that type. JsObjects are blanket implemented as before, and struct types are implemented in codegen.rs. Due to the way these traits are defined, Rust requires implementing types to be Sized, so they can't be used for the existing String implementations. Converting structs from JsValues was accomplished by adding an unwrap function to the generated JavaScript class, and calling that from Rust. * Remove unneeded require * Move uses out of if_std * Add documentation * Move incorrect use statements * Fix mistake in comment * Throw on invalid array elements instead of silently removing them I put some work into making sure that you can tell what function the error message is coming from. You still have to dig into the call stack of the error message to see it, but hopefully that's not too big an ask? * Get rid of `JsValueVector` The main reason for this, which I didn't mention before, is that I found it really confusing when I was originally reviewing this PR what the difference was between `JsValueVector` and `Vector{From,Into}WasmAbi`, since it really looks like another conversion trait at first glance. * Respect the configured `wasm_bindgen` crate path * Change the error type for String and rust structs' TryFrom<JsValue> impls to JsValue * test string vecs too * Refactor `String` impls I moved the `TryFrom<JsValue>` impl out of convert/slices.rs, it doesn't really belong there, and also got rid of the `js_value_vectors!` macro in favour of just implementing it for `String` directly; there's not much point in a macro you only use for one type. * Don't require manual `OptionVector{From,Into}WasmAbi` impls I noticed that strings and rust structs weren't implementing `OptionVectorFromWasmAbi`, so I tried to make a failing test and... it worked. That threw me for a loop for a bit until I realised that it was because I'd used `Vec<T>`, and `Vec<T>`'s impls of `Option{From,Into}WasmAbi` didn't actually rely on `Box<[T]>` implementing the traits: they just required that it implemented `{From,Into}WasmAbi` with an ABI of `WasmSlice`, since from there the element type doesn't matter. So then I thought 'well, why not do that for `Box<[T]>` too? so that's how this commit's pile of new tests came to be. * fix clippy * Fix generated typescript Since vecs of strings and rust structs were describing themselves as `Box<[JsValue]>`, they got typed as such - as `any[]`. This fixes that by using `NAMED_EXTERNREF` instead of just plain `EXTERNREF` with the type we want. This is maybe _slightly_ sketchy, since `NAMED_EXTERNREF` is meant for imported JS types, but ehhh it's fine. You can already put arbitrary TypeScript in there with `typescript_type`. * reorder some impls This is the nitpickiest of nitpicks, but this is my PR goddammit and I can do what I want :) * Update schema hash I didn't actually bump the schema version because it didn't change. If you don't use the `TryFrom<JsValue>` impl for Rust structs (or pass a `Vec` of them from JS to Rust), using an old CLI version will work fine; if you do though, you get a bit of a cryptic error message: ``` error: import of `__wbg_foo_unwrap` doesn't have an adapter listed ``` (That's from trying to pass a `Vec<Foo>` from JS to Rust.) So idk, maybe that's worth bumping the schema version over anyway? * undo some unnecessary refactors * don't pointlessly use assert.deepStrictEqual for numbers * Update the guide * update reference tests * add WASI check * Extremely nitpicky tweaks --------- Co-authored-by: Billy Bradley <[email protected]> Co-authored-by: Billy Bradley <[email protected]>
1 parent 86fd961 commit 4d4851d

File tree

23 files changed

+511
-85
lines changed

23 files changed

+511
-85
lines changed

crates/backend/src/codegen.rs

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,10 @@ impl ToTokens for ast::Struct {
164164
let name = &self.rust_name;
165165
let name_str = self.js_name.to_string();
166166
let name_len = name_str.len() as u32;
167-
let name_chars = name_str.chars().map(|c| c as u32);
167+
let name_chars: Vec<u32> = name_str.chars().map(|c| c as u32).collect();
168168
let new_fn = Ident::new(&shared::new_function(&name_str), Span::call_site());
169169
let free_fn = Ident::new(&shared::free_function(&name_str), Span::call_site());
170+
let unwrap_fn = Ident::new(&shared::unwrap_function(&name_str), Span::call_site());
170171
let wasm_bindgen = &self.wasm_bindgen;
171172
(quote! {
172173
#[automatically_derived]
@@ -293,6 +294,76 @@ impl ToTokens for ast::Struct {
293294
#[inline]
294295
fn is_none(abi: &Self::Abi) -> bool { *abi == 0 }
295296
}
297+
298+
#[allow(clippy::all)]
299+
impl #wasm_bindgen::__rt::core::convert::TryFrom<#wasm_bindgen::JsValue> for #name {
300+
type Error = #wasm_bindgen::JsValue;
301+
302+
fn try_from(value: #wasm_bindgen::JsValue)
303+
-> #wasm_bindgen::__rt::std::result::Result<Self, Self::Error> {
304+
let idx = #wasm_bindgen::convert::IntoWasmAbi::into_abi(&value);
305+
306+
#[link(wasm_import_module = "__wbindgen_placeholder__")]
307+
#[cfg(all(target_arch = "wasm32", not(any(target_os = "emscripten", target_os = "wasi"))))]
308+
extern "C" {
309+
fn #unwrap_fn(ptr: u32) -> u32;
310+
}
311+
312+
#[cfg(not(all(target_arch = "wasm32", not(any(target_os = "emscripten", target_os = "wasi")))))]
313+
unsafe fn #unwrap_fn(_: u32) -> u32 {
314+
panic!("cannot convert from JsValue outside of the wasm target")
315+
}
316+
317+
let ptr = unsafe { #unwrap_fn(idx) };
318+
if ptr == 0 {
319+
#wasm_bindgen::__rt::std::result::Result::Err(value)
320+
} else {
321+
// Don't run `JsValue`'s destructor, `unwrap_fn` already did that for us.
322+
#wasm_bindgen::__rt::std::mem::forget(value);
323+
unsafe {
324+
#wasm_bindgen::__rt::std::result::Result::Ok(
325+
<Self as #wasm_bindgen::convert::FromWasmAbi>::from_abi(ptr)
326+
)
327+
}
328+
}
329+
}
330+
}
331+
332+
impl #wasm_bindgen::describe::WasmDescribeVector for #name {
333+
fn describe_vector() {
334+
use #wasm_bindgen::describe::*;
335+
inform(VECTOR);
336+
inform(NAMED_EXTERNREF);
337+
inform(#name_len);
338+
#(inform(#name_chars);)*
339+
}
340+
}
341+
342+
impl #wasm_bindgen::convert::VectorIntoWasmAbi for #name {
343+
type Abi = <
344+
#wasm_bindgen::__rt::std::boxed::Box<[#wasm_bindgen::JsValue]>
345+
as #wasm_bindgen::convert::IntoWasmAbi
346+
>::Abi;
347+
348+
fn vector_into_abi(
349+
vector: #wasm_bindgen::__rt::std::boxed::Box<[#name]>
350+
) -> Self::Abi {
351+
#wasm_bindgen::convert::js_value_vector_into_abi(vector)
352+
}
353+
}
354+
355+
impl #wasm_bindgen::convert::VectorFromWasmAbi for #name {
356+
type Abi = <
357+
#wasm_bindgen::__rt::std::boxed::Box<[#wasm_bindgen::JsValue]>
358+
as #wasm_bindgen::convert::FromWasmAbi
359+
>::Abi;
360+
361+
unsafe fn vector_from_abi(
362+
js: Self::Abi
363+
) -> #wasm_bindgen::__rt::std::boxed::Box<[#name]> {
364+
#wasm_bindgen::convert::js_value_vector_from_abi(js)
365+
}
366+
}
296367
})
297368
.to_tokens(tokens);
298369

crates/cli-support/src/js/mod.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ pub struct ExportedClass {
7575
generate_typescript: bool,
7676
has_constructor: bool,
7777
wrap_needed: bool,
78+
unwrap_needed: bool,
7879
/// Whether to generate helper methods for inspecting the class
7980
is_inspectable: bool,
8081
/// All readable properties of the class
@@ -935,6 +936,20 @@ impl<'a> Context<'a> {
935936
));
936937
}
937938

939+
if class.unwrap_needed {
940+
dst.push_str(&format!(
941+
"
942+
static __unwrap(jsValue) {{
943+
if (!(jsValue instanceof {})) {{
944+
return 0;
945+
}}
946+
return jsValue.__destroy_into_raw();
947+
}}
948+
",
949+
name,
950+
));
951+
}
952+
938953
if self.config.weak_refs {
939954
self.global(&format!(
940955
"const {}Finalization = new FinalizationRegistry(ptr => wasm.{}(ptr >>> 0));",
@@ -2247,6 +2262,10 @@ impl<'a> Context<'a> {
22472262
require_class(&mut self.exported_classes, name).wrap_needed = true;
22482263
}
22492264

2265+
fn require_class_unwrap(&mut self, name: &str) {
2266+
require_class(&mut self.exported_classes, name).unwrap_needed = true;
2267+
}
2268+
22502269
fn add_module_import(&mut self, module: String, name: &str, actual: &str) {
22512270
let rename = if name == actual {
22522271
None
@@ -3213,6 +3232,14 @@ impl<'a> Context<'a> {
32133232
See https://rustwasm.github.io/wasm-bindgen/reference/cli.html#--split-linked-modules for details.", path))
32143233
}
32153234
}
3235+
3236+
AuxImport::UnwrapExportedClass(class) => {
3237+
assert!(kind == AdapterJsImportKind::Normal);
3238+
assert!(!variadic);
3239+
assert_eq!(args.len(), 1);
3240+
self.require_class_unwrap(class);
3241+
Ok(format!("{}.__unwrap({})", class, args[0]))
3242+
}
32163243
}
32173244
}
32183245

crates/cli-support/src/wit/mod.rs

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -934,17 +934,40 @@ impl<'a> Context<'a> {
934934
self.aux.structs.push(aux);
935935

936936
let wrap_constructor = wasm_bindgen_shared::new_function(struct_.name);
937-
if let Some((import_id, _id)) = self.function_imports.get(&wrap_constructor).cloned() {
937+
self.add_aux_import_to_import_map(
938+
&wrap_constructor,
939+
vec![Descriptor::I32],
940+
Descriptor::Externref,
941+
AuxImport::WrapInExportedClass(struct_.name.to_string()),
942+
)?;
943+
944+
let unwrap_fn = wasm_bindgen_shared::unwrap_function(struct_.name);
945+
self.add_aux_import_to_import_map(
946+
&unwrap_fn,
947+
vec![Descriptor::Externref],
948+
Descriptor::I32,
949+
AuxImport::UnwrapExportedClass(struct_.name.to_string()),
950+
)?;
951+
952+
Ok(())
953+
}
954+
955+
fn add_aux_import_to_import_map(
956+
&mut self,
957+
fn_name: &String,
958+
arguments: Vec<Descriptor>,
959+
ret: Descriptor,
960+
aux_import: AuxImport,
961+
) -> Result<(), Error> {
962+
if let Some((import_id, _id)) = self.function_imports.get(fn_name).cloned() {
938963
let signature = Function {
939964
shim_idx: 0,
940-
arguments: vec![Descriptor::I32],
941-
ret: Descriptor::Externref,
965+
arguments,
966+
ret,
942967
inner_ret: None,
943968
};
944969
let id = self.import_adapter(import_id, signature, AdapterJsImportKind::Normal)?;
945-
self.aux
946-
.import_map
947-
.insert(id, AuxImport::WrapInExportedClass(struct_.name.to_string()));
970+
self.aux.import_map.insert(id, aux_import);
948971
}
949972

950973
Ok(())

crates/cli-support/src/wit/nonstandard.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,11 @@ pub enum AuxImport {
336336
/// The Option may contain the contents of the linked file, so it can be
337337
/// embedded.
338338
LinkTo(String, Option<String>),
339+
340+
/// This import is a generated shim which will attempt to unwrap JsValue to an
341+
/// instance of the given exported class. The class name is one that is
342+
/// exported from the Rust/wasm.
343+
UnwrapExportedClass(String),
339344
}
340345

341346
/// Values that can be imported verbatim to hook up to an import.

crates/cli-support/src/wit/section.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,9 @@ fn check_standard_import(import: &AuxImport) -> Result<(), Error> {
273273
format!("wasm-bindgen specific link function for `{}`", path)
274274
}
275275
AuxImport::Closure { .. } => format!("creating a `Closure` wrapper"),
276+
AuxImport::UnwrapExportedClass(name) => {
277+
format!("unwrapping a pointer from a `{}` js class wrapper", name)
278+
}
276279
};
277280
bail!("import of {} requires JS glue", item);
278281
}

crates/macro/ui-tests/missing-catch.stderr

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ error[E0277]: the trait bound `Result<wasm_bindgen::JsValue, wasm_bindgen::JsVal
88
*const T
99
*mut T
1010
Box<[T]>
11-
Box<[f32]>
12-
Box<[f64]>
13-
Box<[i16]>
14-
Box<[i32]>
15-
Box<[i64]>
16-
and 35 others
11+
Clamped<T>
12+
Option<T>
13+
Option<f32>
14+
Option<f64>
15+
Option<i32>
16+
and $N others

crates/macro/ui-tests/traits-not-implemented.stderr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ error[E0277]: the trait bound `A: IntoWasmAbi` is not satisfied
1313
&'a (dyn Fn(A, B, C, D, E) -> R + 'b)
1414
&'a (dyn Fn(A, B, C, D, E, F) -> R + 'b)
1515
&'a (dyn Fn(A, B, C, D, E, F, G) -> R + 'b)
16-
and 84 others
16+
and $N others
1717
= note: this error originates in the attribute macro `wasm_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info)

crates/shared/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,13 @@ pub fn free_function(struct_name: &str) -> String {
165165
name
166166
}
167167

168+
pub fn unwrap_function(struct_name: &str) -> String {
169+
let mut name = "__wbg_".to_string();
170+
name.extend(struct_name.chars().flat_map(|s| s.to_lowercase()));
171+
name.push_str("_unwrap");
172+
name
173+
}
174+
168175
pub fn free_function_export_name(function_name: &str) -> String {
169176
function_name.to_string()
170177
}

crates/shared/src/schema_hash_approval.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
// If the schema in this library has changed then:
99
// 1. Bump the version in `crates/shared/Cargo.toml`
1010
// 2. Change the `SCHEMA_VERSION` in this library to this new Cargo.toml version
11-
const APPROVED_SCHEMA_FILE_HASH: &str = "12040133795598472740";
11+
const APPROVED_SCHEMA_FILE_HASH: &str = "5679641936258023729";
1212

1313
#[test]
1414
fn schema_version() {

guide/src/SUMMARY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
- [Imported JavaScript Types](./reference/types/imported-js-types.md)
5353
- [Exported Rust Types](./reference/types/exported-rust-types.md)
5454
- [`JsValue`](./reference/types/jsvalue.md)
55-
- [`Box<[JsValue]>`](./reference/types/boxed-jsvalue-slice.md)
55+
- [`Box<[T]>` and `Vec<T>`](./reference/types/boxed-slices.md)
5656
- [`*const T` and `*mut T`](./reference/types/pointers.md)
5757
- [Numbers](./reference/types/numbers.md)
5858
- [`bool`](./reference/types/bool.md)

guide/src/reference/types/boxed-jsvalue-slice.md renamed to guide/src/reference/types/boxed-slices.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
1-
# `Box<[JsValue]>`
1+
# `Box<[T]>` and `Vec<T>`
22

33
| `T` parameter | `&T` parameter | `&mut T` parameter | `T` return value | `Option<T>` parameter | `Option<T>` return value | JavaScript representation |
44
|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
55
| Yes | No | No | Yes | Yes | Yes | A JavaScript `Array` object |
66

7-
Boxed slices of imported JS types and exported Rust types are also supported. `Vec<T>` is supported wherever `Box<[T]>` is.
7+
You can pass boxed slices and `Vec`s of several different types to and from JS:
8+
9+
- `JsValue`s.
10+
- Imported JavaScript types.
11+
- Exported Rust types.
12+
- `String`s.
13+
14+
[You can also pass boxed slices of numbers to JS](boxed-number-slices.html),
15+
except that they're converted to typed arrays (`Uint8Array`, `Int32Array`, etc.)
16+
instead of regular arrays.
817

918
## Example Rust Usage
1019

src/convert/impls.rs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@ use core::mem::{self, ManuallyDrop};
44
use crate::convert::traits::WasmAbi;
55
use crate::convert::{FromWasmAbi, IntoWasmAbi, LongRefFromWasmAbi, RefFromWasmAbi};
66
use crate::convert::{OptionFromWasmAbi, OptionIntoWasmAbi, ReturnWasmAbi};
7-
use crate::{Clamped, JsError, JsValue};
7+
use crate::{Clamped, JsError, JsValue, UnwrapThrowExt};
8+
9+
if_std! {
10+
use std::boxed::Box;
11+
use std::convert::{TryFrom, TryInto};
12+
use std::fmt::Debug;
13+
use std::vec::Vec;
14+
}
815

916
unsafe impl WasmAbi for () {}
1017

@@ -321,7 +328,7 @@ impl IntoWasmAbi for () {
321328
/// - u32/i32/f32/f64 fields at the "leaf fields" of the "field tree"
322329
/// - layout equivalent to a completely flattened repr(C) struct, constructed by an in order
323330
/// traversal of all the leaf fields in it.
324-
///
331+
///
325332
/// This means that you can't embed struct A(u32, f64) as struct B(u32, A); because the "completely
326333
/// flattened" struct AB(u32, u32, f64) would miss the 4 byte padding that is actually present
327334
/// within B and then as a consequence also miss the 4 byte padding within A that repr(C) inserts.
@@ -386,3 +393,37 @@ impl IntoWasmAbi for JsError {
386393
self.value.into_abi()
387394
}
388395
}
396+
397+
if_std! {
398+
// Note: this can't take `&[T]` because the `Into<JsValue>` impl needs
399+
// ownership of `T`.
400+
pub fn js_value_vector_into_abi<T: Into<JsValue>>(vector: Box<[T]>) -> <Box<[JsValue]> as IntoWasmAbi>::Abi {
401+
let js_vals: Box<[JsValue]> = vector
402+
.into_vec()
403+
.into_iter()
404+
.map(|x| x.into())
405+
.collect();
406+
407+
js_vals.into_abi()
408+
}
409+
410+
pub unsafe fn js_value_vector_from_abi<T: TryFrom<JsValue>>(js: <Box<[JsValue]> as FromWasmAbi>::Abi) -> Box<[T]> where T::Error: Debug {
411+
let js_vals = <Vec<JsValue> as FromWasmAbi>::from_abi(js);
412+
413+
let mut result = Vec::with_capacity(js_vals.len());
414+
for value in js_vals {
415+
// We push elements one-by-one instead of using `collect` in order to improve
416+
// error messages. When using `collect`, this `expect_throw` is buried in a
417+
// giant chain of internal iterator functions, which results in the actual
418+
// function that takes this `Vec` falling off the end of the call stack.
419+
// So instead, make sure to call it directly within this function.
420+
//
421+
// This is only a problem in debug mode. Since this is the browser's error stack
422+
// we're talking about, it can only see functions that actually make it to the
423+
// final wasm binary (i.e., not inlined functions). All of those internal
424+
// iterator functions get inlined in release mode, and so they don't show up.
425+
result.push(value.try_into().expect_throw("array contains a value of the wrong type"));
426+
}
427+
result.into_boxed_slice()
428+
}
429+
}

0 commit comments

Comments
 (0)