-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Conversation
Hm, actually I think the original WasmResult class idea was better and doesn't have the limitations this does/works for current code without changes. But this is is a better starting point than using macro-expanded wasm_bindgen output in the wasm_bindgen crate, or something. The tricky thing is getting arbitrary |
Thanks for putting this together! I'm trying to understand this at a high-level before diving too much into the details, and one of the things I'm wondering is whether this is a breaking change? It sounds like this is funneling returns into JS values/objects where it wasn't before, or this is otherwise limiting return values which previously weren't limited before? |
Thanks for having a look. I owe a lot to this library (my job!) and I hope I can give back. As it stands, this is not a breaking change. It only applies when you use JsError instead of JsValue, and JsError is a new Rust type so nobody is using it yet. The added test file notes that the old style However, fixing the old style results as well would require first adding the original WasmResult idea instead of checking
Like the JsError version, both options involve converting to JsValue before returning, they only differ in how. If anyone wants to build a solution using multiple return values, they are welcome to and it would probably be the best implementation by miles, but my own requirements include targeting an ancient version of Firefox, so I can't use that and really neither can anyone else yet. I have built most of option 1, will probably PR separately so you can see it and weigh up whether a breaking change is worthwhile. Some general observations about them:
|
👏 I wish more people explained their work so meticulously. |
Ok thanks for the explanation! It's good to know that this doesn't break things. I realize that the previous design isn't great but I would prefer to not break existing users. Naively though what I would expect is that this: impl<T: IntoWasmAbi> ReturnWasmAbi for Result<T, JsValue> would probably change to impl<T: IntoWasmAbi, U: Into<JsValue>> ReturnWasmAbi for Result<T, U> and the "abi" would be something like: #[repr(C)]
pub struct ResultAbi<T> {
is_ok: u32,
abi: ResultAbiUnion<T>,
}
#[repr(C)]
pub union ResultAbiUnion<T> {
ok: T,
err: u32,
} where One of the things I'm worried about with the system you describe is something like |
That's an interesting idea, it pretty neatly addresses the shim problem and the RustStruct problem at the same time. Nicely done. Regarding the edge cases with JsError, I think simply removing any custom implementation and relying on the generic one would resolve them. (Edit, duh, this is what you mean.) JsError would then simply be a friendly helper type for creating Errors with no effect on ABI. I like that. |
Ah yeah I was hoping that with an explicit discriminant of sorts we don't have to check types and that way I think if this works out we can update most documentation and examples to having |
You reckon ditch the Also, as for custom JS error subclasses, with the error bound expanded to Final thing to address is async. |
Hm that's true, given how fundamental errors are I can see having them in For |
Is it correct that |
7c1594a
to
e06c4d4
Compare
Alright, this works overall pretty well! Assuming that I'm right about JsValue/OptionIntoWasmAbi. But there wasm-bindgen/crates/cli-support/src/wit/mod.rs Lines 1347 to 1358 in e447729
wasm-bindgen/crates/cli-support/src/js/binding.rs Lines 598 to 611 in e447729
This gives you: #[wasm_bindgen]
fn return_option_err() -> Result<Option<f64>, JsError> { ... } module.exports.return_option_err = function() {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-32);
wasm.return_option_err(retptr);
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getFloat64Memory0()[retptr / 8 + 1];
var r2 = getInt32Memory0()[retptr / 4 + 2];
var err0 = r2;
if (err0 !== 0) { throw takeObject(err0); }
return r0 === 0 ? undefined : r1;
} finally {
wasm.__wbindgen_add_to_stack_pointer(32);
}
}; Those offsets are nonsensical. We needed a miniature version of the C struct packing algorithm. |
0ca82ad
to
aa7f415
Compare
fn () -> Result<T, JsError>
that doesn't leak stack spacefn () -> Result<T, JsValue>
leaking stack space
For now this was explicitly omitted because otherwise bindings have no way of differentiating
Hm yeah it's been a long time since I look at all this, but I think we can probably do some tricks to make this easy for us and not try to reimplement C struct layouts. For example the discriminant could be forced to a u64 to force 8-byte alignment of the rest of the fields, and loading the rest of the fields should hopefully just need to factor in a new offset. I think some low-level instructions may need tweaking to do this but I would hope that we could load the raw values with a new optionally-specified offset or something like that to handle this. |
Ah, I will flip it back to use a separate discriminant then.
I chose to place the JsValue's u32 at the end and reduce the problem of getting its value off the stack down to
The type in question (not having fixed the JsValue zero thing yet) is I agree full-on struct packing is overboard. We only need to generate aligned glue code for WasmAbis we know about, so the instruction builder has perfect knowledge of where the fields should be, and it would be unnecessary (however cool!) to expose this via eg WasmDescribe. All you need to do is communicate this information from the instruction builder to the stack reader, for each field you know needs tweaking. I don't think you would need to actually use a u64, for instance, just annotate the adapter types.
Review the use of f64 within other structures (basically Option), a bit of fiddling with the sanity checks on inputs.len or whatever, and I think that would do it and be a little bit future proof. |
Hm so another option is something like I also think it's fine to place any field wherever, what you're doing already it seems for whatever's easiest feels like the best way to go to me. |
Nah, the Float64Array view of the wasm memory can only do aligned reads. You'd have copy from a UInt32Array view into a buffer and reinterpret as f64. |
Oh I since learned awhile back that there's |
basic wasmresult support one WasmResult ctor per WasmAbi Remove WasmResult class
impl OptionIntoWasmAbi for JsValue
remove console_log
describe Result<_, JsError> implement Intrinsics::ErrorNew Revert impl From<()> for JsValue (src/lib.rs) impl WasmDescribe etc for JsError remove unused JsError
This last thing, I am finally out of my depth, but:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking really good to me, I think the only semi-major thing is what you've pointed out, but otherwise I'm quite happy with how this is shaping up!
// 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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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).
Now we do not leak externrefs, as the take function calls drop on them.
There was a crash where _outgoing inserted more than one instruction, e.g. for string. In that case, the deferred free() call was using the wrong popped values, and tried to free nonsense formed by the is_ok/err pair. Now it does a similar idea, but without assuming exactly one instruction will be pushed by self._outgoing().
Alright there is ONE MORE.... consider this: module.exports.return_string_err = function() {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
wasm.return_string_err(retptr);
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
var r2 = getInt32Memory0()[retptr / 4 + 2];
var r3 = getInt32Memory0()[retptr / 4 + 3];
if (r3) {
throw takeObject(r2);
}
return getStringFromWasm0(r0, r1);
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
wasm.__wbindgen_free(r0, r1);
}
}; If you
|
Or actually, __wbindgen_free is fine to call with len=0. We can just set |
b76e3d6
to
e907229
Compare
This looks fantastic to me, thanks again for your work on this! I've got one remaining nit about terminology of quads vs bytes, but that's pretty minor and can be fixed in a follow-up if truly necessary. Otherwise I'm gonna go ahead and merge this. |
It was a pleasure working with you! If there are any issues related to this or other ABI composition problems, feel free to ping me. (Also, you can probably also close #1742 now (edit: although the docs book needs updating).) |
This stack space leak has been a plague on so many projects for years. Thank you so much for helping to solve this issue. I am eternally grateful ❤ |
During #81 `utils.rs` was refactored into many places and some wouldn't be using `error::JsError` which in wasm builds is a typedef for `js_sys::JsValue` and is its own struct for rust builds (to avoid panics in JsValue when used from non-wasm builds). Howeve, some places couldn't see these types and would pull it in directly from `wasm_bindgen::prelude` from the wildcare import in `lib.rs`. This caused an issue when bumping `wasm_bindgen` as the `from_str()` method is no longer in `js_sys::JsError` but still is in `JsValue`. Between now and #81 this would have also led to panics if many places when an error was returned when using CML from rust instead of wasm. In the future we should deprecate the direct usage of `js_sys::JsValue` and remove our `JsError` type on rust in favor for real rust errors (not just string wrappers). See rustwasm/wasm-bindgen#2710 >Expands the acceptable return-position types to Result<T, E> where T: IntoWasmAbi, E: Into<JsValue>, so you can easily throw your own custom error classes imported from JS which will greatly improve error handling from the rust side but this should be done after the rust/wasm split.
During #81 `utils.rs` was refactored into many places and some wouldn't be using `error::JsError` which in wasm builds is a typedef for `js_sys::JsValue` and is its own struct for rust builds (to avoid panics in JsValue when used from non-wasm builds). Howeve, some places couldn't see these types and would pull it in directly from `wasm_bindgen::prelude` from the wildcare import in `lib.rs`. This caused an issue when bumping `wasm_bindgen` as the `from_str()` method is no longer in `js_sys::JsError` but still is in `JsValue`. Between now and #81 this would have also led to panics if many places when an error was returned when using CML from rust instead of wasm. In the future we should deprecate the direct usage of `js_sys::JsValue` and remove our `JsError` type on rust in favor for real rust errors (not just string wrappers). See rustwasm/wasm-bindgen#2710 >Expands the acceptable return-position types to Result<T, E> where T: IntoWasmAbi, E: Into<JsValue>, so you can easily throw your own custom error classes imported from JS which will greatly improve error handling from the rust side but this should be done after the rust/wasm split.
Resolves rustwasm#1004 rustwasm#2710 added support for returning `Result<T, impl Into<JsValue>>` rather than just `Result<T, JsValue>`, but the `wasm-bindgen` guide still claims that only the latter is supported. This fixes that, and also fixes a mistake in the table for what forms `Result` can be returned in (it previously claimed that only `Option<Result<...>>` was supported, when in fact only a plain `Result<...>` is supported).
Resolves #1004 #2710 added support for returning `Result<T, impl Into<JsValue>>` rather than just `Result<T, JsValue>`, but the `wasm-bindgen` guide still claims that only the latter is supported. This fixes that, and also fixes a mistake in the table for what forms `Result` can be returned in (it previously claimed that only `Option<Result<...>>` was supported, when in fact only a plain `Result<...>` is supported).
Other than keep them in the stream. New returned struct is added. Also change the error type to JsError. See rustwasm/wasm-bindgen#2710 .
Fixes #1963
Overview
-> Result<T, JsValue>
by throwing it from within the wasm stack frameResult<T, E> where T: IntoWasmAbi, E: Into<JsValue>
, so you can easily throw your own custom error classes imported from JSJsError
that makes the common need forthrow new Error("message")
easier to use. It converts automatically fromstd::error::Error
for use with?
.Basically, while you can continue using
-> Result<T, JsValue>
and it will no longer leak, this is overall much better at its job:You can also use
?
with anystd::error::Error
implementation, e.g.anyhow::Error
, usingthiserror
on your own types, or just addingimpl core::fmt::Display for MyType { ... } impl std::error::Error for MyType {}
.The remaining questions are
Result<T, JsValue>
whereT: IntoWasmAbi
. That's distinct fromT: Into<JsValue>
(for example,#[wasm_bindgen] enum A { ... }
does not implementInto<JsValue>
), so you can't just change the impl toT: Into<JsValue>
.JsError
in exported async function signatures with minimal conversion should be enough.RuntimeError
.JsError
constructor that converts from aJsValue
without implementingFrom<JsValue>
lest?
convert any oldJsValue
without you meaning to.?
conversion perfectly on these while also allowing conversion fromT: std::error::Error
. You could pick one or the other. Or you could add a second typeJsErrorSubclass
or something, but that's getting a bit much.Obsolete
Overview (obsolete)
JsError
equivalent tojs_sys::Error
that implements conversions fromT
whereT: std::error::Error
usingT::to_string()
impl ReturnWasmAbi for Result<T, JsError>
distinct from theResult<T, JsValue>
, that just converts theJsError
to aJsValue
and returns it instead of usingwasm_bindgen::throw_val(e)
function throwIfError(x) { if (x instanceof Error) { throw x } }
let tmp0 = takeObject(...); throwIfError(tmp0); return tmp0
Notes
This is more lightweight than the
WasmResult
I described in the issue. It only works for Result types withT: Into<JsValue> + WasmDescribe
andE = JsError
. The outcome is similar toWasmResult
in that every result return involves creating aJsValue
(ok.into()
orJsError::from
something, e.g. via?
) but it does not require a wholeWasmResult
class in the glue code. The glue code just checks if the returned heap object happens to be aninstanceof Error
and throws the error if so. You get a reasonably good stack trace because the Error was created in a Rust stack frame.