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

Construct external Rust types with named fields #589

Merged
merged 13 commits into from
Jul 30, 2023

Conversation

jbdutton
Copy link
Contributor

@jbdutton jbdutton commented Jul 27, 2023

This pull request allows Rune scripts to construct Rust types with named fields, like so:

#[derive(Default, Debug, Any, PartialEq, Eq)]
#[rune(constructor)]
struct External {
    #[rune(get, set)]
    suite_name: String,
    #[rune(get, set)]
    room_number: usize,
}
pub fn main() {
  let external = External {
      suite_name: "Fowler",
      room_number: 1300,
  };

  external
}

I've tested this with structs and it works as expected. It should also work with enum variants that have named fields, though I haven't tested that yet.

It does this by creating a closure during the Any macro expansion, and installing that as a constructor function associated with the type:

|suite_name: String, room_number: usize| {
    External { suite_name, room_number }
}

By invoking the constructor function, the VM is placing the External type onto the stack as a Value::Any. This means you can successfully extract the typed object using the FromValue trait:

let output = vm.call(["main"], ())?;
let output: External = rune::from_value(output)?;
println!("{:?}", output);

Right now, this relies on the caller to assign the struct fields in the correct order. Otherwise the call to the closure fails due to mismatched types. There are probably a few ways to fix that, including making the closure take a single Object argument and extracting the fields from it.

This probably also doesn't work with nested structs or generics, at least for the moment.

Fixes #400.

@jbdutton jbdutton changed the title Construct external structs Construct external Rust types with named fields Jul 27, 2023
Copy link
Collaborator

@udoprog udoprog left a comment

Choose a reason for hiding this comment

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

Very nice, thanks for starting the work towards external struct construction!

Everything looks pretty good to me, apart that we need to deal with re-ordering. Since we want to maintain the evaluation order we unfortunately can't just re-order field value construction (which would've been nice). So we'll have to cope by swapping or copying values around for now.

I have some thoughts about changing the underlying instructions so that each has a target address they're being assembled into. It would simplify construction but is further down the line.

@udoprog udoprog added the enhancement New feature or request label Jul 29, 2023
@jbdutton jbdutton force-pushed the construct-external-structs branch from 3698d71 to 4fde80e Compare July 30, 2023 14:07
Copy link
Collaborator

@udoprog udoprog left a comment

Choose a reason for hiding this comment

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

Change looks nice, there's possibly a stack offset issue during assembly I'd like you to test for and possibly fix.

cx.asm.push(
Inst::Swap {
a: stack_position,
b: desired_position,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think these will have to be offset from the base of stack construction since the stack might contain other data before this point? That is, based of cx.scopes.total(&span) before the assignments above are constructed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I was going to ask you about this. Currently this is using stack_bottom.checked_add to ensure the position is inside the call frame. But a lot of your stack operations are relative to the top, so I wasn't sure if you preferred Inst::Swap to also be relative to the top for consistency.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Relative to the stack frame seems fine and easy to reason about. Unless you can come up with some other reason.

crates/rune/src/compile/v1/assemble.rs Outdated Show resolved Hide resolved

let mut sources = rune::sources! {
entry => {
pub fn main() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Add some padding in this test, like a few variable declarations before you construct the struct and it should produce the error which requires adding a stack offset (see above).

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 was planning to add a true test (or tests) to exercise the new functionality, and leave the example focused on demonstrating the basics. Will push something up momentarily.


let output = vm.call(["main"], ())?;
let output: External = rune::from_value(output)?;
println!("{:?}", output);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Add an assert_eq! here which checks that the constructed object has the expected values.

Comment on lines 328 to 330
let a = self.stack.len().checked_sub(a).ok_or(StackError)?;
let b = self.stack.len().checked_sub(b).ok_or(StackError)?;
self.stack.swap(a, b);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is using offsets from the top of the stack now. Should it check that the computed offsets don't exceed stack_bottom?

Copy link
Collaborator

@udoprog udoprog Jul 30, 2023

Choose a reason for hiding this comment

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

Yeah, that's necessary to ensure stack isolation between function calls, which could otherwise pose a security risk in case there are any miscompilations. Note that you get that for free if you checked_add from stack_bottom instead (relative to the stack frame), all though you still need to check that neither exceeds the size of the stack to avoid the panic when out of bounds during slice::swap.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah ok, I misunderstood your previous comment. Unfortunately I'm out of pocket the next few days. If you have time, feel free to push a fix and merge everything. Thanks for all the feedback!

Copy link
Collaborator

@udoprog udoprog Jul 30, 2023

Choose a reason for hiding this comment

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

Ok, thanks for your work and giving me the heads up!

@jbdutton jbdutton marked this pull request as ready for review July 30, 2023 17:43
@@ -1976,6 +1976,10 @@ fn expr_object<'hir>(
hir::ExprObjectKind::StructVariant { hash } => {
cx.asm.push(Inst::StructVariant { hash, slot }, span);
}
hir::ExprObjectKind::Constructor { hash, args } => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This isn't a great name, IMO. Any suggestions? Maybe hir::ExprObjectKind::ExternalType, or something along those lines?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Including "External" seems good, so 👍. Not important to be perfect since it's an internal variant and can easily be renamed.

@udoprog udoprog merged commit 35dded3 into rune-rs:main Jul 30, 2023
@jbdutton jbdutton deleted the construct-external-structs branch August 8, 2023 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Can't construct struct with named field
2 participants