Skip to content

Experiment with splitting up signature differently #1042

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

lilizoey
Copy link
Member

@lilizoey lilizoey commented Feb 9, 2025

VarcallSignatureTuple and PtrcallSignatureTuple are now a struct Signature<Params, Ret>. The behavior of the params are in a new set of traits: ParamList, InParamList and OutParamList.

This split allows input parameters and output parameters to be treated differently, for instance this makes it so that only FromGodot is needed for user-defined functions.

Should probably be coordinated with #1000 if we actually want to add it, as both this PR and that PR adds a generic type for parameter tuples.

Currently I've just added the new signature stuff to a separate file, rather than overwriting signature.rs. Mostly to make it easier to read here in the diff and such. This would be changed if we go with this. This needs a lot of cleanup in general lol.

But i wanna at least run the minimal CI on it here and get feedback on if this is the strategy we want to go for before i like polish it up. Since there are other strategies we could go with. The main concrete issue this really solves is splitting up parameters based on if they're in an in vs out call.

I'm not entirely sure if this is a breaking change, i think it isnt? It should just make code that used to not compile, compile. And it doesn't change any public symbols.

@lilizoey lilizoey added feature Adds functionality to the library quality-of-life No new functionality, but improves ergonomics/internals c: ffi Low-level components and interaction with GDExtension API labels Feb 9, 2025
@GodotRust
Copy link

API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-1042

@lilizoey lilizoey force-pushed the refactor/signature branch from a0f9079 to f2411b0 Compare March 16, 2025 18:26
@lilizoey lilizoey force-pushed the refactor/signature branch 2 times, most recently from ae505cf to e29f2f8 Compare March 23, 2025 22:01
@lilizoey lilizoey marked this pull request as ready for review March 23, 2025 22:12
@lilizoey
Copy link
Member Author

Im gonna do another pass at cleaning up some minor things later, but all the functionality is in place so im marking it ready for review

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 a lot! 👍

This change now leaks a new type to the public API:

fn param_info(
    index: usize,
    param_name: &str
) -> Option<MethodParamOrReturnInfo>

However MethodParamOrReturnInfo is not public.

What's the general idea of APIs, in terms of what's public?

@@ -298,7 +300,9 @@ impl<C: WithBaseField, R: ParamTuple + Sync + Send> TypedSignal<'_, C, R> {
}
}

impl<C: WithBaseField, R: ParamTuple + Sync + Send> IntoFuture for &TypedSignal<'_, C, R> {
impl<C: WithBaseField, R: InParamTuple + Sync + Send + 'static> IntoFuture
Copy link
Member

Choose a reason for hiding this comment

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

Any reason why this adds 'static bound?
Also in some other places...

InParamTuple can maybe not be 'static itself, if RefArg or so is used?

Copy link
Member Author

Choose a reason for hiding this comment

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

It adds a 'static bound because ParamTuple had a 'static bound before and TypedSignal relied on that.

And yeah, i don't remember exactly where but there are cases where ParamTuple can't be 'static.

unsafe_impl_param_tuple!(13; (p0, 0): P0, (p1, 1): P1, (p2, 2): P2, (p3, 3): P3, (p4, 4): P4, (p5, 5): P5, (p6, 6): P6, (p7, 7): P7, (p8, 8): P8, (p9, 9): P9, (p10, 10): P10, (p11, 11): P11, (p12, 12): P12);
unsafe_impl_param_tuple!(14; (p0, 0): P0, (p1, 1): P1, (p2, 2): P2, (p3, 3): P3, (p4, 4): P4, (p5, 5): P5, (p6, 6): P6, (p7, 7): P7, (p8, 8): P8, (p9, 9): P9, (p10, 10): P10, (p11, 11): P11, (p12, 12): P12, (p13, 13): P13);

// Manually implement for () so we dont have to add a bunch of #[allow(..)] above for the 0-length case.
Copy link
Member

Choose a reason for hiding this comment

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

I'd rather have such #[allow]s than manually duplicating code that can be generated by the macro.

They can also cover an entire impls module or so.

Copy link
Member Author

Choose a reason for hiding this comment

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

i mainly dont like it because it can be useful to have those lints for the code, though i think it may be possible to generate them only when necessary?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, the question is how much indirection this adds if it's handled inside the macro. Having complex de-linting logic may be overkill.

Otherwise, would this potentially work?

#[allow(bla)]
mod impl0 {
    // macro invocation for 0
}

mod impls {
   // others
}

Comment on lines 12 to 16
use crate::meta::{signature, CallContext};
use crate::meta::{
FromGodot, GodotConvert, GodotFfiVariant, GodotType, InParamTuple, OutParamTuple, ParamTuple,
ToGodot,
};
Copy link
Member

Choose a reason for hiding this comment

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

These 2 can be combined 🙂

Comment on lines 40 to 50
format!(
// This repeat expression is basically just `"{$p:?}"`, the rest is only needed so that
// the repetition separator can be `", "` instead of `,`.
concat!("" $(, "{", stringify!($p), ":?}" ,)", "*),
$($p=$p),*
)
Copy link
Member

Choose a reason for hiding this comment

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

Would it not be easier to use positional format args?

Then you don't need stringify!($p) and the expression is already much simpler.

Copy link
Member Author

Choose a reason for hiding this comment

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

oh yeah true, i didnt think of that actually

Comment on lines 67 to 74
/// As an example, this would be used to call Godot functions through ffi, however this is _not_ used when Godot calls a user-defined
/// function.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// As an example, this would be used to call Godot functions through ffi, however this is _not_ used when Godot calls a user-defined
/// function.
/// As an example, this would be used to call Godot functions through FFI, however this is _not_ used when Godot calls a user-defined
/// function.

use crate::obj::GodotClass;

/// Info relating to an argument or return type in a method.
pub struct MethodParamOrReturnInfo {
info: PropertyInfo,
pub info: PropertyInfo,
Copy link
Member

Choose a reason for hiding this comment

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

Making this pub is intended?

See also main comment about MethodParamOrReturnInfo, which is currently not public. If it becomes public, we should probably think about the encapsulation.

Comment on lines 93 to 92
/// Fully customizable connection setup.
///
/// The returned builder provides several methods to configure how to connect the signal. It needs to be finalized with a call to
/// [`ConnectBuilder::done()`].
pub fn connect_builder(&mut self) -> ConnectBuilder<'_, 'c, C, (), Ps, ()> {
ConnectBuilder::new(self)
}

/// Directly connect a Rust callable `godot_fn`, with a name based on `F`.
///
/// This exists as a short-hand for the connect methods on [`TypedSignal`] and avoids the generic instantiation of the full-blown
/// type state builder for simple + common connections, thus hopefully being a tiny bit lighter on compile times.
fn inner_connect_godot_fn<F>(
&mut self,
godot_fn: impl FnMut(&[&Variant]) -> Result<Variant, ()> + 'static,
) {
let callable_name = make_callable_name::<F>();
let callable = Callable::from_local_fn(&callable_name, godot_fn);

let signal_name = self.name.as_ref();
self.owner.with_object_mut(|obj| {
obj.connect(signal_name, &callable);
});
}

/// Connect an untyped callable, with optional flags.
///
/// Used by [`ConnectBuilder::done()`]. Any other type-state (such as thread-local/sync, callable debug name, etc.) are baked into
/// `callable` and thus type-erased into runtime logic.
pub(super) fn inner_connect_untyped(
&mut self,
callable: &Callable,
flags: Option<ConnectFlags>,
) {
use crate::obj::EngineBitfield;

let signal_name = self.name.as_ref();

self.owner.with_object_mut(|obj| {
let mut c = obj.connect_ex(signal_name, callable);
if let Some(flags) = flags {
c = c.flags(flags.ord() as u32);
}
c.done();
});
}

pub(crate) fn to_untyped(&self) -> crate::builtin::Signal {
crate::builtin::Signal::from_object_signal(&self.receiver_object(), &*self.name)
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Moving those up changes the order of declaration, which no longer documents the symbols in order of importance.

See

Could you restore the order?

Comment on lines 28 to 29
use crate::meta::{AsArg, AsObjectArg, ClassName, CowArg, ObjectArg, ObjectCow, RefArg};
use crate::meta::{ParamTuple, InParamTuple, OutParamTuple, Signature};
Copy link
Member

Choose a reason for hiding this comment

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

Also here can be combined 🙂


use godot_ffi as sys;
use sys::{BuiltinMethodBind, ClassMethodBind, GodotFfi, UtilityFunctionBind};
use std::{borrow::Cow, fmt, marker::PhantomData};
Copy link
Member

Choose a reason for hiding this comment

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

Please flatten

Comment on lines 355 to 360
/// Performs a ptrcall and processes the return value to give nice error output.
///
/// # Safety
///
/// This calls [`GodotFfi::new_with_init`] and passes the ptr as the second argument to `f`, see that function for safety docs.
unsafe fn perform_ptrcall(
Copy link
Member

Choose a reason for hiding this comment

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

The brief description of the method is good, but "perform" doesn't carry any of that information. Is it maybe possible to make the name a bit more descriptive, even if longer?

Copy link
Member Author

Choose a reason for hiding this comment

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

i can't really think of anything that's of similar length, but maybe just perform_ptrcall_and_process_return works?

Copy link
Member

Choose a reason for hiding this comment

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

"process" isn't much better... I'd like to avoid generic filler words; if it's not easy to add more context, let's keep it short.

Maybe something highlighting that this operates on the raw FFI args/ret: raw_ptrcall(...)
That would at least differentiate it from the other functions like out_builtin_ptrcall and so on.

@lilizoey
Copy link
Member Author

lilizoey commented Mar 25, 2025

Thanks a lot! 👍

This change now leaks a new type to the public API:

fn param_info(
    index: usize,
    param_name: &str
) -> Option<MethodParamOrReturnInfo>

However MethodParamOrReturnInfo is not public.

What's the general idea of APIs, in terms of what's public?

Hm yeah, ParamTuple should probably be public so people can see what determines a legal signature. However i am not sure if that particular method should be. The way it was before with one method for the property info and one for the property info + metadata is a bit redundant? maybe i can just make this #[doc(hidden)] and make a public method that just returns PropertyInfo? so like what it was before but they share implementation by property_info calling param_info. That would make it impossible for the user to implement their own tuples, but also we dont really want people to be doing that anyway.

macro_rules! unsafe_impl_param_tuple {
($Len:literal; $(($p:ident, $n:tt): $P:ident),+) => {
impl<$($P),+> ParamTuple for ($($P,)+) where $($P: GodotConvert + fmt::Debug),+ {
const LEN: usize = $Len;
Copy link
Contributor

Choose a reason for hiding this comment

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

you can count the number of $Ps with something like this and don't have to manually maintain the count:

    (count_items; $PP:ident, $($P:ident),+) => {
        1 + unsafe_impl_param_tuple!(count_items; $($P),+)
    };

    (count_items; $P:ident) => {
        1
    };

and:

const LEN: usize = unsafe_impl_param_tuple!(count_items; $($P),+);

@lilizoey lilizoey force-pushed the refactor/signature branch 3 times, most recently from dcd467b to 4d5e63a Compare March 30, 2025 18:48
@Bromeon
Copy link
Member

Bromeon commented Mar 30, 2025

Since I caused merge conflicts with #1111, I added a commit to resolve them. I hope I added the new traits in the right places. Feel free to do whatever with it (including revert/change/...), don't need to keep authorship either 🙂

@lilizoey lilizoey force-pushed the refactor/signature branch from f5dc50c to 7578d86 Compare April 4, 2025 16:20
remove signature from compiling

merge `ParamList` with `ParamTuple`

some cleanup

docs

changes from code review
@Bromeon Bromeon force-pushed the refactor/signature branch from 7578d86 to 3aac094 Compare April 17, 2025 11:54
@Bromeon Bromeon added this to the 0.3 milestone Apr 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c: ffi Low-level components and interaction with GDExtension API feature Adds functionality to the library quality-of-life No new functionality, but improves ergonomics/internals
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants