-
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
Remove class wrap for constructors in Rust exports #3562
Conversation
After rustwasm#1594 constructors of Rust exported structs started using class wrapping when generating JS shims. Wrapping erases prototype information from the object instance in JS and as a result it is not possible to override methods (via inheritance) of the generated class. Additionally, some checks to ensure constructors always return an instance of `Self` were lost. This PR is rebased from rustwasm#3561, it passes the constructor information from the `Builder` into the instruction translation function which is then used to modify the generated bindings. The `return` statement is also removed instead the value is directly attached to the instance. Signed-off-by: Oliver T <[email protected]>
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.
I think I phrased myself poorly in the last PR - I wasn't trying to say that the constructor for Foo
should accept either Foo
or Option<Foo>
, and disallow Bar
or Option<Bar>
, I was trying to say that Option<Foo>
shouldn't be allowed either. Foo
should be the only valid return type - Option<Foo>
, Bar
, and non-struct types such as String
should all be disallowed.
As for choosing to pass constructor
to instruction
: sure, that's fine, I think it's about as clean as my suggestion. You'll need to insert a check that the function has the right return type somewhere else though, since you can't tell whether it's the last instruction or not.
Oops, I just noticed the CI failure. That's because of this test: #[wasm_bindgen]
pub struct AsyncStruct;
#[wasm_bindgen]
impl AsyncStruct {
#[wasm_bindgen(constructor)]
pub async fn new() -> AsyncStruct {
AsyncStruct
}
pub async fn method(&self) -> u32 {
42
}
} It expects returning a Thinking back on it, I vaguely remember trying to fix this issue before, noticing that failing test and then bailing on it. (found it: 052281f) While that's probably the most sensible use of a constructor returning something other than the type it's a constructor for, I still don't think it should be allowed - just use a builder instead. I just added that test on a whim without really thinking about it. However, I'm now a bit worried that someone might already be relying on that, and this could actually cause breakage. Ugh, I don't know - the breakage could be avoided by falling back on the old behaviour if a constructor returns the wrong type (which is basically what the last PR did, now that I think about it), but it feels kind of crappy to be explicitly supporting this weirdness. I guess we could print some kind of deprecation warning, but that'd only be able to show up when you ran the CLI, not when using the macro... or we could just risk the breakage. Do you have any opinion? |
Initially, that's what I thought as well. Hence why in #3561 I inserted the check before generating the adapter. Because you can check the return type from the
This is why inserting the check at this point is a bit more cumbersome because you now need to peek into the instructions that are being passed to generate this adapter. But this should not be that big of a deal, so long as we allow things like If we instead decide to remove support for these, then it becomes trickier to do the check here since you need to know the sequence of instructions these types generate. E.g. |
Interestingly enough the test passes if you return fn instruction(
js: &mut JsBuilder,
instr: &Instruction,
log_error: &mut bool,
constructor: &Option<String>,
) -> Result<(), Error> {
match instr {
Instruction::RustFromI32 { class } => {
let val = js.pop();
if let Some(name) = constructor {
if name != class {
bail!("constructor for `{}` cannot return `{}`", name, class);
}
js.prelude(&format!("this.__wbg_ptr = {} >>> 0;", val));
js.push(String::from("this"));
} else {
js.cx.require_class_wrap(class);
js.push(format!("{}.__wrap({})", class, val));
}
}
}
}
I think there are already a lot of users relying on this behaviour! The ray-tracing example in the repository returns
I think it might be possible to query |
We could emit the warning from the macro by using something like proc-macro-warning. |
Ah, I didn't think of I also found an example of async constructors being used in the wild, so we probably need to support those too: https://github.com/RingsNetwork/rings-node/blob/master/node/src/browser/client.rs#L143-L153 So yeah, people are definitely already relying on this; let's not break it. I hadn't heard of sourcegraph before, thank you for pointing me to it! We might still want to disallow some types though, because JavaScript handles constructor returns kind of weirdly: class Foo {
constructor(x) {
this.a = 2;
return x;
}
}
console.log(new Foo(null)); // Foo { a: 2 }
console.log(new Foo(undefined)); // Foo { a: 2 }
console.log(new Foo("hi")); // Foo { a: 2 }
console.log(new Foo(12)); // Foo { a: 2 }
console.log(new Foo(() => {})); // [Function]
console.log(new Foo({})); // {} Anything other than an object gets completely ignored, so I think we should disallow returning JS primitives ( That also includes You'll still be able to trigger this by accident using The last thing to consider is whether to allow returning different Rust structs. I'm basically indifferent now, it's not any weirder than returning a You'll still need to check for it though - we can't just set
That's working because Going back to returning As for where to insert the type checks, you should be able to do it in the same place as I did in 052281f: inside |
Co-authored-by: Liam Murphy <[email protected]>
Signed-off-by: Oliver T <[email protected]>
Signed-off-by: Oliver T <[email protected]>
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.
Looks good, I just have a couple small comments.
crates/cli-support/src/wit/mod.rs
Outdated
Descriptor::I8 | ||
| Descriptor::U8 | ||
| Descriptor::ClampedU8 | ||
| Descriptor::I16 | ||
| Descriptor::U16 | ||
| Descriptor::I32 | ||
| Descriptor::U32 | ||
| Descriptor::F32 | ||
| Descriptor::F64 | ||
| Descriptor::I64 | ||
| Descriptor::U64 | ||
| Descriptor::Boolean | ||
| Descriptor::Char | ||
| Descriptor::CachedString | ||
| Descriptor::String | ||
| Descriptor::Option(_) | ||
| Descriptor::Unit => bail!(msg), |
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 should also include Enum
, since (exported) enums are represented as integers in JS.
Ref
s and RefMut
s to any of these should probably also be included, since they'll still map to primitives while behind a reference, although it doesn't matter a whole lot since you can't return references right now anyway.
(Imported / string enums are handled purely at the macro level, so we don't need to worry about them here.)
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.
I moved this check to it's own function so that it is cleaner to check nested descriptors. Like you mentioned Ref
and RefMut
are not relevant for now, but it occurred to me that Result
still is.
Signed-off-by: Oliver T <[email protected]>
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.
Thanks!
After #1594 constructors of Rust exported structs started using class wrapping when generating JS shims. Wrapping erases prototype information from the object instance in JS and as a result it is not possible to override methods (via inheritance) of the generated class.
This PR is rebased from #3561, it passes the constructor information from the
Builder
into the instruction translation function which ensures that constructors returningSelf
no longer rely on__wrap
.Fixes #3213