-
-
Notifications
You must be signed in to change notification settings - Fork 229
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
base: master
Are you sure you want to change the base?
Introduce convenient, type-safe call_deferred alternative #1204
Conversation
There was a problem hiding this 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 haveapply_deferred
returning a generic return typeR
. - Regarding formatting of RustDoc comments, please check how other code does it (line length, header + body separation, etc.)
API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-1204 |
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.
I understand that godot-itest 4.1 compat could break, but I'm surprised that local |
67c8ea7
to
944805d
Compare
I need help with the doc-lint issue :) I don't understand why "it works on my machine" (🫡)... |
944805d
to
b20806a
Compare
That book page states:
In other words, But in your case it's not just notify-docs. The compatibility integration test for Godot 4.1 also fails, because |
3be4a2b
to
613b2df
Compare
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 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. |
I just checked, it's also wrong. The lint probably misses it because Thanks for bringing it up -- I can fix it, currently merging with some other doc issues that came up 🙂 |
c043bed
to
f67b834
Compare
What is the point of returning a generic type |
I meant the closure, not |
Why we would want to bind such function? Wouldn't it cause confusion instead of providing any benefit? |
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 |
godot-core/src/obj/gd.rs
Outdated
/// 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. |
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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:
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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).
f67b834
to
b95974f
Compare
0cc9408
to
4503c5c
Compare
18f1602
to
00692f0
Compare
00692f0
to
3e988eb
Compare
I've now introduced I saw three variations
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 |
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
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.