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

Unwrangling the mess that is godot_wrap_method, adding generic method support as a side effect #681

Merged
merged 5 commits into from Feb 5, 2021

Conversation

ghost
Copy link

@ghost ghost commented Jan 25, 2021

Due to repeated past additions to godot_wrap_method_inner, the implementation has become a total mess of unsafe macro code that is very hard to follow. Meanwhile, the manual interface for method registration has largely been neglected, lagging behind the rest of the crate, exposing sys types and unsafe FFI signatures. Recently, I've found myself in need to register a few methods manually and having to deal with this entire situation, so I've taken the chance to make some improvements to this part of the codebase, with all the hindsight that we now have. This has resulted in satisfactory results:

  • All the unsafe FFI code previously in godot_wrap_method_inner is extracted to the library proper, and can no longer be touched by obscure import/inference problems that come from delayed type checking.
  • Argument errors are now represented with a proper error type with a Display implementation. The library can report multiple argument errors at once. "Missing/excessive argument" errors are also more informative.
  • It's possible to manually register one method for many classes that it may apply to, or many specializations of a generic method to one class by different names.
  • Safe, straightforward support for stateful methods.

This is not in itself a breaking change, but it's expected that the older unsafe interface will be removed at the next possibility.


As a curiosity, the syntax for manually registering generic methods currently looks like this, and I find it already fairly usable even without further macro goodness:

#[derive(Copy, Clone, Default)]
struct Mixin<T> {
    _marker: PhantomData<T>,
}

#[derive(FromVarargs)]
struct Args<T> {
    a: T,
    b: T,
}

impl<T, C> StaticArgsMethod<C> for Mixin<T>
where
    T: Add<Output = T> + Send + Sync + ToVariant + FromVariant + 'static,
    C: NativeClass,
{
    type Args = Args<T>;
    fn call(&self, _this: RefInstance<'_, C, Shared>, Args { a, b }: Args<T>) -> Variant {
        (a + b).to_variant()
    }
}

fn register_methods(builder: &ClassBuilder<SomeClass>) {
    builder
        .build_method("add_i", StaticArgs::<Mixin::<i32>>::default())
        .done_stateless();
    builder
        .build_method("add_f", StaticArgs::<Mixin::<f32>>::default())
        .done_stateless();
    builder
        .build_method("add_v", StaticArgs::<Mixin::<Vector2>>::default())
        .done_stateless();
}

Unresolved questions

As a side effect of moving the error handling to the library side, error sites all point to the library now. This should be fixable with something on the macro side that provides borrow-able site information, like how profiling::Signature is handled? This would mean moving the FFI call out of godot_error into some interface as well. Resolved.

Future development

If desired, it should be fairly easy to add a higher-level, derivable Mixin interface based on this. It should also be possible to add support for generic methods directly in #[export].

toasteater added 3 commits January 25, 2021 19:23
This adds the `init::method::Varargs` type, moving logic for argument
checking and error reporting out of the macro, into proper library code.
This is the first step in simplifying the `godot_wrap_method` macro.
This lifts the unsafe FFI code out of macros and into proper library code
as a generic function. Stateful methods are also properly supported using
the `Method` trait. This is the second step in simplifying the
`godot_wrap_method` macro.
This adds the `FromVarargs` trait and derive macro, which supports using
generic structs as argument lists, with the same `#[opt]` syntax for
optional arguments. This makes it a lot easier to actually make use of
the `Method` interface outside `#[methods]`.

This is the final step in simplifying `godot_wrap_method_inner`, which is
now a lot easier to follow, and no longer outputs unsafe code into the user
crate!
@ghost ghost added feature Adds functionality to the library c: export Component: export (mod export, derive) labels Jan 25, 2021
@ghost
Copy link
Author

ghost commented Jan 25, 2021

bors try

bors bot added a commit that referenced this pull request Jan 25, 2021
@bors
Copy link
Contributor

bors bot commented Jan 25, 2021

try

Build succeeded:

C: NativeClass,
F: Method<C>,
{
pub(super) fn new(class_builder: &'a ClassBuilder<C>, name: &'a str, method: F) -> Self {
Copy link
Member

Choose a reason for hiding this comment

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

If class_builder and name don't have the same lifetime, will this still work?

Copy link
Author

Choose a reason for hiding this comment

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

Since the lifetime isn't carried on ClassBuilder itself, but is inferred by the compiler, the lifetime of name simply places a shorter bound on 'a, which is then inferred by the compiler to be the same as class_builder. As a result, this does compile: builder.build_method(&format!("foo{}", 123), method).done().

Copy link
Member

@halzy halzy left a comment

Choose a reason for hiding this comment

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

Nice changes! It is much more understandable than before.

attributes: ScriptMethodAttributes {
rpc_mode: self.rpc_mode,
},
method_data: 1 as *mut libc::c_void,
Copy link
Member

Choose a reason for hiding this comment

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

Could you comment on why 1 for the void*.

Copy link
Author

Choose a reason for hiding this comment

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

Stateless<F> is a ZST for any type F.

pub struct ArgBuilder<'r, 'a, T> {
args: &'r mut Varargs<'a>,
name: Option<&'a str>,
ty: Option<&'a str>,
Copy link
Member

Choose a reason for hiding this comment

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

Will name and ty have the same issue? If they have different lifetimes?

Copy link
Author

@ghost ghost Jan 26, 2021

Choose a reason for hiding this comment

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

This one does turn out to be problematic since all error objects need to have the same lifetime parameter, which will have to be 'a since 'r is temporary. I think it might be best to store a Cow<'static, str> here instead, since we don't want to allocate all the parameter names when the vast majority of them are just going to be 'static.

Refactored error macros to forward to a high-level interface. Methods and
argument errors now contain site information for error reporting. Strings
in arguments errors replaced with `Cow<'a, str>` to avoid lifetime issues.
@ghost
Copy link
Author

ghost commented Jan 26, 2021

Added explanation of the ZST pointer in comments. Made strings in ArgumentError Cow. Added a Site API to deal with the error reporting problem introduced by the original PR. Errors from wrapped methods now have correct file paths and type/method names set:

ERROR: OptionalArgs :: opt_sum: missing non-optional parameter `b` (#1)
   At: test/src/lib.rs:198.
ERROR: OptionalArgs :: opt_sum: an excessive argument is given: I64(6)
   At: test/src/lib.rs:198.

Compared to original PR (path pointing to library):

ERROR: <native>: missing non-optional parameter `b` (#1)
   At: gdnative-core/src/nativescript/init/method.rs:188.
ERROR: <native>: an excessive argument is given: I64(6)
   At: gdnative-core/src/nativescript/init/method.rs:181.

Compared to current master (type and method names not shown, worse message):

ERROR: <native>: Incorrect number of parameters: required 2 but got 1
   At: test/src/lib.rs:198.
ERROR: <native>: Incorrect number of parameters: expected at most 5 but got 6
   At: test/src/lib.rs:198.

@ghost
Copy link
Author

ghost commented Jan 26, 2021

bors try

bors bot added a commit that referenced this pull request Jan 26, 2021
@bors
Copy link
Contributor

bors bot commented Jan 26, 2021

try

Build succeeded:

@ghost ghost added the breaking-change Issues and PRs that are breaking to fix/merge. label Jan 26, 2021
@ghost
Copy link
Author

ghost commented Jan 26, 2021

Marking as breaking change since I just realized that this breaks code currently using godot_wrap_method manually to create pointers.

@ghost ghost added this to the 0.10 milestone Jan 26, 2021
@ghost ghost marked this pull request as draft January 29, 2021 10:03
@ghost
Copy link
Author

ghost commented Jan 29, 2021

Found an issue with the current code: since <$retty as $crate::core_types::OwnedToVariant>::owned_to_variant(ret) is now wrapped in an impl StaticArgsMethod<T> for ThisMethod block, any Selfs in the return type signature now refers to ThisMethod instead of what's intended. Should be easy to fix by just dropping the fully qualified syntax and using OwnedToVariant::owned_to_variant, will update later.

@ghost ghost marked this pull request as ready for review February 4, 2021 07:14
@ghost
Copy link
Author

ghost commented Feb 4, 2021

Should be ready for merging!

@halzy
Copy link
Member

halzy commented Feb 5, 2021

bors r+

@bors
Copy link
Contributor

bors bot commented Feb 5, 2021

Build succeeded:

@bors bors bot merged commit 64c4a3b into godot-rust:master Feb 5, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking-change Issues and PRs that are breaking to fix/merge. 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.

None yet

1 participant