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

Remove class wrap for constructors in Rust exports #3561

Closed
wants to merge 4 commits into from

Conversation

snOm3ad
Copy link
Contributor

@snOm3ad snOm3ad commented Aug 17, 2023

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.

Additionally, some checks to ensure constructors always return an instance of Self were lost.

This PR fixes the above two issues by embedding the kind of export into the CallExport instruction and monitoring for constructor exports calls during the export adapter creation.

Once a constructor export call is detected and validated a new instruction SelfFromI32 is pushed into the stack that avoids class wrapping.

Fixes #3213

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 fixes the above two issues by embedding the kind of export into
the `CallExport` instruction and monitoring for constructor exports
calls during the export adapter creation.

Once a constructor export call is detected and validated a new
instruction `SelfFromI32` is pushed into the stack that avoids class
wrapping.

Fixes rustwasm#3213

Signed-off-by: Oliver T <geronimooliver00@gmail.com>
Signed-off-by: Oliver T <geronimooliver00@gmail.com>
ret.outgoing(&signature.ret)?;
if let Instruction::CallExport(_, AuxExportKind::Constructor(ref class)) = call {
if let Descriptor::RustStruct(ref name) = signature.ret {
if class != name {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

How useful is this check really? I mean all you have to do to bypass it is to wrap your foreign return type inside an Option or any other parameterized type and you've gone back to the old behaviour.

Copy link
Collaborator

Choose a reason for hiding this comment

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

It should reject Option<T> too - only the Rust struct that the constructor's for should be allowed.

Copy link
Collaborator

@Liamolucko Liamolucko left a comment

Choose a reason for hiding this comment

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

I had a bit of a different idea for how to implement this, which was to replace the generated return {} with this.__wbg_ptr = {} for constructors. return statements get inserted here, and you can already check whether the function's a constructor using self.constructor, so it doesn't require piping extra state around. I should probably have mentioned this when you asked for pointers; sorry about that.

Anyway, this implementation is also fine so you can stick with it if you want, but getting rid of the return entirely is a bit cleaner IMO.


## Caveats

Starting from v0.2.48 there is a bug in the `wasm-bindgen-cli-support` crate which breaks inheritance of exported Rust structs from JavaScript side (see [#3213](https://github.com/rustwasm/wasm-bindgen/issues/3213)). If you want to inherit a Rust struct such as:
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think anyone cares about which internal crate caused the problem; you should probably just say wasm-bindgen or 'the wasm-bindgen CLI'.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wasn't sure if it was worth even including this. But yeah good point, I'll make these changes.

@Liamolucko
Copy link
Collaborator

I had a bit of a different idea for how to implement this, which was to replace the generated return {} with this.__wbg_ptr = {} for constructors. return statements get inserted here, and you can already check whether the function's a constructor using self.constructor, so it doesn't require piping extra state around.

Oops, that wouldn't actually work, that'd try to assign an object to this.__wbg_ptr. You could work around that by manually getting rid of the final RustFromI32 instruction if a constructor's being generated. (You could also check that it has the right return value at the same time: if the last instruction isn't a RustFromI32, it doesn't return a Rust struct, and then you can check that the contained class is correct.)

@snOm3ad
Copy link
Contributor Author

snOm3ad commented Aug 18, 2023

I had a bit of a different idea for how to implement this, which was to replace the generated return {} with this.__wbg_ptr = {} for constructors. return statements get inserted here, and you can already check whether the function's a constructor using self.constructor, so it doesn't require piping extra state around. I should probably have mentioned this when you asked for pointers; sorry about that.

Anyway, this implementation is also fine so you can stick with it if you want, but getting rid of the return entirely is a bit cleaner IMO.

Hmm, I had not thought about this!

You could work around that by manually getting rid of the final RustFromI32 instruction if a constructor's being generated.

I was thinking of instead passing the constructor field as a parameter to the instruction function. Since it's not enough to get rid of RustFromI32 I'd also need to do the same for OptionRustFromI32. Something like this:

fn instruction(js: &mut JsBuilder, instr: &Instruction, log_error: &mut bool, constructor: &Option<String>) -> Result<(), Error> {
    match instr {
        Instruction::RustFromI32 { class } => {
            if let Some(name) = constructor {
                if name != class {
                    bail!("constructor for `{}` cannot return `{}`", class, name);
                }
                let val = js.pop();
                js.push(format!("{} >>> 0;", val));
            } else {
                js.cx.require_class_wrap(class);
                let val = js.pop();
                js.push(format!("{}.__wrap({})", class, val));
            }
        }
    }
}

What do you think? I'd also have to modify the return like you mentioned above. And, thank you for looking into this! It is indeed much cleaner. I'll probably make another PR based off from this one.

@snOm3ad snOm3ad closed this Aug 18, 2023
@snOm3ad snOm3ad deleted the fix-export-constructors branch August 18, 2023 20:38
snOm3ad added a commit to snOm3ad/wasm-bindgen that referenced this pull request Aug 18, 2023
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 <geronimooliver00@gmail.com>
Liamolucko added a commit that referenced this pull request Aug 25, 2023
* Remove class wrap for constructors in Rust exports

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.

Additionally, some checks to ensure constructors always return an
instance of `Self` were lost.

This PR is rebased from #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 <geronimooliver00@gmail.com>

* Fix typo

Co-authored-by: Liam Murphy <liampm32@gmail.com>

* Disallow returning JS primitives from constructors

Signed-off-by: Oliver T <geronimooliver00@gmail.com>

* Added missing String in match

Signed-off-by: Oliver T <geronimooliver00@gmail.com>

* Handle nested descriptors

Signed-off-by: Oliver T <geronimooliver00@gmail.com>

---------

Signed-off-by: Oliver T <geronimooliver00@gmail.com>
Co-authored-by: Liam Murphy <liampm32@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Cannot inherit from structs created via #[wasm_bindgen] attribute as expected
2 participants