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

Add TryFrom to convert from Varargs to tuples #886

Merged
merged 2 commits into from
Jul 13, 2022

Conversation

B-head
Copy link
Contributor

@B-head B-head commented May 4, 2022

This PR is stacked on varargs_gets branch. See #892 for details on some of the commits included in this PR.

Feature

Dirty syntax to convert to tuples. This makes manual binding easier to write than ever before.

Implement via varargs_into_tuple!() macro. Can be converted to tuples up to 12 in length. This is the same length supported by standard library.

fn call(&self, _this: TInstance<'_, Foo>, args: Varargs<'_>) -> Variant {

    // Convert from Varargs to tuples.
    let (a, b, c): (i64, i64, i64) = args.try_into().unwrap();

    let ret = (a * b - c);
    ret.to_variant()
}

Compatibility

Since this PR only adds an API, there are no compatibility issues.

B-head added a commit to B-head/godot-rust that referenced this pull request May 4, 2022
@B-head B-head marked this pull request as draft May 5, 2022 07:09
Copy link
Member

@Bromeon Bromeon left a comment

Choose a reason for hiding this comment

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

Thanks for this! 🙂

Maybe a general question, what was your use case for this feature?

It seems like type safety is the main reason, i.e. not dealing with Variant but direct Rust types. On the other hand, this is quite a low-level API (implementing Method trait), and for type safety we also have #[derive(FromVarargs)] struct Args which is only slightly more verbose. I'm a bit hesitant to heavily invest in convenience of the low-level builder APIs, as most people use the proc-macros, and a lot of this might become entirely obsolete with GDExtension 🤔

This doesn't support match patterns (for different number of arguments), right?
I guess for that we could still use slice patterns, even though that means Variant elements:

match varargs.as_slice() {
    [a, b]            => ...,
    [first, .., last] => ...,
    _                 => ...,
}

gdnative-core/src/export/method.rs Show resolved Hide resolved
Comment on lines 363 to 483
/// All possible error types for convert from Varargs.
#[derive(Debug)]
pub enum VarargsError {
ArgumentLengthError(ArgumentLengthError),
FromVariantError(FromVariantError),
}
Copy link
Member

Choose a reason for hiding this comment

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

Do we really need nested types here?

We have struct-like enumerators for FromVariantError and it seems to work well.

We also don't really need all the From<T> implementations -- as I see it, they provide minor convenience for this one file, at the cost of much more code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's what I missed! 🤣

However, FromVariantError does not appear to be specialized to arguments error messages. In Rust, Change of type enum is breaking change, so it cannot be changed immediately either. So it looks like we will create an ArgumentTypeError that is specialized for argument errors and VarargsError will remain nested.

Copy link
Member

Choose a reason for hiding this comment

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

It could also be done like this, no?

#[derive(Debug)]
pub enum VarargsError {
    ArgumentLengthError  {
        passed: usize,
        expected: usize,
    },
    FromVariantError(FromVariantError),
}

Or if you find ArgumentTypeError more meaningful than FromVariantError, you could also do a struct-like enumerator for that.

Unless of course we find a good reason to reuse those types, but they seem used isolated.

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 did not do that, because if we write the structure directly to enum, we would not be able to write methods for each errors.

test/src/test_register.rs Outdated Show resolved Hide resolved
@Bromeon Bromeon added feature Adds functionality to the library c: core Component: core (mod core_types, object, log, init, ...) labels May 8, 2022
@Bromeon
Copy link
Member

Bromeon commented May 8, 2022

Btw, the Varargs API as it exists now in godot-rust is a bit strange. It is simultaneously a collection and iterator.

I think the proper way would have been implementing IntoIterator, which could reuse the slice iterator. This would also get rid of storing idx as the iterator state inside Varargs.

That's probably something we can adjust for v0.11.

@B-head
Copy link
Contributor Author

B-head commented May 9, 2022

Maybe a general question, what was your use case for this feature?

My use case is refactoring of the #[methods] macro. Since macros will generate manual binding code, improve manual binding will improve macros.

However, this is a failure because it lacks a bit of flexibility. 😅 Nevertheless, the PR was created because it is useful.

a lot of this might become entirely obsolete with GDExtension 🤔

Don't worry, the types passed as arguments are the same. See header (GDNativeVariantPtr is void*).

This doesn't support match patterns (for different number of arguments), right? I guess for that we could still use slice patterns, even though that means Variant elements:

match varargs.as_slice() {
    [a, b]            => ...,
    [first, .., last] => ...,
    _                 => ...,
}

Interesting, but a, b, first and last are still &Variant types, so more type conversion code is needed.

@Bromeon
Copy link
Member

Bromeon commented May 9, 2022

My use case is refactoring of the #[methods] macro. Since macros will generate manual binding code, improve manual binding will improve macros.

I see, but that on its own is not a reason to make that a public and documented API. If it's only used by the proc-macro, it can still be #[doc(hidden)], no?

In other words: why does a user need this, when there is already the safer ClassBuilder::method() API?


Don't worry, the types passed as arguments are the same. See header (GDNativeVariantPtr is void*).

Thanks for checking! However, both being void* doesn't mean they refer to the same types -- it literally means "pointer to anything" 🙂

So, we would need to check that. What I meant is that with the current bottom-up approach toward GDExtension, I'm not sure yet which parts of the code can be kept (and at what cost). A lot of stuff has accumulated over the years, and it's an opportunity to strip down rarely used legacy code, also in light of bigger safety refactorings. So... time will tell, don't be discouraged if not everything will be taken over to GDExtension 😉

@B-head
Copy link
Contributor Author

B-head commented May 9, 2022

In other words: why does a user need this, when there is already the safer ClassBuilder::method() API?

This PR is replaces the StaticArgs struct, but the Method trait and ClassBuilder::method() can still be used. No unsafe code is needed.

@Bromeon
Copy link
Member

Bromeon commented May 9, 2022

Sorry, I totally wrote to the wrong PR. I meant to comment on your other changes #887 🙈 hope that makes more sense.

Comment on lines 293 to 299
if self.idx < self.args.len() {
let idx = self.idx;
self.idx += 1;
Some(self.args[idx])
} else {
None
}
Copy link
Contributor

@Bogay Bogay May 10, 2022

Choose a reason for hiding this comment

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

This part may be simplified by using slice.get?

Suggested change
if self.idx < self.args.len() {
let idx = self.idx;
self.idx += 1;
Some(self.args[idx])
} else {
None
}
let result = self.args.get(self.idx);
self.idx += 1;
result

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The test did not pass, so I made some fixes.

Suggested change
if self.idx < self.args.len() {
let idx = self.idx;
self.idx += 1;
Some(self.args[idx])
} else {
None
}
let ret = self.args.get(self.idx);
ret.map(|&v| {
self.idx += 1;
v
})

Copy link
Member

Choose a reason for hiding this comment

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

This is slightly confusing, as map() typically transforms the contents of the Option and has no side effects.

I'd rather do:

let ret = self.args.get(self.idx);
if ret.is_some() {
    self.idx += 1;
}
ret

In the future there might be Option::inspect() for this purpose, but it's not yet stable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Reflected in next push.

B-head added a commit to B-head/godot-rust that referenced this pull request May 11, 2022
@B-head B-head marked this pull request as ready for review May 11, 2022 22:20
@B-head
Copy link
Contributor Author

B-head commented May 11, 2022

Note that this PR now depends on #892!

@Bromeon Bromeon added c: export Component: export (mod export, derive) and removed c: core Component: core (mod core_types, object, log, init, ...) labels May 13, 2022
@Bromeon
Copy link
Member

Bromeon commented May 13, 2022

Then I'd say we sort out #892 first, which would make this PR significantly smaller to review.

B-head and others added 2 commits July 13, 2022 19:34
Change Varargs struct to store a slice instead of an iterator
Since this is a rarely used feature, a rich encapsulated API for error handling is likely not needed.
Most users do not explicitly differentiate error variants, having a descriptive Display impl is usually more important.

Supports only 4 expressions for length checks: a, a.., ..=b, a..=b.
These are almost always literals and exclusive bounds are likely error prone in this context.
Additionally, check_length() with the full range '..' makes no sense, so it's not supported.
@Bromeon
Copy link
Member

Bromeon commented Jul 13, 2022

Rebased on master, completed the PR and integrated my change suggestions.
Thanks again for your work! 👍

bors r+

@bors
Copy link
Contributor

bors bot commented Jul 13, 2022

Build succeeded:

@bors bors bot merged commit ebf4427 into godot-rust:master Jul 13, 2022
@chitoyuu chitoyuu mentioned this pull request Dec 6, 2022
@B-head B-head deleted the varargs_to_tuple branch February 3, 2023 16:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c: export Component: export (mod export, derive) feature Adds functionality to the library
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants