Skip to content

Commit ef4303e

Browse files
authored
Fix references passed to async functions (#3188)
* Fix references passed to async functions Fixes #3167 #3167 was happening because references to `JsValue`s work by pushing the JS value onto the JS stack, and then popping it off again when the function returns. This breaks for async functions, because the `JsValue` needs to remain valid for the lifetime of the future, not just the initial synchronous call. To fix this, I introduced a new descriptor, `LONGREF`, which is used instead of `REF` in async functions and marks that the reference can't be invalidated after the function returns. I then made a `LONGREF` to a `JsValue` behave the same as an owned `JsValue`, while working like a regular `REF` for everything else. I also had to add a new version of `RefFromWasmAbi`, `LongRefFromWasmAbi`, to accomodate for `LONGREF`s to `JsValue`s requiring that the `JsValue` is destroyed from the Rust side. While working on that, I also noticed that mutable references to slices were broken in async functions. They work by copying the data from JS to Rust, and then copying it back after the function returns to apply the changes. For async functions, that means that it's copied back before the future is actually finished, and so the changes won't be applied properly. To fix that, I changed it so that the data is copied back from the Rust side when the anchor is dropped, which happens when the future resolves for async functions. I also bumped the schema version, since this is a breaking change in the interface between the macro and the CLI, even though the schema itself is unchanged. * Update UI tests * Use a separate instruction to lower the typed array reference `Instruction::MutableSliceToMemory` previously manually lowered the reference to the type array using `addHeapObject`, but that broke when reference types were enabled. The most direct solution would have been to check whether reference types were enabled and use `addToExternrefTable` instead if so, but using a separate instruction means that it gets picked up by `wasm-bindgen-externref-xform`, and an `externref` is passed directly to wasm.
1 parent 46a8ae1 commit ef4303e

File tree

18 files changed

+311
-92
lines changed

18 files changed

+311
-92
lines changed

crates/backend/src/codegen.rs

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,16 @@ impl ToTokens for ast::Struct {
242242
}
243243
}
244244

245+
#[automatically_derived]
246+
impl wasm_bindgen::convert::LongRefFromWasmAbi for #name {
247+
type Abi = u32;
248+
type Anchor = wasm_bindgen::__rt::Ref<'static, #name>;
249+
250+
unsafe fn long_ref_from_abi(js: Self::Abi) -> Self::Anchor {
251+
<Self as wasm_bindgen::convert::RefFromWasmAbi>::ref_from_abi(js)
252+
}
253+
}
254+
245255
#[automatically_derived]
246256
impl wasm_bindgen::convert::OptionIntoWasmAbi for #name {
247257
#[inline]
@@ -404,7 +414,7 @@ impl TryToTokens for ast::Export {
404414

405415
let mut argtys = Vec::new();
406416
for (i, arg) in self.function.arguments.iter().enumerate() {
407-
argtys.push(&arg.ty);
417+
argtys.push(&*arg.ty);
408418
let i = i + offset;
409419
let ident = Ident::new(&format!("arg{}", i), Span::call_site());
410420
let ty = &arg.ty;
@@ -426,16 +436,31 @@ impl TryToTokens for ast::Export {
426436
});
427437
}
428438
syn::Type::Reference(syn::TypeReference { elem, .. }) => {
429-
args.push(quote! {
430-
#ident: <#elem as wasm_bindgen::convert::RefFromWasmAbi>::Abi
431-
});
432-
arg_conversions.push(quote! {
433-
let #ident = unsafe {
434-
<#elem as wasm_bindgen::convert::RefFromWasmAbi>
435-
::ref_from_abi(#ident)
436-
};
437-
let #ident = &*#ident;
438-
});
439+
if self.function.r#async {
440+
args.push(quote! {
441+
#ident: <#elem as wasm_bindgen::convert::LongRefFromWasmAbi>::Abi
442+
});
443+
arg_conversions.push(quote! {
444+
let #ident = unsafe {
445+
<#elem as wasm_bindgen::convert::LongRefFromWasmAbi>
446+
::long_ref_from_abi(#ident)
447+
};
448+
let #ident = <<#elem as wasm_bindgen::convert::LongRefFromWasmAbi>
449+
::Anchor as core::borrow::Borrow<#elem>>
450+
::borrow(&#ident);
451+
});
452+
} else {
453+
args.push(quote! {
454+
#ident: <#elem as wasm_bindgen::convert::RefFromWasmAbi>::Abi
455+
});
456+
arg_conversions.push(quote! {
457+
let #ident = unsafe {
458+
<#elem as wasm_bindgen::convert::RefFromWasmAbi>
459+
::ref_from_abi(#ident)
460+
};
461+
let #ident = &*#ident;
462+
});
463+
}
439464
}
440465
_ => {
441466
args.push(quote! {
@@ -550,6 +575,22 @@ impl TryToTokens for ast::Export {
550575
})
551576
.to_tokens(into);
552577

578+
let describe_args: TokenStream = argtys
579+
.iter()
580+
.map(|ty| match ty {
581+
syn::Type::Reference(reference)
582+
if self.function.r#async && reference.mutability.is_none() =>
583+
{
584+
let inner = &reference.elem;
585+
quote! {
586+
inform(LONGREF);
587+
<#inner as WasmDescribe>::describe();
588+
}
589+
}
590+
_ => quote! { <#ty as WasmDescribe>::describe(); },
591+
})
592+
.collect();
593+
553594
// In addition to generating the shim function above which is what
554595
// our generated JS will invoke, we *also* generate a "descriptor"
555596
// shim. This descriptor shim uses the `WasmDescribe` trait to
@@ -573,7 +614,7 @@ impl TryToTokens for ast::Export {
573614
inform(FUNCTION);
574615
inform(0);
575616
inform(#nargs);
576-
#(<#argtys as WasmDescribe>::describe();)*
617+
#describe_args
577618
#describe_ret
578619
},
579620
attrs: attrs.clone(),
@@ -659,7 +700,7 @@ impl ToTokens for ast::ImportType {
659700
const #const_name: () = {
660701
use wasm_bindgen::convert::{IntoWasmAbi, FromWasmAbi};
661702
use wasm_bindgen::convert::{OptionIntoWasmAbi, OptionFromWasmAbi};
662-
use wasm_bindgen::convert::RefFromWasmAbi;
703+
use wasm_bindgen::convert::{RefFromWasmAbi, LongRefFromWasmAbi};
663704
use wasm_bindgen::describe::WasmDescribe;
664705
use wasm_bindgen::{JsValue, JsCast, JsObject};
665706
use wasm_bindgen::__rt::core;
@@ -731,6 +772,17 @@ impl ToTokens for ast::ImportType {
731772
}
732773
}
733774

775+
impl LongRefFromWasmAbi for #rust_name {
776+
type Abi = <JsValue as LongRefFromWasmAbi>::Abi;
777+
type Anchor = #rust_name;
778+
779+
#[inline]
780+
unsafe fn long_ref_from_abi(js: Self::Abi) -> Self::Anchor {
781+
let tmp = <JsValue as LongRefFromWasmAbi>::long_ref_from_abi(js);
782+
#rust_name { obj: tmp.into() }
783+
}
784+
}
785+
734786
// TODO: remove this on the next major version
735787
impl From<JsValue> for #rust_name {
736788
#[inline]

crates/cli-support/src/descriptor.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ tys! {
2828
STRING
2929
REF
3030
REFMUT
31+
LONGREF
3132
SLICE
3233
VECTOR
3334
EXTERNREF
@@ -89,7 +90,7 @@ pub struct Closure {
8990
pub mutable: bool,
9091
}
9192

92-
#[derive(Clone, Debug)]
93+
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
9394
pub enum VectorKind {
9495
I8,
9596
U8,
@@ -132,6 +133,15 @@ impl Descriptor {
132133
CLOSURE => Descriptor::Closure(Box::new(Closure::decode(data))),
133134
REF => Descriptor::Ref(Box::new(Descriptor::_decode(data, clamped))),
134135
REFMUT => Descriptor::RefMut(Box::new(Descriptor::_decode(data, clamped))),
136+
LONGREF => {
137+
// This descriptor basically just serves as a macro, where most things
138+
// become normal `Ref`s, but long refs to externrefs become owned.
139+
let contents = Descriptor::_decode(data, clamped);
140+
match contents {
141+
Descriptor::Externref | Descriptor::NamedExternref(_) => contents,
142+
_ => Descriptor::Ref(Box::new(contents)),
143+
}
144+
}
135145
SLICE => Descriptor::Slice(Box::new(Descriptor::_decode(data, clamped))),
136146
VECTOR => Descriptor::Vector(Box::new(Descriptor::_decode(data, clamped))),
137147
OPTIONAL => Descriptor::Option(Box::new(Descriptor::_decode(data, clamped))),

crates/cli-support/src/intrinsic.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ fn opt_i64() -> Descriptor {
8484
Descriptor::Option(Box::new(Descriptor::I64))
8585
}
8686

87+
fn slice(contents: Descriptor) -> Descriptor {
88+
Descriptor::Ref(Box::new(Descriptor::Slice(Box::new(contents))))
89+
}
90+
8791
intrinsics! {
8892
pub enum Intrinsic {
8993
#[symbol = "__wbindgen_jsval_eq"]
@@ -263,6 +267,9 @@ intrinsics! {
263267
#[symbol = "__wbindgen_json_serialize"]
264268
#[signature = fn(ref_externref()) -> String]
265269
JsonSerialize,
270+
#[symbol = "__wbindgen_copy_to_typed_array"]
271+
#[signature = fn(slice(U8), ref_externref()) -> Unit]
272+
CopyToTypedArray,
266273
#[symbol = "__wbindgen_externref_heap_live_count"]
267274
#[signature = fn() -> I32]
268275
ExternrefHeapLiveCount,

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

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -929,15 +929,8 @@ fn instruction(js: &mut JsBuilder, instr: &Instruction, log_error: &mut bool) ->
929929
js.push(format!("len{}", i));
930930
}
931931

932-
Instruction::MutableSliceToMemory {
933-
kind,
934-
malloc,
935-
mem,
936-
free,
937-
} => {
938-
// First up, pass the JS value into wasm, getting out a pointer and
939-
// a length. These two pointer/length values get pushed onto the
940-
// value stack.
932+
Instruction::MutableSliceToMemory { kind, malloc, mem } => {
933+
// Copy the contents of the typed array into wasm.
941934
let val = js.pop();
942935
let func = js.cx.pass_to_wasm_function(kind.clone(), *mem)?;
943936
let malloc = js.cx.export_name_of(*malloc);
@@ -950,25 +943,12 @@ fn instruction(js: &mut JsBuilder, instr: &Instruction, log_error: &mut bool) ->
950943
malloc = malloc,
951944
));
952945
js.prelude(&format!("var len{} = WASM_VECTOR_LEN;", i));
946+
// Then pass it the pointer and the length of where we copied it.
953947
js.push(format!("ptr{}", i));
954948
js.push(format!("len{}", i));
955-
956-
// Next we set up a `finally` clause which will both update the
957-
// original mutable slice with any modifications, and then free the
958-
// Rust-backed memory.
959-
let free = js.cx.export_name_of(*free);
960-
let get = js.cx.memview_function(kind.clone(), *mem);
961-
js.finally(&format!(
962-
"
963-
{val}.set({get}().subarray(ptr{i} / {size}, ptr{i} / {size} + len{i}));
964-
wasm.{free}(ptr{i}, len{i} * {size});
965-
",
966-
val = val,
967-
get = get,
968-
free = free,
969-
size = kind.size(),
970-
i = i,
971-
));
949+
// Then we give wasm a reference to the original typed array, so that it can
950+
// update it with modifications made on the wasm side before returning.
951+
js.push(val);
972952
}
973953

974954
Instruction::BoolFromI32 => {

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3498,6 +3498,15 @@ impl<'a> Context<'a> {
34983498
"JSON.stringify(obj === undefined ? null : obj)".to_string()
34993499
}
35003500

3501+
Intrinsic::CopyToTypedArray => {
3502+
assert_eq!(args.len(), 2);
3503+
format!(
3504+
"new Uint8Array({dst}.buffer, {dst}.byteOffset, {dst}.byteLength).set({src})",
3505+
src = args[0],
3506+
dst = args[1]
3507+
)
3508+
}
3509+
35013510
Intrinsic::ExternrefHeapLiveCount => {
35023511
assert_eq!(args.len(), 0);
35033512
self.expose_global_heap();

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

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,9 +204,13 @@ impl InstructionBuilder<'_, '_> {
204204
kind,
205205
malloc: self.cx.malloc()?,
206206
mem: self.cx.memory()?,
207-
free: self.cx.free()?,
208207
},
209-
&[AdapterType::I32, AdapterType::I32],
208+
&[AdapterType::I32, AdapterType::I32, AdapterType::Externref],
209+
);
210+
self.late_instruction(
211+
&[AdapterType::Externref],
212+
Instruction::I32FromExternrefOwned,
213+
&[AdapterType::I32],
210214
);
211215
} else {
212216
self.instruction(
@@ -373,6 +377,27 @@ impl InstructionBuilder<'_, '_> {
373377
self.output.extend_from_slice(outputs);
374378
}
375379

380+
/// Add an instruction whose inputs are the results of previous instructions
381+
/// instead of the parameters from JS / results from Rust.
382+
pub fn late_instruction(
383+
&mut self,
384+
inputs: &[AdapterType],
385+
instr: Instruction,
386+
outputs: &[AdapterType],
387+
) {
388+
for input in inputs {
389+
assert_eq!(self.output.pop().unwrap(), *input);
390+
}
391+
self.instructions.push(InstructionData {
392+
instr,
393+
stack_change: StackChange::Modified {
394+
popped: inputs.len(),
395+
pushed: outputs.len(),
396+
},
397+
});
398+
self.output.extend_from_slice(outputs);
399+
}
400+
376401
fn number(&mut self, input: wit_walrus::ValType, output: walrus::ValType) {
377402
let std = wit_walrus::Instruction::IntToWasm {
378403
input,

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ pub enum AdapterJsImportKind {
6565
Normal,
6666
}
6767

68-
#[derive(Debug, Clone)]
68+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6969
pub enum AdapterType {
7070
S8,
7171
S16,
@@ -185,7 +185,6 @@ pub enum Instruction {
185185
MutableSliceToMemory {
186186
kind: VectorKind,
187187
malloc: walrus::FunctionId,
188-
free: walrus::FunctionId,
189188
mem: walrus::MemoryId,
190189
},
191190

@@ -469,12 +468,9 @@ impl walrus::CustomSection for NonstandardWitSection {
469468
roots.push_memory(mem);
470469
roots.push_func(malloc);
471470
}
472-
MutableSliceToMemory {
473-
free, malloc, mem, ..
474-
} => {
471+
MutableSliceToMemory { malloc, mem, .. } => {
475472
roots.push_memory(mem);
476473
roots.push_func(malloc);
477-
roots.push_func(free);
478474
}
479475
VectorLoad { free, mem, .. }
480476
| OptionVectorLoad { free, mem, .. }
Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
error[E0277]: the trait bound `Result<wasm_bindgen::JsValue, wasm_bindgen::JsValue>: FromWasmAbi` is not satisfied
2-
--> $DIR/missing-catch.rs:6:9
3-
|
4-
6 | pub fn foo() -> Result<JsValue, JsValue>;
5-
| ^^^ the trait `FromWasmAbi` is not implemented for `Result<wasm_bindgen::JsValue, wasm_bindgen::JsValue>`
6-
|
2+
--> ui-tests/missing-catch.rs:6:9
3+
|
4+
6 | pub fn foo() -> Result<JsValue, JsValue>;
5+
| ^^^ the trait `FromWasmAbi` is not implemented for `Result<wasm_bindgen::JsValue, wasm_bindgen::JsValue>`
6+
|
77
note: required by a bound in `FromWasmAbi`
8-
--> $DIR/traits.rs:23:1
9-
|
10-
23 | / pub trait FromWasmAbi: WasmDescribe {
11-
24 | | /// The wasm ABI type that this converts from when coming back out from the
12-
25 | | /// ABI boundary.
13-
26 | | type Abi: WasmAbi;
14-
... |
15-
35 | | unsafe fn from_abi(js: Self::Abi) -> Self;
16-
36 | | }
17-
| |_^ required by this bound in `FromWasmAbi`
8+
--> $WORKSPACE/src/convert/traits.rs
9+
|
10+
| / pub trait FromWasmAbi: WasmDescribe {
11+
| | /// The wasm ABI type that this converts from when coming back out from the
12+
| | /// ABI boundary.
13+
| | type Abi: WasmAbi;
14+
... |
15+
| | unsafe fn from_abi(js: Self::Abi) -> Self;
16+
| | }
17+
| |_^ required by this bound in `FromWasmAbi`
Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
error[E0277]: the trait bound `A: IntoWasmAbi` is not satisfied
2-
--> $DIR/traits-not-implemented.rs:5:1
3-
|
4-
5 | #[wasm_bindgen]
5-
| ^^^^^^^^^^^^^^^ the trait `IntoWasmAbi` is not implemented for `A`
6-
|
2+
--> ui-tests/traits-not-implemented.rs:5:1
3+
|
4+
5 | #[wasm_bindgen]
5+
| ^^^^^^^^^^^^^^^ the trait `IntoWasmAbi` is not implemented for `A`
6+
|
77
note: required by a bound in `IntoWasmAbi`
8-
--> $DIR/traits.rs:9:1
9-
|
10-
9 | / pub trait IntoWasmAbi: WasmDescribe {
11-
10 | | /// The wasm ABI type that this converts into when crossing the ABI
12-
11 | | /// boundary.
13-
12 | | type Abi: WasmAbi;
14-
... |
15-
16 | | fn into_abi(self) -> Self::Abi;
16-
17 | | }
17-
| |_^ required by this bound in `IntoWasmAbi`
18-
= note: this error originates in the attribute macro `wasm_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info)
8+
--> $WORKSPACE/src/convert/traits.rs
9+
|
10+
| / pub trait IntoWasmAbi: WasmDescribe {
11+
| | /// The wasm ABI type that this converts into when crossing the ABI
12+
| | /// boundary.
13+
| | type Abi: WasmAbi;
14+
... |
15+
| | fn into_abi(self) -> Self::Abi;
16+
| | }
17+
| |_^ required by this bound in `IntoWasmAbi`
18+
= 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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ mod schema_hash_approval;
66
// This gets changed whenever our schema changes.
77
// At this time versions of wasm-bindgen and wasm-bindgen-cli are required to have the exact same
88
// SCHEMA_VERSION in order to work together.
9-
pub const SCHEMA_VERSION: &str = "0.2.83";
9+
pub const SCHEMA_VERSION: &str = "0.2.84";
1010

1111
#[macro_export]
1212
macro_rules! shared_api {

0 commit comments

Comments
 (0)