Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix fn () -> Result<T, JsValue> leaking stack space #2710

Merged
merged 32 commits into from
Dec 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4f794df
add Descriptor RESULT and Instruction::UnwrapResult
cormacrelf Jan 18, 2021
c3de03b
ResultAbi / ResultAbiUnion
cormacrelf Nov 3, 2021
9676d51
Reverse the fields on ResultAbi, remove is_ok as err can be 0
cormacrelf Nov 3, 2021
75c97b1
implement ResultAbi
cormacrelf Nov 3, 2021
5ce54c8
Add tests for `-> Result<T, JsError>`
cormacrelf Oct 27, 2021
3bf2f11
split result.rs tests in two
cormacrelf Nov 3, 2021
9662fbb
initial implementation of `-> Result<T, JsError>`
cormacrelf Jan 18, 2021
3fcec72
docs on JsError, move to lib.rs
cormacrelf Nov 3, 2021
490fc29
Add test for returning Result<(), _>
cormacrelf Nov 3, 2021
ec441f1
Add failing test for returning `Result<Option<f64>, _>`
cormacrelf Nov 3, 2021
5fbba71
Make LoadRetptr offsets factor in alignment and previously read values
cormacrelf Nov 3, 2021
98c31b6
Add a doc comment to UnwrapResult
cormacrelf Nov 3, 2021
61b1d59
Slight correction to a comment
cormacrelf Nov 3, 2021
29cb459
Better error message
cormacrelf Nov 3, 2021
b5d12e9
un-implement OptionIntoWasmAbi for JsValue, use discriminant instead
cormacrelf Dec 1, 2021
f0cef2c
Add some documentation from the PR discussion to ResultAbi
cormacrelf Dec 1, 2021
ab2d482
un-implement OptionIntoWasmAbi for &'a JsValue too
cormacrelf Dec 1, 2021
5b24678
bless some UI tests, not sure why
cormacrelf Dec 1, 2021
390a5a4
bless the CLI's output tests
cormacrelf Dec 1, 2021
bb892ca
fix indentation of if (is_ok === 0) { throw ... } code
cormacrelf Dec 1, 2021
0e588c1
add tests for async fn() -> Result<_, JsError> and custom error types
cormacrelf Dec 1, 2021
1e3dda7
cargo fmt
cormacrelf Dec 1, 2021
c755921
fix bug where takeObject was missing
cormacrelf Dec 1, 2021
a5bcf99
support externref in UnwrapResult
cormacrelf Dec 1, 2021
dec2d8a
add a WASM_BINDGEN_EXTERNREF=1 test to ci
cormacrelf Dec 1, 2021
81b4d9b
getFromExternrefTable -> takeFromExternrefTable
cormacrelf Dec 3, 2021
8df853e
rewrite outgoing_result
cormacrelf Dec 3, 2021
f7be77e
rename is_ok -> is_err, which makes generated glue easier to read
cormacrelf Dec 3, 2021
a7933aa
update ui tests
cormacrelf Dec 3, 2021
6880631
add a crashing (if you uncomment the throw) test of Result<String, _>
cormacrelf Dec 3, 2021
2205f69
add result-string reference test
cormacrelf Dec 3, 2021
e907229
Fix the crashy Result<String, _> by setting ptr/len to 0 on error
cormacrelf Dec 3, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ jobs:
- run: cargo test --target wasm32-unknown-unknown --test wasm --features serde-serialize
env:
WASM_BINDGEN_WEAKREF: 1
- run: cargo test --target wasm32-unknown-unknown
env:
WASM_BINDGEN_EXTERNREF: 1
NODE_ARGS: --experimental-wasm-reftypes


test_wasm_bindgen_windows:
name: "Run wasm-bindgen crate tests (Windows)"
Expand Down
3 changes: 3 additions & 0 deletions crates/cli-support/src/descriptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ tys! {
RUST_STRUCT
CHAR
OPTIONAL
RESULT
UNIT
CLAMPED
}
Expand Down Expand Up @@ -68,6 +69,7 @@ pub enum Descriptor {
RustStruct(String),
Char,
Option(Box<Descriptor>),
Result(Box<Descriptor>),
Unit,
}

Expand Down Expand Up @@ -133,6 +135,7 @@ impl Descriptor {
SLICE => Descriptor::Slice(Box::new(Descriptor::_decode(data, clamped))),
VECTOR => Descriptor::Vector(Box::new(Descriptor::_decode(data, clamped))),
OPTIONAL => Descriptor::Option(Box::new(Descriptor::_decode(data, clamped))),
RESULT => Descriptor::Result(Box::new(Descriptor::_decode(data, clamped))),
CACHED_STRING => Descriptor::CachedString,
STRING => Descriptor::String,
EXTERNREF => Descriptor::Externref,
Expand Down
10 changes: 10 additions & 0 deletions crates/cli-support/src/externref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ pub fn process(module: &mut Module) -> Result<()> {
aux.externref_table = Some(meta.table);
if module_needs_externref_metadata(&aux, section) {
aux.externref_alloc = meta.alloc;
aux.externref_drop = meta.drop;
aux.externref_drop_slice = meta.drop_slice;
}

Expand Down Expand Up @@ -108,6 +109,15 @@ pub fn process(module: &mut Module) -> Result<()> {
} => {
*table_and_alloc = meta.alloc.map(|id| (meta.table, id));
}

Instruction::UnwrapResult {
ref mut table_and_drop,
}
| Instruction::UnwrapResultString {
ref mut table_and_drop,
} => {
*table_and_drop = meta.drop.map(|id| (meta.table, id));
}
_ => continue,
};
}
Expand Down
3 changes: 3 additions & 0 deletions crates/cli-support/src/intrinsic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@ intrinsics! {
#[symbol = "__wbindgen_rethrow"]
#[signature = fn(Externref) -> Unit]
Rethrow,
#[symbol = "__wbindgen_error_new"]
#[signature = fn(ref_string()) -> Externref]
ErrorNew,
#[symbol = "__wbindgen_memory"]
#[signature = fn() -> Externref]
Memory,
Expand Down
77 changes: 72 additions & 5 deletions crates/cli-support/src/js/binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,16 +596,20 @@ fn instruction(js: &mut JsBuilder, instr: &Instruction, log_error: &mut bool) ->
}

Instruction::LoadRetptr { ty, offset, mem } => {
let (mem, size) = match ty {
AdapterType::I32 => (js.cx.expose_int32_memory(*mem), 4),
AdapterType::F32 => (js.cx.expose_f32_memory(*mem), 4),
AdapterType::F64 => (js.cx.expose_f64_memory(*mem), 8),
let (mem, quads) = match ty {
AdapterType::I32 => (js.cx.expose_int32_memory(*mem), 1),
AdapterType::F32 => (js.cx.expose_f32_memory(*mem), 1),
AdapterType::F64 => (js.cx.expose_f64_memory(*mem), 2),
other => bail!("invalid aggregate return type {:?}", other),
};
let size = quads * 4;
// Separate the offset and the scaled offset, because otherwise you don't guarantee
// that the variable names will be unique.
let scaled_offset = offset / quads;
// If we're loading from the return pointer then we must have pushed
// it earlier, and we always push the same value, so load that value
// here
let expr = format!("{}()[retptr / {} + {}]", mem, size, offset);
let expr = format!("{}()[retptr / {} + {}]", mem, size, scaled_offset);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the changes in this block still necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. The offsets placed in the generated LoadRetptr instructions have changed from .enumerate() indexes, into correct quad offsets. So before, Optionalf64 worked by a fluke, because the f64 LoadRetptr had offset=1 size=8, and the f64 field was aligned to 8 bytes, and that happened to line up. Now, the same situation gives you offset=2. So this code has to change at least a little bit, in order to get f64 LoadRetptrs to divide that quad offset by 2 to get an offset into the Float64Array.

The variable names uniqueness is the rest of it, we introduce scaled_offset to keep offset for use in the var r{} = ... below, as offset is unique to each LoadRetptr but scaled_offset is not (sometimes being divided by two).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm sorry so discuss this further, I don't really understand why the term "quad" is being used in wasm-bindgen. I think it would make more sense for everything to be byte-based instead of quad-based. I realize that wasm-bindgen only works with 4 and 8-byte things but I think it makes more sense to still use bytes internally instead of bytes some places and quads/etc other places

Copy link
Contributor Author

@cormacrelf cormacrelf Dec 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough, it would be better with only bytes. I'll submit a separate PR, it requires modifying this function alongside the StructUnpacker. Fortunately doing so is at most a glue code variable name change (as opposed to an ABI change).

js.prelude(&format!("var r{} = {};", offset, expr));
js.push(format!("r{}", offset));
}
Expand Down Expand Up @@ -783,6 +787,69 @@ fn instruction(js: &mut JsBuilder, instr: &Instruction, log_error: &mut bool) ->
js.push(format!("len{}", i));
}

Instruction::UnwrapResult { table_and_drop } => {
let take_object = if let Some((table, drop)) = *table_and_drop {
js.cx
.expose_take_from_externref_table(table, drop)?
.to_string()
} else {
js.cx.expose_take_object();
"takeObject".to_string()
};
// is_err is popped first. The original layout was: ResultAbi {
// abi: ResultAbiUnion<T>,
// err: u32,
// is_err: u32,
// }
// So is_err is last to be added to the stack.
let is_err = js.pop();
let err = js.pop();
js.prelude(&format!(
"
if ({is_err}) {{
throw {take_object}({err});
}}
",
take_object = take_object,
is_err = is_err,
err = err,
));
}

Instruction::UnwrapResultString { table_and_drop } => {
let take_object = if let Some((table, drop)) = *table_and_drop {
js.cx
.expose_take_from_externref_table(table, drop)?
.to_string()
} else {
js.cx.expose_take_object();
"takeObject".to_string()
};
let is_err = js.pop();
let err = js.pop();
let len = js.pop();
let ptr = js.pop();
let i = js.tmp();
js.prelude(&format!(
"
var ptr{i} = {ptr};
var len{i} = {len};
if ({is_err}) {{
ptr{i} = 0; len{i} = 0;
throw {take_object}({err});
}}
",
take_object = take_object,
is_err = is_err,
err = err,
i = i,
ptr = ptr,
len = len,
));
js.push(format!("ptr{}", i));
js.push(format!("len{}", i));
}

Instruction::OptionString {
mem,
malloc,
Expand Down
33 changes: 33 additions & 0 deletions crates/cli-support/src/js/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2194,6 +2194,34 @@ impl<'a> Context<'a> {
true
}

fn expose_take_from_externref_table(
&mut self,
table: TableId,
drop: FunctionId,
) -> Result<MemView, Error> {
let view = self.memview_table("takeFromExternrefTable", table);
assert!(self.config.externref);
if !self.should_write_global(view.to_string()) {
return Ok(view);
}
let drop = self.export_name_of(drop);
let table = self.export_name_of(table);
self.global(&format!(
"
function {view}(idx) {{
const value = wasm.{table}.get(idx);
wasm.{drop}(idx);
return value;
}}
",
view = view,
table = table,
drop = drop,
));

Ok(view)
}
cormacrelf marked this conversation as resolved.
Show resolved Hide resolved

fn expose_add_to_externref_table(
&mut self,
table: TableId,
Expand Down Expand Up @@ -3141,6 +3169,11 @@ impl<'a> Context<'a> {
format!("throw {}", args[0])
}

Intrinsic::ErrorNew => {
assert_eq!(args.len(), 1);
format!("new Error({})", args[0])
}

Intrinsic::Module => {
assert_eq!(args.len(), 0);
if !self.config.mode.no_modules() && !self.config.mode.web() {
Expand Down
1 change: 1 addition & 0 deletions crates/cli-support/src/wit/incoming.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ impl InstructionBuilder<'_, '_> {
Descriptor::Function(_) |
Descriptor::Closure(_) |

Descriptor::Result(_) |
// Always behind a `Ref`
Descriptor::Slice(_) => bail!(
"unsupported argument type for calling Rust function from JS: {:?}",
Expand Down
49 changes: 47 additions & 2 deletions crates/cli-support/src/wit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1346,9 +1346,11 @@ impl<'a> Context<'a> {
});
if uses_retptr {
let mem = ret.cx.memory()?;
for (i, ty) in ret.input.into_iter().enumerate() {
let mut unpacker = StructUnpacker::new();
for ty in ret.input.into_iter() {
let offset = unpacker.read_ty(&ty)?;
instructions.push(InstructionData {
instr: Instruction::LoadRetptr { offset: i, ty, mem },
instr: Instruction::LoadRetptr { offset, ty, mem },
stack_change: StackChange::Modified {
pushed: 1,
popped: 0,
Expand Down Expand Up @@ -1569,3 +1571,46 @@ fn verify_schema_matches<'a>(data: &'a [u8]) -> Result<Option<&'a str>, Error> {
fn concatenate_comments(comments: &[&str]) -> String {
comments.iter().map(|&s| s).collect::<Vec<_>>().join("\n")
}

/// The C struct packing algorithm, in terms of u32.
struct StructUnpacker {
next_offset: usize,
}

impl StructUnpacker {
fn new() -> Self {
Self { next_offset: 0 }
}
fn align_up(&mut self, alignment_pow2: usize) -> usize {
let mask = alignment_pow2 - 1;
self.next_offset = (self.next_offset + mask) & (!mask);
self.next_offset
}
fn append(&mut self, quads: usize, alignment_pow2: usize) -> usize {
let ret = self.align_up(alignment_pow2);
self.next_offset += quads;
ret
}
/// Returns the offset for this member, with the offset in multiples of u32.
fn read_ty(&mut self, ty: &AdapterType) -> Result<usize, Error> {
let (quads, alignment) = match ty {
AdapterType::I32 | AdapterType::U32 | AdapterType::F32 => (1, 1),
AdapterType::F64 => (2, 2),
other => bail!("invalid aggregate return type {:?}", other),
};
Ok(self.append(quads, alignment))
}
}

#[test]
fn test_struct_packer() {
let mut unpacker = StructUnpacker::new();
let i32___ = &AdapterType::I32;
let double = &AdapterType::F64;
let mut read_ty = |ty| unpacker.read_ty(ty).unwrap();
assert_eq!(read_ty(i32___), 0); // u32
assert_eq!(read_ty(i32___), 1); // u32
assert_eq!(read_ty(double), 2); // f64, already aligned
assert_eq!(read_ty(i32___), 4); // u32, already aligned
assert_eq!(read_ty(double), 6); // f64, NOT already aligned, skips up to offset 6
}
1 change: 1 addition & 0 deletions crates/cli-support/src/wit/nonstandard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ pub struct WasmBindgenAux {
pub externref_table: Option<walrus::TableId>,
pub function_table: Option<walrus::TableId>,
pub externref_alloc: Option<walrus::FunctionId>,
pub externref_drop: Option<walrus::FunctionId>,
pub externref_drop_slice: Option<walrus::FunctionId>,

/// Various intrinsics used for JS glue generation
Expand Down
Loading