Skip to content

Introduce convenient, type-safe call_deferred alternative #1204

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 3 commits into
base: master
Choose a base branch
from

Conversation

goatfryed
Copy link

@goatfryed goatfryed commented Jun 15, 2025

apply_deferred

Implements #1199

I'm note entirely sold on my api design here, but I guess test and partial implementation are a good next step to explore the desired outcome. See comments in code

adds test for call_deferred

This seems like a cyclic test expectation, because async_test uses call_deferred internally and now call_deferred test uses async_test internally, but it was the first best idea i came up with, that wouldn't require changes to the test engine and allows me to await the idle time of the current frame.

todo

  • Finalize signature
  • Expose the same signature on GodotClass. I'm getting familiar with the code base
  • Improve docs. Thankful for any pointers what I should update, once this is finalized

Depending on the discussion of the two loosely coupled topics here, feel free to cherry pick the test improvements or let just ask me to create a dedicated PR for that.

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 for your contribution!

Some first comments:

  • You can mark the PR as draft without needing to change the title. This also prevents accidental merging.
  • You can do a first attempt of running CI locally with check.sh. See book.
  • I don't see the point of try_apply_deferred 🙂 any errors can't be returned to the user, so you might as well just have apply_deferred returning a generic return type R.
  • Regarding formatting of RustDoc comments, please check how other code does it (line length, header + body separation, etc.)

@Bromeon Bromeon added feature Adds functionality to the library c: core Core components c: engine Godot classes (nodes, resources, ...) and removed c: core Core components labels Jun 15, 2025
@Bromeon Bromeon linked an issue Jun 15, 2025 that may be closed by this pull request
@goatfryed goatfryed marked this pull request as draft June 15, 2025 18:30
@goatfryed goatfryed changed the title DRAFT: Introduce convenient, type-safe call_deferred alternative Introduce convenient, type-safe call_deferred alternative Jun 15, 2025
@GodotRust
Copy link

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

@goatfryed
Copy link
Author

I don't see the point of try_apply_deferred 🙂 any errors can't be returned to the user, so you might as well just have apply_deferred returning a generic return type R.

No reason, just unfamiliarty, so I mirrored Callable::from_local_fn closely at first. Great, than let's drop that and keep just the simple version.

You can do a first attempt of running CI locally with check.sh. See book.

I understand that godot-itest 4.1 compat could break, but I'm surprised that local ./check.sh doesn't catch doc-lint issues. I assume I just need to use the linked type symbol to fix this?

image

@goatfryed goatfryed force-pushed the feat/1199-type-safe-call-deferred branch from 67c8ea7 to 944805d Compare June 15, 2025 18:57
@goatfryed
Copy link
Author

I need help with the doc-lint issue :) I don't understand why "it works on my machine" (🫡)...
I see that L289 references Object as well and there it works.

@goatfryed goatfryed force-pushed the feat/1199-type-safe-call-deferred branch from 944805d to b20806a Compare June 15, 2025 19:05
@Bromeon
Copy link
Member

Bromeon commented Jun 15, 2025

but I'm surprised that local ./check.sh doesn't catch doc-lint issues.
[...]
I need help with the doc-lint issue :) I don't understand why "it works on my machine" (🫡)...

That book page states:

The script check.sh in the project root can be used to mimic a minimal version of CI locally.

[...]

If you want to have the full CI experience, you can experiment as much as you like on your own gdext fork, before submitting a pull request.

In other words, check.sh is just a first sanity check. The authority on correctness is the GitHub Actions pipeline. And you can enable GitHub Actions on your repo fork to see the CI status, it's all described on that page 😉

But in your case it's not just notify-docs. The compatibility integration test for Godot 4.1 also fails, because signals() isn't available there. You need to #[cfg(since_api = "4.2")] your test/module out if you don't want it to run under 4.1, see other tests.

@goatfryed goatfryed force-pushed the feat/1199-type-safe-call-deferred branch 2 times, most recently from 3be4a2b to 613b2df Compare June 15, 2025 19:38
@goatfryed
Copy link
Author

Fixed the PR. I guess, to enable the pipeline on my fork is a bit late since I already created the PR. Sorry for triggering your builds so frequently.

I'm still curious why Object::get_class() is fine for L289, but well, I guess i can just qualify my link. Should i qualify L289 as well while I'm at it?

I was a bit surprised that doc lint uses stricter pipeline rules tbh. I read the section that you linked, but I unconsiously assumed, that linting wouldn't be affected. My bad.

@Bromeon
Copy link
Member

Bromeon commented Jun 15, 2025

I'm still curious why Object::get_class() is fine for L289, but well, I guess i can just qualify my link. Should i qualify L289 as well while I'm at it?

I just checked, it's also wrong. The lint probably misses it because Gd::dynamic_class_string() is a private method.

Thanks for bringing it up -- I can fix it, currently merging with some other doc issues that came up 🙂

@goatfryed goatfryed force-pushed the feat/1199-type-safe-call-deferred branch 2 times, most recently from c043bed to f67b834 Compare June 15, 2025 21:08
@Houtamelo
Copy link
Contributor

Thanks a lot for your contribution!

Some first comments:

* You can mark the PR as draft without needing to change the title. This also prevents accidental merging.

* You can do a first attempt of running CI locally with `check.sh`. See [book](https://godot-rust.github.io/book/contribute/dev-tools.html).

* I don't see the point of `try_apply_deferred` 🙂 any errors can't be returned to the user, so you might as well just have `apply_deferred` returning a generic return type `R`.

* Regarding formatting of RustDoc comments, please check how other code does it (line length, header + body separation, etc.)

What is the point of returning a generic type R when the return value of the function is always discarded? Why not just return () (nothing) ?

@Bromeon
Copy link
Member

Bromeon commented Jun 16, 2025

I meant the closure, not apply_deferred itself. It would allow binding functions that have a return value (which is discarded).

@Houtamelo
Copy link
Contributor

I meant the closure, not apply_deferred itself. It would allow binding functions that have a return value (which is discarded).

Why we would want to bind such function? Wouldn't it cause confusion instead of providing any benefit?

@Bromeon
Copy link
Member

Bromeon commented Jun 16, 2025

I was thinking that some methods perform an action and only return an informational return value.

But maybe you're right, let's start with () only -- after all, it's a non-breaking change to extend this in the future 👍

/// Runs the given Closure deferred.
///
/// This can be a type-safe alternative to [`classes::Object::call_deferred`], but does not handle dynamic dispatch, unless explicitly used.
/// This constructor only allows the callable to be invoked from the same thread as creating it.The closure receives a reference to this object back.
Copy link
Contributor

@Yarwin Yarwin Jun 17, 2025

Choose a reason for hiding this comment

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

This constructor only allows the callable to be invoked from the same thread as creating it.

Note: might not be important in current scope, but call_deferred always calls from the main thread and is recommended way to "sync" thread with a main thread in Godot (Godot itself even uses it few times, look for something along the lines of _thread_function).

https://docs.godotengine.org/en/stable/tutorials/performance/thread_safe_apis.html#scene-tree

If you want to call functions from a thread, the call_deferred function may be used (…)

In other words following code:

    godot_print!("Starting from the main thread… {:?}", std::thread::current().id());
    
    std::thread::spawn(move || {
        godot_print!("Hello from the secondary thread! {:?}", std::thread::current().id());
        let c = Callable::from_sync_fn("Thread thread", |_| {
            godot_print!("hello from the main thread!, {:?}", std::thread::current().id());
            Ok(Variant::nil())
        }); 
        c.call_deferred(&[]);
    }).join().unwrap();

Would print:

Starting from the main thread… ThreadId(1)
Hello from the secondary thread! ThreadId(3)
hello from the main thread!, ThreadId(1)

Now, since Gd<T> is NOT thread safe it is debatable if we want to support such operations 🤷. It would open a whole can of worms for sure. (IMO current implementation with from_local_fn is fine! Gd is not thread safe, thus all the hack/syncs can be done with from_sync_fn/CustomCallable and should be responsibility of the user… and we have proper, better tools for syncing threads like for example the channels. Just wanted to note it).

The closure receives a reference to this object back.

I'm not sure what does it mean 🤔? Wouldn't it better to ensure that closure/callable keeps reference to a given object (i.e. if it is refcounted it won't be pruned before idle time/before said apply deferred is applied)?

Copy link
Member

Choose a reason for hiding this comment

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

We should maybe enforce this for now, using this function:

gdext/godot-ffi/src/lib.rs

Lines 434 to 441 in 20cd346

/// Check if the current thread is the main thread.
///
/// # Panics
/// - If it is called before the engine bindings have been initialized.
#[cfg(not(wasm_nothreads))]
pub fn is_main_thread() -> bool {
std::thread::current().id() == main_thread_id()
}

Btw, is_main_thread should have an implementation for #[cfg(wasm_nothreads)] returning true, otherwise clients constantly have to differentiate cases.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep, enforcing it is good idea, since

    let mut test_node = DeferredTestNode::new_alloc();
    ctx.scene_tree.clone().add_child(&test_node);
    let hack = test_node.instance_id();
    
    std::thread::spawn(move || {
        let mut obj: Gd<DeferredTestNode> = Gd::from_instance_id(hack);
        obj.apply_deferred(|mut this| this.bind_mut().accept());
    }).join().unwrap();

Will always fail anyway.

Copy link
Author

Choose a reason for hiding this comment

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

The closure receives a reference to this object back.

I'm not sure what does it mean 🤔? Wouldn't it better to ensure that closure/callable keeps reference to a given object (i.e. if it is refcounted it won't be pruned before idle time/before said apply deferred is applied)?

I drop that sentence. I wanted to document, that the callable is actually invoked with slef. Or now after signature
update, the inner self. Should be clear though.


Regarding multi threading

Okay, i was a bit too curious and came up with a new method bind_deferred that returns a closure which is a bit more restricted, but thread safe to call ... i think?
Godot recommends call_deferred for thread safe communication back to the main thread and it seems easier than channels for some cases.

If it is interesting enough, we can add it to this pr, but I guess we shouldn't get side tracked and follow up on this topic in a different one.
Check it out here goatfryed/gdext@feat/1199-type-safe-call-deferred...feat/1199-thread-safe-call-deferred

Copy link
Contributor

Choose a reason for hiding this comment

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

If it is interesting enough, we can add it to this pr, but I guess we shouldn't get side tracked and follow up on this topic in a different one.

Yeah, I think we shouldn't support it for now 🤔; we should only restrict invocations of apply_deferred to a main thread (and note that in the description to avoid any confusion).

Dealing with cross-thread access to user-defined GodotClass instances has been discussed here: #18. IMO until we deal with this problem we shouldn't encourage any workflows related to multi threading.

Godot recommends call_deferred for thread safe communication back to the main thread and it seems easier than channels for some cases.

Yep, thus the mention – it is something people would look for naturally, so noting down that it is available only on main thread would avoid any confusion (and we provide tools to use this pattern unsafely).

@goatfryed goatfryed force-pushed the feat/1199-type-safe-call-deferred branch from f67b834 to b95974f Compare June 19, 2025 09:30
@goatfryed goatfryed force-pushed the feat/1199-type-safe-call-deferred branch 2 times, most recently from 0cc9408 to 4503c5c Compare June 19, 2025 09:48
@goatfryed goatfryed force-pushed the feat/1199-type-safe-call-deferred branch 2 times, most recently from 18f1602 to 00692f0 Compare June 19, 2025 11:54
@goatfryed goatfryed force-pushed the feat/1199-type-safe-call-deferred branch from 00692f0 to 3e988eb Compare June 19, 2025 11:57
@goatfryed
Copy link
Author

goatfryed commented Jun 19, 2025

I've now introduced apply_deferred on the various types users might want to call it on and thus refactored the code a bit.

I saw three variations

  • call on a GodotClass, e.g. self in some impl
  • call on a Gd where T is a user defined class with a base field
  • call on a Gd where T is an engine defined class

Since the implementation for user and engine defined classes differ only in generic types, I had to introduce two distinct traits. Curious, if there is a better way.

The pipeline fails atm for linux_release https://github.com/goatfryed/gdext/actions/runs/15757419567/job/44415859048
If I interpret it right, it's due to todays godot 4.5 release and some bc in another part of the code base? Please help me, if I caused this issue.

@goatfryed goatfryed requested a review from Bromeon June 19, 2025 12:23
@goatfryed goatfryed marked this pull request as ready for review June 19, 2025 12:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c: engine Godot classes (nodes, resources, ...) feature Adds functionality to the library
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Type-Safe deferred method calls
5 participants