Repository: godot-rust/gdext
Pull request #1043: Async Signals
Author: login: TitanNano
Labels: name: feature
description: Adds functionality to the library
name: c: async
description: Everything related to async tasks/signals
Created: 2025-02-09T23:32:57Z | Merged: 2025-03-11T21:59:44Z

This has been developed last year in #261 and consists of two somewhat independent parts:

- **A Future for `Signal`**: an implementation of the `Future` trait for Godots signals.
- **Async runtime for Godot**: a wrapper around Godots deferred code execution that acts as a async runtime for rust futures.

The `SignalFuture` does not depend on the async runtime and vice versa, but there is no point in having a future without a way to execute it.

For limitations see: https://github.com/godot-rust/gdext/issues/261#issuecomment-2323249444

## Example

```rs
let node = Node::new_gd();

// spawn a new async task
godot_task(async move {
    // do something before waiting for a signal
    let children = node.get_children();
    
    // await a signal
    let _: () = Signal::from_object_signal(&node, "tree_entered").to_future().await;

    // do more after the signal
   children.iter_shared().for_each(|child| ... );
});
```
-----

## TODOs
- [x] Decide if we want to keep the `GuaranteedSignalFuture`. Should it be the default? (We keep it as `TrySignalFuture`, the _plain_ signal is a wrapper that panics in the error case.)
- [x] Documentation
- [x] figure out async testing.
- [x] deal with async panics (in tests)

CC @jrb0001 because they provided very valuable feedback while refining the POC.
Closes #261 

Linked issue #261: Async/Await for Signals
It should be possible to implement `Future` for signals, and add support for `async` functions in exported methods, with them returning a signal.

These two things combined should allow us to use async/await syntax when calling methods both in rust and gdscript.

This is going to require a lot of upfront work before it's feasible to start working on this feature. First an actual implementation of signals are needed, but we'll also likely need to create some godot-objects directly in our bindings that will handle the awaiting and execution of these futures, and that is not something we have planned/thought out how to do yet.
Linked issue comments: user: login: TitanNano
created_at: 2024-08-14T12:24:33Z
body: I implemented a POC for a `SignalFuture` and minimal async runtime that allows to await signals. https://github.com/godot-rust/gdext/commit/5891c5bb2a2ca15e9a1ba904797bda608dcc997d

It still has two issues which I did not bother to resolve yet:
1. ~~it's currently not possible to spawn nested tasks. It should be possible to resolve that, but I'm not sure how important it actually is.~~ EDIT: has been resolved.
2. Only Signal arguments that are `Sync + Send` can currently be used in the async context. This makes it impossible to access any `Gd<T>` coming from a signal.
author_association: CONTRIBUTOR
user: login: jrb0001
created_at: 2024-08-24T21:59:10Z
body: @TitanNano I think your implementation doesn't work correctly if the `Callable` is called on another thread than where the task has been created.

https://github.com/jrb0001/gdext/commit/74afb5b4b58a0c1cfee6e7f1d9a48a22dc57d9b0 is a variant without the thread-local, but it needs some unsafe and panics when called from another thread. It should also fix recursively creating tasks, but I didn't test it.

https://github.com/jrb0001/gdext/commit/a27d0bc384e27238410df7ac6200ed6874bfca0c is a third, more complex variant which has both `!Send` and `Send` futures. It can only be called by another thread if both the task and the future are `Send`. Otherwise it panics or doesn't even compile.

`Sync` isn't needed here because polling a `Future` needs a `&mut` so only one thread can access it at a time anyway.
author_association: CONTRIBUTOR
user: login: TitanNano
created_at: 2024-08-24T22:30:20Z
body: @jrb0001 yes that is true. I ignored that so far. Have you encountered a case were the callable was called on an other thread?

> https://github.com/jrb0001/gdext/commit/74afb5b4b58a0c1cfee6e7f1d9a48a22dc57d9b0 is a variant without the thread-local, but it needs some unsafe and panics when called from another thread. It should also fix recursively creating tasks, but I didn't test it.

I don't see the benefit of this if it still panics. 

> https://github.com/jrb0001/gdext/commit/a27d0bc384e27238410df7ac6200ed6874bfca0c is a third, more complex variant which has both !Send and Send futures. It can only be called by another thread if both the task and the future are Send. Otherwise it panics or doesn't even compile.

> Sync isn't needed here because polling a Future needs a &mut so only one thread can access it at a time anyway.

`Gd<T>` is neither `Send` nor `Sync` so as soon as either of them is required the future becomes much less useful since you can't access any `Gd<T>` inside of it.
author_association: CONTRIBUTOR
user: login: jrb0001
created_at: 2024-08-25T07:20:04Z
body: > @jrb0001 yes that is true. I ignored that so far. Have you encountered a case were the callable was called on an other thread?

No, I spotted it while reading through it.

> > [jrb0001@74afb5b](https://github.com/jrb0001/gdext/commit/74afb5b4b58a0c1cfee6e7f1d9a48a22dc57d9b0) is a variant without the thread-local, but it needs some unsafe and panics when called from another thread. It should also fix recursively creating tasks, but I didn't test it.
> 
> I don't see the benefit of this if it still panics.

The panic is guaranteed while your version can run another future which was assigned the same ID on the thread of the signal.

> > [jrb0001@a27d0bc](https://github.com/jrb0001/gdext/commit/a27d0bc384e27238410df7ac6200ed6874bfca0c) is a third, more complex variant which has both !Send and Send futures. It can only be called by another thread if both the task and the future are Send. Otherwise it panics or doesn't even compile.
> 
> > Sync isn't needed here because polling a Future needs a &mut so only one thread can access it at a time anyway.
> 
> `Gd<T>` is neither `Send` nor `Sync` so as soon as either of them is required the future becomes much less useful since you can't access any `Gd<T>` inside of it.

`LocalSignalFuture` of the third variant works with `Gd<T>` both as signal argument and when captured by a closure with the limitation that the future is `!Send` and panics if the signal happens on another thread. I think that solves the main usecase?

`SendSignalFuture` of the third variant is less useful, I agree. Maybe it makes sense to merge them and select the implementation based on the `experimental-threads` feature?
author_association: CONTRIBUTOR
user: login: TitanNano
created_at: 2024-08-25T18:34:25Z
body: > The panic is guaranteed while your version can run another future which was assigned the same ID on the thread of the signal.

This can easily be solved by adding the `ThreadId` into the Waker. 

> LocalSignalFuture of the third variant works with Gd<T> both as signal argument and when captured by a closure with the limitation that the future is !Send and panics if the signal happens on another thread. I think that solves the main usecase?

But it adds a lot of complexity without solving any problems (at least as far as I'm aware). Correctly, panicking can be solved like I mentioned above.

> SendSignalFuture of the third variant is less useful, I agree. Maybe it makes sense to merge them and select the implementation based on the experimental-threads feature?

Even with `extermental-threads` `Gd<T>` is still not thread-safe. Without access to engine managed objects, I don't see much value for a `Send + Sync` future. But we can even offer a thread-safe task spawn function which either moves the future into the Waker or uses a `static` (i.e. not thread-local) ASYNC_RUNTIME.

----

I have been thinking about the stated concern of signals being emitted on other threads, and I believe the correct solution would be to redirect the wakers future polling into the correct thread. 
Unfortunately, this is not possible as far as I can see. Godot does not expose any way to queue callable for a specific thread. Generally, there is also too little control over where things get executed to come up with a reliable approach. 

So the alternatives for now that I can see are:

a) limit async tasks to the main thread by panicking when trying to spawn a task on another thread. With this limitation in place, we can connect to signals in deferred mode inside the Signal Future and reliably run the callable on the main-thread. 

b) properly panic when the callable runs on a different thread than the future and leave the rest up to the user. This is more flexible but much less predictable, so I think a) is probably preferred.

Once there is a thread-safe version of `Gd<T>` we can add a way to spawn `Send + Sync` futures and poll them on whatever thread the signal is emitted. This should be quite trivial.
author_association: CONTRIBUTOR
user: login: jrb0001
created_at: 2024-08-25T20:11:28Z
body: There are seem to be some issues related to emitting a signal from a callable connected to itself. This will call the `Callable` a second time and its `Mutex` is already locked. From looking at Godot source, it seems that oneshot connections are disconnected after calling the `Callable`. So it needs to either ignore calls while the `Mutex` is locked or use deferred mode.

I also tried implementing `futures_lite::Stream` and the only way to avoid the issue there is to use deferred mode, as far as I can see.

So if deferred mode signals always run on the main thread, then that's probably the best option.

I also ran into an issue with the `async-channel` crate which deadlocks when the receiving `Future` is polled from inside the `Waker` (also caused by a `Mutex`). `std::task` documentation doesn't prohibit our behavior, but I wouldn't be surprised if other crates have similar issues. There can be quite some recursion if there is a chain of futures, each doing something that wakes up the next one. I think it would be better to simply avoid calling user code from inside the waker, if that's even possible.

I ended up using an Autoload with a `smol::LocalExecutor`, ~10 lines. Maybe there is some other way to execute code every frame, without adding a node or registering a class?
author_association: CONTRIBUTOR
user: login: TitanNano
created_at: 2024-08-25T22:53:31Z
body: > There are seem to be some issues related to emitting a signal from a callable connected to itself. This will call the Callable a second time and its Mutex is already locked. From looking at Godot source, it seems that oneshot connections are disconnected after calling the Callable. So it needs to either ignore calls while the Mutex is locked or use deferred mode.

I think this was changed in 4.3. In [4.2](https://github.com/godotengine/godot/blob/3504c98c1233bbd2506e89ce46509bc79afaec17/core/object/object.cpp#L1074) it's disconnected after calling and in [4.3](https://github.com/godotengine/godot/blob/28a72fa4344befeae251d8e9b11b84acd601a244/core/object/object.cpp#L1151) it's disconnected first.
author_association: CONTRIBUTOR
user: login: jrb0001
created_at: 2024-08-26T20:26:24Z
body: I think I found another two issues that affect all our implementations:

- Dropping the `Future` doesn't disconnect the `Callable`.
- Disconnecting the `Callable` causes the `Future` to get stuck forever.

Disconnecting requires the `Callable` so we need to keep it around somewhere in the `Future`.  Which means that its internal refcount will never drop to zero so the `Callable` is never dropped. I don't know any other way to reliably detect a disconnected signal.

author_association: CONTRIBUTOR
user: login: TitanNano
created_at: 2024-08-26T22:05:10Z
body: > Disconnecting requires the Callable so we need to keep it around somewhere in the Future. Which means that its internal refcount will never drop to zero so the Callable is never dropped. I don't know any other way to reliably detect a disconnected signal.

We can use a `WeakRef` I think. 

> I also ran into an issue with the async-channel crate which deadlocks when the receiving Future is polled from inside the Waker (also caused by a Mutex). std::task documentation doesn't prohibit our behavior, but I wouldn't be surprised if other crates have similar issues. There can be quite some recursion if there is a chain of futures, each doing something that wakes up the next one. I think it would be better to simply avoid calling user code from inside the waker, if that's even possible.

We might want to connect to the signal in the normal way and schedule a deferred callable to actually poll the future. But I haven't tried it yet. 
author_association: CONTRIBUTOR
user: login: jrb0001
created_at: 2024-08-27T06:08:40Z
body: > > Disconnecting requires the Callable so we need to keep it around somewhere in the Future. Which means that its internal refcount will never drop to zero so the Callable is never dropped. I don't know any other way to reliably detect a disconnected signal.
> 
> We can use a `WeakRef` I think.

Isn't that only for `RefCounted`? Unfortunately `Callable` are their own type, not even an `Object`.

> > I also ran into an issue with the async-channel crate which deadlocks when the receiving Future is polled from inside the Waker (also caused by a Mutex). std::task documentation doesn't prohibit our behavior, but I wouldn't be surprised if other crates have similar issues. There can be quite some recursion if there is a chain of futures, each doing something that wakes up the next one. I think it would be better to simply avoid calling user code from inside the waker, if that's even possible.

> We might want to connect to the signal in the normal way and schedule a deferred callable to actually poll the future. But I haven't tried it yet.

I had that idea too but didn't find a generic way to schedule it. There is `Callable::call_deferred` but that isn't exposed to gdextension as far as I can see. Maybe something like `callable.to_variant().call("call_deferred", args)` but not sure.

author_association: CONTRIBUTOR
user: login: TitanNano
created_at: 2024-08-27T08:44:13Z
body: > Isn't that only for RefCounted? Unfortunately Callable are their own type, not even an Object.

Yeah right I missed that. 

> Disconnecting requires the Callable so we need to keep it around somewhere in the Future. Which means that its internal refcount will never drop to zero so the Callable is never dropped. I don't know any other way to reliably detect a disconnected signal.

Why do you believe it will never be dropped? As soon as the future is dropped, the callable should be dropped as well, shouldn't it?

> I had that idea too but didn't find a generic way to schedule it. There is Callable::call_deferred but that isn't exposed to gdextension as far as I can see. Maybe something like callable.to_variant().call("call_deferred", args) but not sure.

Looks like `Callable::call_deferred` is only available as a `varcall` and those are not implemented for built-in types. I will see if your suggestion work and we should see that `Callable::call_deferred` is properly implemented as a varcall.
author_association: CONTRIBUTOR
user: login: TitanNano
created_at: 2024-08-27T09:50:50Z
body: I added the handling of the following cases to my implementation:
1. `godot_task` should panic if the current thread is not the main thread.
2. `Waker::wake` should panic if the current thread ID doesn't match the original one.
3. The future is now polled inside a deferred callable, so waking and polling are no longer happening in the same call stack. This way, signals emitted on a different thread should also be redirected to poll the future on the main thread now. 
4. `SignalFuture` disconnects its callable if the signal hasn't been emitted when the future is dropped.

https://github.com/godot-rust/gdext/compare/7634fe769d1fcb66209586f0b6c06aac40978253...4c2d9a85a0ea29fc0ade525e8538c216cd792c34?expand=1

EDIT: added handing for nested godot_tasks and clean-up when deinitializing. 
https://github.com/godot-rust/gdext/compare/7634fe769d1fcb66209586f0b6c06aac40978253...341a40c0a144e57edc8392090a55881b2b15f50a
author_association: CONTRIBUTOR
user: login: jrb0001
created_at: 2024-08-27T19:18:13Z
body: > > Disconnecting requires the Callable so we need to keep it around somewhere in the Future. Which means that its internal refcount will never drop to zero so the Callable is never dropped. I don't know any other way to reliably detect a disconnected signal.
> 
> Why do you believe it will never be dropped? As soon as the future is dropped, the callable should be dropped as well, shouldn't it?

If the Future is dropped, then yes, it will work as intended. But if the signal is disconnected (freed object / through code), then the Future will wait for a wake-up which will never come.
author_association: CONTRIBUTOR
user: login: TitanNano
created_at: 2024-08-27T19:37:05Z
body: > If the Future is dropped, then yes, it will work as intended. But if the signal is disconnected (freed object / through code), then the Future will wait for a wake-up which will never come.

Yes, if someone goes out of their way to disconnect the callable, then the future will never complete. I don't see how this can be avoided. I wonder how this behaves in GDScript.
author_association: CONTRIBUTOR
user: login: jrb0001
created_at: 2024-08-27T20:09:24Z
body: If something intentionally disconnect a callable it didn't connect, then we can blame that. But can we expect every code that calls `Node::queue_free()`, decrements a `RefCounted` or otherwise frees an object to check if there is any callable connected to any signal on that object?

It's possible to detect the freed object case by polling `Signal::is_null()` but that's too heavy to do by default.

I did a quick test and it looks like gdscript `await` also gets stuck, or at least doesn't continue with the next statement.
author_association: CONTRIBUTOR
user: login: TitanNano
created_at: 2024-08-28T10:32:52Z
body: @jrb0001 I pretty much had the same ideas and came to the same conclusion. Getting a callback on object deletion is just not possible, and checking all pending futures every frame is excessive. 

A middle ground solution that came to me could be: If the `AsyncRuntime` `Vec` is filled to capacity (`.len() == .capacity()`) when adding a new task, then we could go through the pending futures and try to garbage collect some of them before extending the capacity.

Alternatively, we could always check for dead futures during insert, since we already scan the buffer for empty slots anyway.
author_association: CONTRIBUTOR
user: login: TitanNano
created_at: 2024-08-28T12:20:54Z
body: I don't think we can know when a future is dead, though. If we want to keep this runtime agnostic (and I strongy believe we should) we have no information when a future will stop making progress. 

Best we can do is to return a `TaskHandle` from `godot_task` and let the caller manually cancel the task.
author_association: CONTRIBUTOR
user: login: jrb0001
created_at: 2024-08-28T18:19:11Z
body: It looks like it is possible to use a new `Callable` for disconnecting, as long as they are equal and `hash()` returns the same value.

Signal disconnected or object freed:
- Connected `Callable` is dropped automatically.
- Notify `Future`.

Future dropped:
- Do nothing if `signal.object().map(|o| o.is_instance_valid()) != Some(true)` (freed object).
- Use second `Callable` to disconnect the signal.
- Connected `Callable` is dropped automatically.
author_association: CONTRIBUTOR
user: login: TitanNano
created_at: 2024-08-28T18:30:36Z
body: > Notify `Future`.

How do you do this? 

> Future dropped:
> - Do nothing if signal.object().map(|o| o.is_instance_valid()) != Some(true) (freed object).
> - Use second Callable to disconnect the signal.
> - Connected Callable is dropped automatically.

I already implemented this.
author_association: CONTRIBUTOR
user: login: jrb0001
created_at: 2024-08-28T19:22:08Z
body: > > Notify `Future`.
> 
> How do you do this?

You probably need to change the future to return `Option<T>`/`Result<T, _>`, unless you want to let it panic on disconnect. Then inside the `Drop` impl  just write into your state and wake the `Waker`, just like you do when called from Godot.
author_association: CONTRIBUTOR
user: login: TitanNano
created_at: 2024-08-28T21:10:30Z
body: > You probably need to change the future to return Option<T>/Result<T, _>, unless you want to let it panic on disconnect. Then inside the Drop impl just write into your state and wake the Waker, just like you do when called from Godot.

Let's assume we can create a custom callable which wakes the future both on invocation and on `Drop`. The callable also can be cloned and maintains the same hash, so we don't have to reference the engine callable in the future. 

New references to the callable can still be created via `signal.get_connections()`. So `Object.free() != drop(callable)`, we can't rely on the callable being dropped when the object is freed. 

Or do you see another way to detect when the object is freed?
author_association: CONTRIBUTOR
user: login: jrb0001
created_at: 2024-08-29T19:58:08Z
body: True, as long as some other code keeps the reference it got from `signal.get_connections()`, the `Callable` isn't dropped. Such code could also call it directly or connect it to some other signal and I think that could do more damage than just a stuck `Future`.

We can't guarantee much in presence of malicious code (except the basic rust guarantees), but in my opinion best-effort detection of stuck futures is better than not having anything at all.

The guaranteed way to detect a freed object is through something like this:
```rust
let object: InstanceId = signal.object_id().unwrap();
...;
let is_freed: bool = Gd::try_from_instance_id(object).is_err();
```
It requires polling which can get expensive if there are hundreds/thousands of waiting futures and it doesn't help in the `signal.disconnect()` case. The `Drop` impl approach is much cheaper and should be good enough in most situations.
author_association: CONTRIBUTOR
user: login: TitanNano
created_at: 2024-08-29T21:20:28Z
body: I have now added:
- A `TaskHandle` that can be used to cancel a task and check if the task is still pending.
- A second `GuaranteedSignalFuture` that resolves to an `Option<R>` and wakes the future if the `Callable` is dropped.

https://github.com/godot-rust/gdext/compare/7634fe769d1fcb66209586f0b6c06aac40978253...a9d4f0e5bd3f7d4ba6231f1dcc00145d4ae3723e

Let's see how well this all works. I'm also open to suggestions for a better name, for the second future.

---

@Bromeon are you interested in including this into the project?  I will start testing this in my project now, but I happy to promote it to a PR.
author_association: CONTRIBUTOR
user: login: Bromeon
created_at: 2024-08-31T18:46:10Z
body: Very cool to see progress on this front! :+1: 

@TitanNano I'm trying to catch up, can you maybe summarize how the user API of this might look? Any notable trade-offs/downsides? Can it be feature-gated?
author_association: MEMBER
user: login: TitanNano
created_at: 2024-09-01T09:18:05Z
body: @Bromeon sure, so far, we have the following:

### User-facing APIs
- `godot_task(...)` This should be used to create a new async task on our runtime. The `TaskHandle` can be used to check if the task is still in progress and cancel it if desired.
- `TaskHandle::is_pending()` returns a `bool` indicating whether the task is still in progress or has been completed.
- `TaskHandle::cancel()` allows canceling a pending task.
- `ToSingalFuture::to_future()` creates a new `SignalFuture` from an existing signal.
- `ToGuaranteedSignalFuture::to_guaranteed_future()` creates a new `GuaranteedSignalFuture`.
- `SignalFuture` resolves when the corresponding signal is emitted.
- `GuaranteedSignalFuture` resolves when the corresponding signal is emitted or the object of the signal is freed. This future resolves to an option.

### Trade-offs
- `godot_task(...)` accepts any future and does not require them to be `Send + Sync`. This has been chosen to allow non `Send + Sync` values to be moved into the future, like `Gd<T>`. The consequence of this is that it can only be used on the main-thread and will panic on any other thread. Unfortunately, signals can be emitted from any thread and the engine only provides APIs to move execution back to the main-thread. Our non thread safe futures have to remain on their origin thread, and we can only do this on the main-thread.
When thread-safe alternatives for `Gd<T>` and other types exist, we can use thread-safe futures and have more flexibility in how and where we execute them.
- `SignalFuture` could potentially never resolve if the object of the signal is freed while the future exists. The corresponding task has to be canceled manually.
- `GuaranteedSignalFuture` also resolves when the object of the signal is freed. This is not 100% guaranteed, though. Users can acquire a reference to the signal connection of the future via `Signal::get_connections()` and the future will not resolve until all references to the connected `Callable` are dropped.
- Both futures can only resolve to `Send + Sync` values. Signal arguments have to be passed through a `Mutex` because of the `Send + Sync` constraint of `Callable`s. This makes it impossible to get a `Gd<T>`, `Dictionary` or `Array` from a signal.

### Feature Gate
All changes are self-contained, so feature gating them should not be a problem.
author_association: CONTRIBUTOR
user: login: TitanNano
created_at: 2024-09-30T22:21:10Z
body: @Bromeon let me know if you got any questions 😃 
author_association: CONTRIBUTOR

Diff:
diff --git a/godot-core/src/init/mod.rs b/godot-core/src/init/mod.rs
index 94cfaeb27..49aeeaeb0 100644
--- a/godot-core/src/init/mod.rs
+++ b/godot-core/src/init/mod.rs
@@ -172,6 +172,9 @@ fn gdext_on_level_deinit(level: InitLevel) {
         // If lowest level is unloaded, call global deinitialization.
         // No business logic by itself, but ensures consistency if re-initialization (hot-reload on Linux) occurs.
 
+        #[cfg(since_api = "4.2")]
+        crate::task::cleanup();
+
         // Garbage-collect various statics.
         // SAFETY: this is the last time meta APIs are used.
         unsafe {
diff --git a/godot-core/src/lib.rs b/godot-core/src/lib.rs
index 567d1258e..133121610 100644
--- a/godot-core/src/lib.rs
+++ b/godot-core/src/lib.rs
@@ -27,6 +27,10 @@ pub mod init;
 pub mod meta;
 pub mod obj;
 pub mod registry;
+#[cfg(since_api = "4.2")]
+pub mod task;
+#[cfg(before_api = "4.2")]
+pub mod task {}
 pub mod tools;
 
 mod storage;
diff --git a/godot-core/src/registry/signal/typed_signal.rs b/godot-core/src/registry/signal/typed_signal.rs
index 7f7901b13..fe44435e4 100644
--- a/godot-core/src/registry/signal/typed_signal.rs
+++ b/godot-core/src/registry/signal/typed_signal.rs
@@ -207,4 +207,8 @@ impl<'c, C: WithBaseField, Ps: meta::ParamTuple> TypedSignal<'c, C, Ps> {
             c.done();
         });
     }
+
+    pub(crate) fn to_untyped(&self) -> crate::builtin::Signal {
+        crate::builtin::Signal::from_object_signal(&self.receiver_object(), &*self.name)
+    }
 }
diff --git a/godot-core/src/task/async_runtime.rs b/godot-core/src/task/async_runtime.rs
new file mode 100644
index 000000000..5f683bcfd
--- /dev/null
+++ b/godot-core/src/task/async_runtime.rs
@@ -0,0 +1,485 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, LocalKey, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_fallible_future`](crate::builtin::Signal::to_fallible_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+///
+/// # Example
+/// ```no_run
+/// # use godot::builtin::Signal;
+/// # use godot::classes::Node;
+/// # use godot::obj::NewAlloc;
+/// let node = Node::new_alloc();
+/// let signal = Signal::from_object_signal(&node, "signal");
+///
+/// godot::task::spawn(async move {
+///     println!("starting task...");
+///
+///     signal.to_future::<()>().await;
+///
+///     println!("node has changed: {}", node.get_name());
+/// });
+/// ```
+pub fn spawn(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task() can only be used on the main thread"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_runtime_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _no_send_sync: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _no_send_sync: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_runtime_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                // Getting the task from the runtime might return None if the runtime has already been deinitialized. In this case, we just
+                // ignore the cancel request, as the entire runtime has already been canceled.
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_runtime(|rt| {
+            let slot = rt
+                .tasks
+                .get(self.index)
+                .unwrap_or_else(|| unreachable!("missing future slot at index {}", self.index));
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+const ASYNC_RUNTIME_DEINIT_PANIC_MESSAGE: &str = "The async runtime is being accessed after it has been deinitialized. This should not be possible and is most likely a bug.";
+
+thread_local! {
+    /// The thread local is only initialized the first time it's used. This means the async runtime won't be allocated until a task is
+    /// spawned.
+    static ASYNC_RUNTIME: RefCell<Option<AsyncRuntime>> = RefCell::new(Some(AsyncRuntime::new()));
+}
+
+/// Will be called during engine shutdown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.set(None);
+}
+
+#[cfg(feature = "trace")]
+pub fn has_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_runtime(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    /// Attempts to extract the future with the given ID from the slot.
+    ///
+    /// Puts the slot into [`FutureSlotState::Polling`] state after taking the future out. It is expected that the future is either parked
+    /// again or the slot is cleared.
+    /// In cases were the slot state is not [`FutureSlotState::Pending`], a copy of the state is returned but the slot remains untouched.
+    fn take_for_polling(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
+
+    /// Parks the future in this slot again.
+    ///
+    /// # Panics
+    /// - If the slot is not in state [`FutureSlotState::Polling`].
+    fn park(&mut self, value: T) {
+        match self.value {
+            FutureSlotState::Empty | FutureSlotState::Gone => {
+                panic!("cannot park future in slot which is unoccupied")
+            }
+            FutureSlotState::Pending(_) => {
+                panic!(
+                    "cannot park future in slot, which is already occupied by a different future"
+                )
+            }
+            FutureSlotState::Polling => {
+                self.value = FutureSlotState::Pending(value);
+            }
+        }
+    }
+}
+
+/// The storage for the pending tasks of the async runtime.
+#[derive(Default)]
+struct AsyncRuntime {
+    tasks: Vec<FutureSlot<Pin<Box<dyn Future<Output = ()>>>>>,
+    next_task_id: u64,
+    #[cfg(feature = "trace")]
+    panicked_tasks: std::collections::HashSet<u64>,
+}
+
+impl AsyncRuntime {
+    fn new() -> Self {
+        Self {
+            // We only create a new async runtime inside a thread_local, which has lazy initialization on first use.
+            tasks: Vec::with_capacity(16),
+            next_task_id: 0,
+            #[cfg(feature = "trace")]
+            panicked_tasks: std::collections::HashSet::default(),
+        }
+    }
+
+    /// Get the next task ID.
+    fn next_id(&mut self) -> u64 {
+        let id = self.next_task_id;
+        self.next_task_id += 1;
+        id
+    }
+
+    /// Store a new async task in the runtime.
+    ///
+    /// First, a linear search is performed to locate an already existing but currently unoccupied slot in the task buffer. If there is no
+    /// free slot, a new slot is added which may grow the underlying [`Vec`].
+    ///
+    /// The future storage always starts out with a capacity of 10 tasks.
+    fn add_task<F: Future<Output = ()> + 'static>(&mut self, future: F) -> TaskHandle {
+        let id = self.next_id();
+        let index_slot = self
+            .tasks
+            // If we find an available slot, we will assign the new future to it.
+            .iter_mut()
+            .enumerate()
+            .find(|(_, slot)| slot.is_empty());
+
+        let boxed = Box::pin(future);
+
+        let index = match index_slot {
+            Some((index, slot)) => {
+                *slot = FutureSlot::pending(id, boxed);
+                index
+            }
+            None => {
+                self.tasks.push(FutureSlot::pending(id, boxed));
+                self.tasks.len() - 1
+            }
+        };
+
+        TaskHandle::new(index, id)
+    }
+
+    /// Extract a pending task from the storage.
+    ///
+    /// Attempts to extract a future with the given ID from the specified index and leaves the slot in state [`FutureSlotState::Polling`].
+    /// In cases were the slot state is not [`FutureSlotState::Pending`], a copy of the state is returned but the slot remains untouched.
+    fn take_task_for_polling(
+        &mut self,
+        index: usize,
+        id: u64,
+    ) -> FutureSlotState<Pin<Box<dyn Future<Output = ()> + 'static>>> {
+        let slot = self.tasks.get_mut(index);
+        slot.map(|inner| inner.take_for_polling(id))
+            .unwrap_or(FutureSlotState::Empty)
+    }
+
+    /// Remove a future from the storage and free up its slot.
+    ///
+    /// The slot is left in the [`FutureSlotState::Gone`] state.
+    fn clear_task(&mut self, index: usize) {
+        self.tasks[index].clear();
+    }
+
+    /// Move a future back into its slot.
+    ///
+    /// # Panic
+    /// - If the underlying slot is not in the [`FutureSlotState::Polling`] state.
+    fn park_task(&mut self, index: usize, future: Pin<Box<dyn Future<Output = ()>>>) {
+        self.tasks[index].park(future);
+    }
+
+    /// Track that a future caused a panic.
+    ///
+    /// This is only available for itest.
+    #[cfg(feature = "trace")]
+    fn track_panic(&mut self, task_id: u64) {
+        self.panicked_tasks.insert(task_id);
+    }
+}
+
+trait WithRuntime {
+    fn with_runtime<R>(&'static self, f: impl FnOnce(&AsyncRuntime) -> R) -> R;
+    fn with_runtime_mut<R>(&'static self, f: impl FnOnce(&mut AsyncRuntime) -> R) -> R;
+}
+
+impl WithRuntime for LocalKey<RefCell<Option<AsyncRuntime>>> {
+    fn with_runtime<R>(&'static self, f: impl FnOnce(&AsyncRuntime) -> R) -> R {
+        self.with_borrow(|rt| {
+            let rt_ref = rt.as_ref().expect(ASYNC_RUNTIME_DEINIT_PANIC_MESSAGE);
+
+            f(rt_ref)
+        })
+    }
+
+    fn with_runtime_mut<R>(&'static self, f: impl FnOnce(&mut AsyncRuntime) -> R) -> R {
+        self.with_borrow_mut(|rt| {
+            let rt_ref = rt.as_mut().expect(ASYNC_RUNTIME_DEINIT_PANIC_MESSAGE);
+
+            f(rt_ref)
+        })
+    }
+}
+
+/// Use a godot waker to poll it's associated future.
+///
+/// # Panics
+/// - If called from a thread other than the main-thread.
+fn poll_future(godot_waker: Arc<GodotWaker>) {
+    let current_thread = thread::current().id();
+
+    assert_eq!(
+        godot_waker.thread_id,
+        current_thread,
+        "trying to poll future on a different thread!\n  Current thread: {:?}\n  Future thread: {:?}",
+        current_thread,
+        godot_waker.thread_id,
+    );
+
+    let waker = Waker::from(godot_waker.clone());
+    let mut ctx = Context::from_waker(&waker);
+
+    // Move future out of the runtime while we are polling it to avoid holding a mutable reference for the entire runtime.
+    let future = ASYNC_RUNTIME.with_runtime_mut(|rt| {
+        match rt.take_task_for_polling(godot_waker.runtime_index, godot_waker.task_id) {
+            FutureSlotState::Empty => {
+                panic!("Future slot is empty when waking it! This is a bug!");
+            }
+
+            FutureSlotState::Gone => None,
+
+            FutureSlotState::Polling => {
+                unreachable!("the same GodotWaker has been called recursively");
+            }
+
+            FutureSlotState::Pending(future) => Some(future),
+        }
+    });
+
+    let Some(future) = future else {
+        // Future has been canceled while the waker was already triggered.
+        return;
+    };
+
+    let error_context = || "Godot async task failed";
+
+    // If Future::poll() panics, the future is immediately dropped and cannot be accessed again,
+    // thus any state that may not have been unwind-safe cannot be observed later.
+    let mut future = AssertUnwindSafe(future);
+
+    let panic_result = handle_panic(error_context, move || {
+        (future.as_mut().poll(&mut ctx), future)
+    });
+
+    let Ok((poll_result, future)) = panic_result else {
+        // Polling the future caused a panic. The task state has to be cleaned up and we want track the panic if the trace feature is enabled.
+        ASYNC_RUNTIME.with_runtime_mut(|rt| {
+            #[cfg(feature = "trace")]
+            rt.track_panic(godot_waker.task_id);
+            rt.clear_task(godot_waker.runtime_index);
+        });
+
+        return;
+    };
+
+    // Update the state of the Future in the runtime.
+    ASYNC_RUNTIME.with_runtime_mut(|rt| match poll_result {
+        // Future is still pending, so we park it again.
+        Poll::Pending => rt.park_task(godot_waker.runtime_index, future.0),
+
+        // Future has resolved, so we remove it from the runtime.
+        Poll::Ready(()) => rt.clear_task(godot_waker.runtime_index),
+    });
+}
+
+/// Implementation of a [`Waker`] to poll futures with the engine.
+struct GodotWaker {
+    runtime_index: usize,
+    task_id: u64,
+    thread_id: ThreadId,
+}
+
+impl GodotWaker {
+    fn new(index: usize, task_id: u64, thread_id: ThreadId) -> Self {
+        Self {
+            runtime_index: index,
+            thread_id,
+            task_id,
+        }
+    }
+}
+
+// Uses a deferred callable to poll the associated future, i.e. at the end of the current frame.
+impl Wake for GodotWaker {
+    fn wake(self: Arc<Self>) {
+        let mut waker = Some(self);
+
+        /// Enforce the passed closure is generic over its lifetime. The compiler gets confused about the livetime of the argument otherwise.
+        /// This appears to be a common issue: https://github.com/rust-lang/rust/issues/89976
+        fn callback_type_hint<F>(f: F) -> F
+        where
+            F: for<'a> FnMut(&'a [&Variant]) -> Result<Variant, ()>,
+        {
+            f
+        }
+
+        #[cfg(not(feature = "experimental-threads"))]
+        let create_callable = Callable::from_local_fn;
+
+        #[cfg(feature = "experimental-threads")]
+        let create_callable = Callable::from_sync_fn;
+
+        let callable = create_callable(
+            "GodotWaker::wake",
+            callback_type_hint(move |_args| {
+                poll_future(waker.take().expect("Callable will never be called again"));
+                Ok(Variant::nil())
+            }),
+        );
+
+        // Schedule waker to poll the Future at the end of the frame.
+        callable.call_deferred(&[]);
+    }
+}
diff --git a/godot-core/src/task/futures.rs b/godot-core/src/task/futures.rs
new file mode 100644
index 000000000..7a13417fc
--- /dev/null
+++ b/godot-core/src/task/futures.rs
@@ -0,0 +1,330 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use core::panic;
+use std::fmt::Display;
+use std::future::{Future, IntoFuture};
+use std::pin::Pin;
+use std::sync::{Arc, Mutex};
+use std::task::{Context, Poll, Waker};
+
+use crate::builtin::{Callable, RustCallable, Signal, Variant};
+use crate::classes::object::ConnectFlags;
+use crate::meta::ParamTuple;
+use crate::obj::{EngineBitfield, WithBaseField};
+use crate::registry::signal::TypedSignal;
+
+/// The panicking counter part to the [`FallibleSignalFuture`].
+///
+/// This future works in the same way as `FallibleSignalFuture`, but panics when the signal object is freed, instead of resolving to a
+/// [`Result::Err`].
+pub struct SignalFuture<R: ParamTuple + Sync + Send>(FallibleSignalFuture<R>);
+
+impl<R: ParamTuple + Sync + Send> SignalFuture<R> {
+    fn new(signal: Signal) -> Self {
+        Self(FallibleSignalFuture::new(signal))
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> Future for SignalFuture<R> {
+    type Output = R;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        let poll_result = self.get_mut().0.poll(cx);
+
+        match poll_result {
+            Poll::Pending => Poll::Pending,
+            Poll::Ready(Ok(value)) => Poll::Ready(value),
+            Poll::Ready(Err(FallibleSignalFutureError)) => panic!(
+                "the signal object of a SignalFuture was freed, while the future was still waiting for the signal to be emitted"
+            ),
+        }
+    }
+}
+
+// Not derived, otherwise an extra bound `Output: Default` is required.
+struct SignalFutureData<T> {
+    state: SignalFutureState<T>,
+    waker: Option<Waker>,
+}
+
+impl<T> Default for SignalFutureData<T> {
+    fn default() -> Self {
+        Self {
+            state: Default::default(),
+            waker: None,
+        }
+    }
+}
+
+// Only public for itest.
+#[cfg_attr(feature = "trace", derive(Default))]
+pub struct SignalFutureResolver<R> {
+    data: Arc<Mutex<SignalFutureData<R>>>,
+}
+
+impl<R> Clone for SignalFutureResolver<R> {
+    fn clone(&self) -> Self {
+        Self {
+            data: self.data.clone(),
+        }
+    }
+}
+
+impl<R> SignalFutureResolver<R> {
+    fn new(data: Arc<Mutex<SignalFutureData<R>>>) -> Self {
+        Self { data }
+    }
+}
+
+impl<R> std::hash::Hash for SignalFutureResolver<R> {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        state.write_usize(Arc::as_ptr(&self.data) as usize);
+    }
+}
+
+impl<R> PartialEq for SignalFutureResolver<R> {
+    fn eq(&self, other: &Self) -> bool {
+        Arc::ptr_eq(&self.data, &other.data)
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> RustCallable for SignalFutureResolver<R> {
+    fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+        let waker = {
+            let mut data = self.data.lock().unwrap();
+            data.state = SignalFutureState::Ready(R::from_variant_array(args));
+
+            // We no longer need the waker after we resolved. If the future is polled again, we'll also get a new waker.
+            data.waker.take()
+        };
+
+        if let Some(waker) = waker {
+            waker.wake();
+        }
+
+        Ok(Variant::nil())
+    }
+}
+
+impl<R> Display for SignalFutureResolver<R> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "SignalFutureResolver::<{}>", std::any::type_name::<R>())
+    }
+}
+
+// This resolver will change the futures state when it's being dropped (i.e. the engine removes all connected signal callables). By marking
+// the future as dead we can resolve it to an error value the next time it gets polled.
+impl<R> Drop for SignalFutureResolver<R> {
+    fn drop(&mut self) {
+        let mut data = self.data.lock().unwrap();
+
+        if !matches!(data.state, SignalFutureState::Pending) {
+            // The future is no longer pending, so no clean up is required.
+            return;
+        }
+
+        // We mark the future as dead, so the next time it gets polled we can react to it's inability to resolve.
+        data.state = SignalFutureState::Dead;
+
+        // If we got a waker we trigger it to get the future polled. If there is no waker, then the future has not been polled yet and we
+        // simply wait for the runtime to perform the first poll.
+        if let Some(ref waker) = data.waker {
+            waker.wake_by_ref();
+        }
+    }
+}
+
+#[derive(Default)]
+enum SignalFutureState<T> {
+    #[default]
+    Pending,
+    Ready(T),
+    Dead,
+    Dropped,
+}
+
+impl<T> SignalFutureState<T> {
+    fn take(&mut self) -> Self {
+        let new_value = match self {
+            Self::Pending => Self::Pending,
+            Self::Ready(_) | Self::Dead => Self::Dead,
+            Self::Dropped => Self::Dropped,
+        };
+
+        std::mem::replace(self, new_value)
+    }
+}
+
+/// A future that tries to resolve as soon as the provided Godot signal was emitted.
+///
+/// The future might resolve to an error if the signal object is freed before the signal is emitted.
+pub struct FallibleSignalFuture<R: ParamTuple + Sync + Send> {
+    data: Arc<Mutex<SignalFutureData<R>>>,
+    callable: SignalFutureResolver<R>,
+    signal: Signal,
+}
+
+impl<R: ParamTuple + Sync + Send> FallibleSignalFuture<R> {
+    fn new(signal: Signal) -> Self {
+        debug_assert!(
+            !signal.is_null(),
+            "Failed to create a future for an invalid Signal!\nEither the signal object was already freed or the signal was not registered in the object before using it.",
+        );
+
+        let data = Arc::new(Mutex::new(SignalFutureData::default()));
+
+        // The callable currently requires that the return value is Sync + Send.
+        let callable = SignalFutureResolver::new(data.clone());
+
+        signal.connect(
+            &Callable::from_custom(callable.clone()),
+            ConnectFlags::ONE_SHOT.ord() as i64,
+        );
+
+        Self {
+            data,
+            callable,
+            signal,
+        }
+    }
+    fn poll(&mut self, cx: &mut Context<'_>) -> Poll<Result<R, FallibleSignalFutureError>> {
+        let mut data = self.data.lock().unwrap();
+
+        data.waker.replace(cx.waker().clone());
+
+        let value = data.state.take();
+
+        match value {
+            SignalFutureState::Pending => Poll::Pending,
+            SignalFutureState::Dropped => unreachable!(),
+            SignalFutureState::Dead => Poll::Ready(Err(FallibleSignalFutureError)),
+            SignalFutureState::Ready(value) => Poll::Ready(Ok(value)),
+        }
+    }
+}
+
+/// Error that might be returned  by the [`FallibleSignalFuture`].
+///
+/// This error is being resolved to when the signal object is freed before the awaited singal is emitted.
+#[derive(Debug)]
+pub struct FallibleSignalFutureError;
+
+impl Display for FallibleSignalFutureError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "The signal object was freed before the awaited signal was emitted"
+        )
+    }
+}
+
+impl std::error::Error for FallibleSignalFutureError {}
+
+impl<R: ParamTuple + Sync + Send> Future for FallibleSignalFuture<R> {
+    type Output = Result<R, FallibleSignalFutureError>;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        self.get_mut().poll(cx)
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> Drop for FallibleSignalFuture<R> {
+    fn drop(&mut self) {
+        // The callable might alredy be destroyed, this occurs during engine shutdown.
+        if self.signal.object().is_none() {
+            return;
+        }
+
+        let mut data_lock = self.data.lock().unwrap();
+
+        data_lock.state = SignalFutureState::Dropped;
+
+        drop(data_lock);
+
+        // We create a new Godot Callable from our RustCallable so we get independent reference counting.
+        let gd_callable = Callable::from_custom(self.callable.clone());
+
+        // is_connected will return true if the signal was never emited before the future is dropped.
+        if self.signal.is_connected(&gd_callable) {
+            self.signal.disconnect(&gd_callable);
+        }
+    }
+}
+
+impl Signal {
+    /// Creates a fallible future for this signal.
+    ///
+    /// The future will resolve the next time the signal is emitted.
+    /// See [`TrySignalFuture`] for details.
+    ///
+    /// Since the `Signal` type does not contain information on the signal argument types, the future output type has to be inferred from
+    /// the call to this function.
+    pub fn to_fallible_future<R: ParamTuple + Sync + Send>(&self) -> FallibleSignalFuture<R> {
+        FallibleSignalFuture::new(self.clone())
+    }
+
+    /// Creates a future for this signal.
+    ///
+    /// The future will resolve the next time the signal is emitted, but might panic if the signal object is freed.
+    /// See [`SignalFuture`] for details.
+    ///
+    /// Since the `Signal` type does not contain information on the signal argument types, the future output type has to be inferred from
+    /// the call to this function.
+    pub fn to_future<R: ParamTuple + Sync + Send>(&self) -> SignalFuture<R> {
+        SignalFuture::new(self.clone())
+    }
+}
+
+impl<C: WithBaseField, R: ParamTuple + Sync + Send> TypedSignal<'_, C, R> {
+    /// Creates a fallible future for this signal.
+    ///
+    /// The future will resolve the next time the signal is emitted.
+    /// See [`FallibleSignalFuture`] for details.
+    pub fn to_fallible_future(&self) -> FallibleSignalFuture<R> {
+        FallibleSignalFuture::new(self.to_untyped())
+    }
+
+    /// Creates a future for this signal.
+    ///
+    /// The future will resolve the next time the signal is emitted, but might panic if the signal object is freed.
+    /// See [`SignalFuture`] for details.
+    pub fn to_future(&self) -> SignalFuture<R> {
+        SignalFuture::new(self.to_untyped())
+    }
+}
+
+impl<C: WithBaseField, R: ParamTuple + Sync + Send> IntoFuture for &TypedSignal<'_, C, R> {
+    type Output = R;
+
+    type IntoFuture = SignalFuture<R>;
+
+    fn into_future(self) -> Self::IntoFuture {
+        self.to_future()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::sys;
+    use std::sync::Arc;
+
+    use super::SignalFutureResolver;
+
+    /// Test that the hash of a cloned future resolver is equal to its original version. With this equality in place, we can create new
+    /// Callables that are equal to their original version but have separate reference counting.
+    #[test]
+    fn future_resolver_cloned_hash() {
+        let resolver_a = SignalFutureResolver::<u8>::new(Arc::default());
+        let resolver_b = resolver_a.clone();
+
+        let hash_a = sys::hash_value(&resolver_a);
+        let hash_b = sys::hash_value(&resolver_b);
+
+        assert_eq!(hash_a, hash_b);
+    }
+}
diff --git a/godot-core/src/task/mod.rs b/godot-core/src/task/mod.rs
new file mode 100644
index 000000000..0834fbdf4
--- /dev/null
+++ b/godot-core/src/task/mod.rs
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+//! Integrates async rust code with the engine.
+//!
+//! This module contains:
+//! - Implementations of [`Future`](std::future::Future) for [`Signal`](crate::builtin::Signal) and [`TypedSignal`](crate::registry::signal::TypedSignal).
+//! - A way to [`spawn`] new async tasks by using the engine as the async runtime.
+
+mod async_runtime;
+mod futures;
+
+pub(crate) use async_runtime::cleanup;
+
+pub use async_runtime::{spawn, TaskHandle};
+pub use futures::{FallibleSignalFuture, FallibleSignalFutureError, SignalFuture};
+
+// Only exported for itest.
+#[cfg(feature = "trace")]
+pub use async_runtime::has_godot_task_panicked;
+#[cfg(feature = "trace")]
+pub use futures::SignalFutureResolver;
diff --git a/godot-macros/src/itest.rs b/godot-macros/src/itest.rs
index d0d23b696..6c0bb06bb 100644
--- a/godot-macros/src/itest.rs
+++ b/godot-macros/src/itest.rs
@@ -8,7 +8,7 @@
 use proc_macro2::TokenStream;
 use quote::{quote, ToTokens};
 
-use crate::util::{bail, path_ends_with, KvParser};
+use crate::util::{bail, extract_typename, ident, path_ends_with, KvParser};
 use crate::ParseResult;
 
 pub fn attribute_itest(input_item: venial::Item) -> ParseResult<TokenStream> {
@@ -17,20 +17,21 @@ pub fn attribute_itest(input_item: venial::Item) -> ParseResult<TokenStream> {
         _ => return bail!(&input_item, "#[itest] can only be applied to functions"),
     };
 
+    let mut attr = KvParser::parse_required(&func.attributes, "itest", &func.name)?;
+    let skipped = attr.handle_alone("skip")?;
+    let focused = attr.handle_alone("focus")?;
+    let is_async = attr.handle_alone("async")?;
+    attr.finish()?;
+
     // Note: allow attributes for things like #[rustfmt] or #[clippy]
     if func.generic_params.is_some()
         || func.params.len() > 1
-        || func.return_ty.is_some()
+        || (func.return_ty.is_some() && !is_async)
         || func.where_clause.is_some()
     {
         return bad_signature(&func);
     }
 
-    let mut attr = KvParser::parse_required(&func.attributes, "itest", &func.name)?;
-    let skipped = attr.handle_alone("skip")?;
-    let focused = attr.handle_alone("focus")?;
-    attr.finish()?;
-
     if skipped && focused {
         return bail!(
             func.name,
@@ -47,9 +48,13 @@ pub fn attribute_itest(input_item: venial::Item) -> ParseResult<TokenStream> {
             // Correct parameter type (crude macro check) -> reuse parameter name
             if path_ends_with(&param.ty.tokens, "TestContext") {
                 param.to_token_stream()
+            } else if is_async {
+                return bad_async_signature(&func);
             } else {
                 return bad_signature(&func);
             }
+        } else if is_async {
+            return bad_async_signature(&func);
         } else {
             return bad_signature(&func);
         }
@@ -57,14 +62,35 @@ pub fn attribute_itest(input_item: venial::Item) -> ParseResult<TokenStream> {
         quote! { __unused_context: &crate::framework::TestContext }
     };
 
+    if is_async
+        && func
+            .return_ty
+            .as_ref()
+            .and_then(extract_typename)
+            .map_or(true, |segment| segment.ident != "TaskHandle")
+    {
+        return bad_async_signature(&func);
+    }
+
     let body = &func.body;
 
+    let (return_tokens, test_case_ty, plugin_name);
+    if is_async {
+        return_tokens = quote! { -> TaskHandle };
+        test_case_ty = quote! { crate::framework::AsyncRustTestCase };
+        plugin_name = ident("__GODOT_ASYNC_ITEST");
+    } else {
+        return_tokens = TokenStream::new();
+        test_case_ty = quote! { crate::framework::RustTestCase };
+        plugin_name = ident("__GODOT_ITEST");
+    };
+
     Ok(quote! {
-        pub fn #test_name(#param) {
+        pub fn #test_name(#param) #return_tokens {
             #body
         }
 
-        ::godot::sys::plugin_add!(crate::framework::__GODOT_ITEST; crate::framework::RustTestCase {
+        ::godot::sys::plugin_add!(crate::framework::#plugin_name; #test_case_ty {
             name: #test_name_str,
             skipped: #skipped,
             focused: #focused,
@@ -84,3 +110,13 @@ fn bad_signature(func: &venial::Function) -> Result<TokenStream, venial::Error>
         f = func.name,
     )
 }
+
+fn bad_async_signature(func: &venial::Function) -> Result<TokenStream, venial::Error> {
+    bail!(
+        func,
+        "#[itest(async)] function must have one of these signatures:\
+        \n  fn {f}() -> TaskHandle {{ ... }}\
+        \n  fn {f}(ctx: &TestContext) -> TaskHandle {{ ... }}",
+        f = func.name,
+    )
+}
diff --git a/godot/src/lib.rs b/godot/src/lib.rs
index c8409382d..52a84c73d 100644
--- a/godot/src/lib.rs
+++ b/godot/src/lib.rs
@@ -35,6 +35,7 @@
 //! * [`tools`], higher-level utilities that extend the generated code, e.g. `load<T>()`.
 //! * [`meta`], fundamental information about types, properties and conversions.
 //! * [`init`], entry point and global library configuration.
+//! * [`task`], integration with async code.
 //!
 //! The [`prelude`] contains often-imported symbols; feel free to `use godot::prelude::*` in your code.
 //! <br><br>
@@ -160,7 +161,7 @@ compile_error!("The feature `double-precision` currently requires `api-custom` d
 // Modules
 
 #[doc(inline)]
-pub use godot_core::{builtin, classes, global, meta, obj, tools};
+pub use godot_core::{builtin, classes, global, meta, obj, task, tools};
 
 #[doc(hidden)]
 pub use godot_core::possibly_docs as docs;
diff --git a/itest/godot/TestRunner.gd b/itest/godot/TestRunner.gd
index 0f204863a..0f27f059d 100644
--- a/itest/godot/TestRunner.gd
+++ b/itest/godot/TestRunner.gd
@@ -66,20 +66,24 @@ func _ready():
 
 	var property_tests = load("res://gen/GenPropertyTests.gd").new()
 
-	var success: bool = rust_runner.run_all_tests(
+	# Run benchmarks after all synchronous and asynchronous tests have completed.
+	var run_benchmarks = func (success: bool):
+		if success:
+			rust_runner.run_all_benchmarks(self)
+
+		var exit_code: int = 0 if success else 1
+		get_tree().quit(exit_code)
+
+	rust_runner.run_all_tests(
 		gdscript_tests,
 		gdscript_suites.size(),
 		allow_focus,
 		self,
 		filters,
-		property_tests
+		property_tests,
+		run_benchmarks
 	)
 
-	if success:
-		rust_runner.run_all_benchmarks(self)
-
-	var exit_code: int = 0 if success else 1
-	get_tree().quit(exit_code)
 
 
 class GDScriptTestCase:
diff --git a/itest/rust/src/engine_tests/async_test.rs b/itest/rust/src/engine_tests/async_test.rs
new file mode 100644
index 000000000..3e62d5ede
--- /dev/null
+++ b/itest/rust/src/engine_tests/async_test.rs
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::ops::Deref;
+
+use godot::builtin::{Callable, Signal, Variant};
+use godot::classes::{Object, RefCounted};
+use godot::meta::ToGodot;
+use godot::obj::{Base, Gd, NewAlloc, NewGd};
+use godot::prelude::{godot_api, GodotClass};
+use godot::task::{self, SignalFuture, SignalFutureResolver, TaskHandle};
+
+use crate::framework::{itest, TestContext};
+
+#[derive(GodotClass)]
+#[class(init)]
+struct AsyncRefCounted {
+    base: Base<RefCounted>,
+}
+
+#[godot_api]
+impl AsyncRefCounted {
+    #[signal]
+    fn custom_signal(value: u32);
+}
+
+#[itest(async)]
+fn start_async_task() -> TaskHandle {
+    let mut object = RefCounted::new_gd();
+    let object_ref = object.clone();
+    let signal = Signal::from_object_signal(&object, "custom_signal");
+
+    object.add_user_signal("custom_signal");
+
+    let task_handle = task::spawn(async move {
+        let signal_future: SignalFuture<(u8,)> = signal.to_future();
+        let (result,) = signal_future.await;
+
+        assert_eq!(result, 10);
+        drop(object_ref);
+    });
+
+    object.emit_signal("custom_signal", &[10.to_variant()]);
+
+    task_handle
+}
+
+#[itest]
+fn cancel_async_task(ctx: &TestContext) {
+    let tree = ctx.scene_tree.get_tree().unwrap();
+    let signal = Signal::from_object_signal(&tree, "process_frame");
+
+    let handle = task::spawn(async move {
+        let _: () = signal.to_future().await;
+
+        unreachable!();
+    });
+
+    handle.cancel();
+}
+
+#[itest(async)]
+fn async_task_fallible_signal_future() -> TaskHandle {
+    let mut obj = Object::new_alloc();
+
+    let signal = Signal::from_object_signal(&obj, "script_changed");
+
+    let handle = task::spawn(async move {
+        let result = signal.to_fallible_future::<()>().await;
+
+        assert!(result.is_err());
+    });
+
+    obj.call_deferred("free", &[]);
+
+    handle
+}
+
+// Test that two callables created from the same future resolver (but cloned) are equal, while they are not equal to an unrelated
+// callable.
+#[itest]
+fn resolver_callabable_equality() {
+    let resolver = SignalFutureResolver::<(u8,)>::default();
+
+    let callable = Callable::from_custom(resolver.clone());
+    let cloned_callable = Callable::from_custom(resolver.clone());
+    let unrelated_callable = Callable::from_local_fn("unrelated", |_| Ok(Variant::nil()));
+
+    assert_eq!(callable, cloned_callable);
+    assert_ne!(callable, unrelated_callable);
+    assert_ne!(cloned_callable, unrelated_callable);
+}
+
+#[itest(async)]
+fn async_typed_signal() -> TaskHandle {
+    let object = AsyncRefCounted::new_gd();
+    let object_ref = object.clone();
+
+    let task_handle = task::spawn(async move {
+        let (result,) = object.signals().custom_signal().deref().await;
+
+        assert_eq!(result, 66);
+    });
+
+    object_ref.signals().custom_signal().emit(66);
+
+    task_handle
+}
diff --git a/itest/rust/src/engine_tests/mod.rs b/itest/rust/src/engine_tests/mod.rs
index 3419b5599..f20a21160 100644
--- a/itest/rust/src/engine_tests/mod.rs
+++ b/itest/rust/src/engine_tests/mod.rs
@@ -5,6 +5,8 @@
  * file, You can obtain one at https://mozilla.org/MPL/2.0/.
  */
 
+#[cfg(since_api = "4.2")]
+mod async_test;
 mod codegen_enums_test;
 mod codegen_test;
 mod engine_enum_test;
diff --git a/itest/rust/src/framework/mod.rs b/itest/rust/src/framework/mod.rs
index f7f519ae7..d581c9ecf 100644
--- a/itest/rust/src/framework/mod.rs
+++ b/itest/rust/src/framework/mod.rs
@@ -24,10 +24,12 @@ pub use godot::test::{bench, itest};
 
 // Registers all the `#[itest]` tests and `#[bench]` benchmarks.
 sys::plugin_registry!(pub(crate) __GODOT_ITEST: RustTestCase);
+#[cfg(since_api = "4.2")]
+sys::plugin_registry!(pub(crate) __GODOT_ASYNC_ITEST: AsyncRustTestCase);
 sys::plugin_registry!(pub(crate) __GODOT_BENCH: RustBenchmark);
 
 /// Finds all `#[itest]` tests.
-fn collect_rust_tests(filters: &[String]) -> (Vec<RustTestCase>, usize, bool) {
+fn collect_rust_tests(filters: &[String]) -> (Vec<RustTestCase>, HashSet<&str>, bool) {
     let mut all_files = HashSet::new();
     let mut tests: Vec<RustTestCase> = vec![];
     let mut is_focus_run = false;
@@ -50,7 +52,38 @@ fn collect_rust_tests(filters: &[String]) -> (Vec<RustTestCase>, usize, bool) {
     // Sort alphabetically for deterministic run order
     tests.sort_by_key(|test| test.file);
 
-    (tests, all_files.len(), is_focus_run)
+    (tests, all_files, is_focus_run)
+}
+
+/// Finds all `#[itest(async)]` tests.
+#[cfg(since_api = "4.2")]
+fn collect_async_rust_tests(
+    filters: &[String],
+    sync_focus_run: bool,
+) -> (Vec<AsyncRustTestCase>, HashSet<&str>, bool) {
+    let mut all_files = HashSet::new();
+    let mut tests = vec![];
+    let mut is_focus_run = sync_focus_run;
+
+    sys::plugin_foreach!(__GODOT_ASYNC_ITEST; |test: &AsyncRustTestCase| {
+        // First time a focused test is encountered, switch to "focused" mode and throw everything away.
+        if !is_focus_run && test.focused {
+            tests.clear();
+            all_files.clear();
+            is_focus_run = true;
+        }
+
+        // Only collect tests if normal mode, or focus mode and test is focused.
+        if (!is_focus_run || test.focused) && passes_filter(filters, test.name) {
+            all_files.insert(test.file);
+            tests.push(*test);
+        }
+    });
+
+    // Sort alphabetically for deterministic run order
+    tests.sort_by_key(|test| test.file);
+
+    (tests, all_files, is_focus_run)
 }
 
 /// Finds all `#[bench]` benchmarks.
@@ -71,7 +104,7 @@ fn collect_rust_benchmarks() -> (Vec<RustBenchmark>, usize) {
 
 // ----------------------------------------------------------------------------------------------------------------------------------------------
 // Shared types
-
+#[derive(Clone)]
 pub struct TestContext {
     pub scene_tree: Gd<Node>,
     pub property_tests: Gd<Node>,
@@ -108,6 +141,19 @@ pub struct RustTestCase {
     pub function: fn(&TestContext),
 }
 
+#[cfg(since_api = "4.2")]
+#[derive(Copy, Clone)]
+pub struct AsyncRustTestCase {
+    pub name: &'static str,
+    pub file: &'static str,
+    pub skipped: bool,
+    /// If one or more tests are focused, only they will be executed. Helpful for debugging and working on specific features.
+    pub focused: bool,
+    #[allow(dead_code)]
+    pub line: u32,
+    pub function: fn(&TestContext) -> godot::task::TaskHandle,
+}
+
 #[derive(Copy, Clone)]
 pub struct RustBenchmark {
     pub name: &'static str,
diff --git a/itest/rust/src/framework/runner.rs b/itest/rust/src/framework/runner.rs
index 299809614..097969b64 100644
--- a/itest/rust/src/framework/runner.rs
+++ b/itest/rust/src/framework/runner.rs
@@ -7,7 +7,7 @@
 
 use std::time::{Duration, Instant};
 
-use godot::builtin::{Array, GString, Variant, VariantArray};
+use godot::builtin::{Array, Callable, GString, Variant, VariantArray};
 use godot::classes::{Engine, Node, Os};
 use godot::global::godot_error;
 use godot::meta::ToGodot;
@@ -18,19 +18,30 @@ use crate::framework::{
     bencher, passes_filter, BenchResult, RustBenchmark, RustTestCase, TestContext,
 };
 
+#[cfg(since_api = "4.2")]
+use super::AsyncRustTestCase;
+
+#[derive(Debug, Clone, Default)]
+struct TestStats {
+    total: usize,
+    passed: usize,
+    skipped: usize,
+    failed_list: Vec<String>,
+}
+
 #[derive(GodotClass, Debug)]
 #[class(init)]
 pub struct IntegrationTests {
-    total: i64,
-    passed: i64,
-    skipped: i64,
-    failed_list: Vec<String>,
+    stats: TestStats,
     focus_run: bool,
+    #[cfg(before_api = "4.2")]
+    base: godot::obj::Base<godot::classes::RefCounted>,
 }
 
 #[godot_api]
 impl IntegrationTests {
     #[allow(clippy::uninlined_format_args)]
+    #[allow(clippy::too_many_arguments)]
     #[func]
     fn run_all_tests(
         &mut self,
@@ -40,7 +51,8 @@ impl IntegrationTests {
         scene_tree: Gd<Node>,
         filters: VariantArray,
         property_tests: Gd<Node>,
-    ) -> bool {
+        on_finished: Callable,
+    ) {
         println!("{}Run{} Godot integration tests...", FMT_CYAN_BOLD, FMT_END);
         let filters: Vec<String> = filters.iter_shared().map(|v| v.to::<String>()).collect();
         let gdscript_tests = gdscript_tests
@@ -50,20 +62,19 @@ impl IntegrationTests {
                 passes_filter(filters.as_slice(), &test_name)
             })
             .collect::<Array<_>>();
-        let (rust_tests, rust_file_count, focus_run) =
-            super::collect_rust_tests(filters.as_slice());
+
+        let rust_test_cases = collect_rust_tests(&filters);
 
         // Print based on focus/not focus.
-        self.focus_run = focus_run;
-        if focus_run {
+        self.focus_run = rust_test_cases.focus_run;
+        if rust_test_cases.focus_run {
             println!("  {FMT_CYAN}Focused run{FMT_END} -- execute only selected Rust tests.")
         }
         println!(
             "  Rust: found {} tests in {} files.",
-            rust_tests.len(),
-            rust_file_count
+            rust_test_cases.rust_test_count, rust_test_cases.rust_file_count
         );
-        if !focus_run {
+        if !rust_test_cases.focus_run {
             println!(
                 "  GDScript: found {} tests in {} files.",
                 gdscript_tests.len(),
@@ -72,18 +83,74 @@ impl IntegrationTests {
         }
 
         let clock = Instant::now();
-        self.run_rust_tests(rust_tests, scene_tree, property_tests.clone());
+        self.run_rust_tests(
+            rust_test_cases.rust_tests,
+            scene_tree.clone(),
+            property_tests.clone(),
+        );
         let rust_time = clock.elapsed();
-        property_tests.free();
 
-        let gdscript_time = if !focus_run {
+        let gdscript_time = if !rust_test_cases.focus_run {
             let extra_duration = self.run_gdscript_tests(gdscript_tests);
-            Some((clock.elapsed() - rust_time) + extra_duration)
+            Some((clock.elapsed() - rust_time, extra_duration))
         } else {
             None
         };
 
-        self.conclude_tests(rust_time, gdscript_time, allow_focus)
+        #[cfg(before_api = "4.2")]
+        {
+            use godot::obj::WithBaseField;
+
+            property_tests.free();
+
+            let result = Self::conclude_tests(
+                &self.stats,
+                rust_time,
+                gdscript_time.map(|(elapsed, extra)| elapsed + extra),
+                allow_focus,
+            );
+
+            // on_finished will call back into self, so we have to make self re-entrant. We also can't call on_finished in deferred mode,
+            // since it's not available under the 4.1 API.
+            let base = self.base_mut();
+            on_finished.callv(&godot::builtin::varray![result]);
+            // We should do something with base to satisfy the compiler.
+            drop(base);
+        }
+
+        #[cfg(since_api = "4.2")]
+        {
+            let stats = self.stats.clone();
+
+            let on_finalize_test = move |stats, property_tests: Gd<Node>| {
+                let gdscript_elapsed = gdscript_time
+                    .as_ref()
+                    .map(|gdtime| gdtime.0)
+                    .unwrap_or_default();
+
+                let rust_async_time = clock.elapsed() - rust_time - gdscript_elapsed;
+
+                property_tests.free();
+
+                let result = Self::conclude_tests(
+                    &stats,
+                    rust_time + rust_async_time,
+                    gdscript_time.map(|(elapsed, extra)| elapsed + extra),
+                    allow_focus,
+                );
+
+                // Calling deferred to break a potentially synchronous call stack and avoid re-entrancy.
+                on_finished.call(&[result.to_variant()]);
+            };
+
+            Self::run_async_rust_tests(
+                stats,
+                rust_test_cases.async_rust_tests,
+                scene_tree,
+                property_tests,
+                on_finalize_test,
+            );
+        }
     }
 
     #[func]
@@ -139,14 +206,69 @@ impl IntegrationTests {
 
         let mut last_file = None;
         for test in tests {
-            print_test_pre(test.name, test.file.to_string(), &mut last_file, false);
+            print_test_pre(test.name, test.file, last_file.as_deref(), false);
+            last_file = Some(test.file.to_string());
+
             let outcome = run_rust_test(&test, &ctx);
 
-            self.update_stats(&outcome, test.file, test.name);
+            Self::update_stats(&mut self.stats, &outcome, test.file, test.name);
             print_test_post(test.name, outcome);
         }
     }
 
+    #[cfg(since_api = "4.2")]
+    fn run_async_rust_tests(
+        stats: TestStats,
+        tests: Vec<AsyncRustTestCase>,
+        scene_tree: Gd<Node>,
+        property_tests: Gd<Node>,
+        on_finalize_test: impl FnOnce(TestStats, Gd<Node>) + 'static,
+    ) {
+        let mut tests_iter = tests.into_iter();
+
+        let Some(first_test) = tests_iter.next() else {
+            return on_finalize_test(stats, property_tests);
+        };
+
+        let ctx = TestContext {
+            scene_tree,
+            property_tests,
+        };
+
+        Self::run_async_rust_tests_step(tests_iter, first_test, ctx, stats, None, on_finalize_test);
+    }
+
+    #[cfg(since_api = "4.2")]
+    fn run_async_rust_tests_step(
+        mut tests_iter: impl Iterator<Item = AsyncRustTestCase> + 'static,
+        test: AsyncRustTestCase,
+        ctx: TestContext,
+        mut stats: TestStats,
+        mut last_file: Option<String>,
+        on_finalize_test: impl FnOnce(TestStats, Gd<Node>) + 'static,
+    ) {
+        print_test_pre(test.name, test.file, last_file.as_deref(), true);
+        last_file.replace(test.file.to_string());
+
+        run_async_rust_test(&test, &ctx.clone(), move |outcome| {
+            Self::update_stats(&mut stats, &outcome, test.file, test.name);
+            print_test_post(test.name, outcome);
+
+            if let Some(next) = tests_iter.next() {
+                return Self::run_async_rust_tests_step(
+                    tests_iter,
+                    next,
+                    ctx,
+                    stats,
+                    last_file,
+                    on_finalize_test,
+                );
+            }
+
+            on_finalize_test(stats, ctx.property_tests);
+        });
+    }
+
     fn run_gdscript_tests(&mut self, tests: VariantArray) -> Duration {
         let mut last_file = None;
         let mut extra_duration = Duration::new(0, 0);
@@ -155,7 +277,9 @@ impl IntegrationTests {
             let test_file = get_property(&test, "suite_name");
             let test_case = get_property(&test, "method_name");
 
-            print_test_pre(&test_case, test_file.clone(), &mut last_file, true);
+            print_test_pre(&test_case, &test_file, last_file.as_deref(), true);
+
+            last_file = Some(test_file.clone());
 
             // If GDScript invokes Rust code that fails, the panic would break through; catch it.
             // TODO(bromeon): use try_call() once available.
@@ -191,28 +315,28 @@ impl IntegrationTests {
                 }
             };
 
-            self.update_stats(&outcome, &test_file, &test_case);
+            Self::update_stats(&mut self.stats, &outcome, &test_file, &test_case);
             print_test_post(&test_case, outcome);
         }
         extra_duration
     }
 
     fn conclude_tests(
-        &self,
+        stats: &TestStats,
         rust_time: Duration,
         gdscript_time: Option<Duration>,
         allow_focus: bool,
     ) -> bool {
-        let Self {
+        let TestStats {
             total,
             passed,
             skipped,
             ..
-        } = *self;
+        } = stats;
 
         // Consider 0 tests run as a failure too, because it's probably a problem with the run itself.
         let failed = total - passed - skipped;
-        let all_passed = failed == 0 && total != 0;
+        let all_passed = failed == 0 && *total != 0;
 
         let outcome = TestOutcome::from_bool(all_passed);
 
@@ -220,7 +344,7 @@ impl IntegrationTests {
         let gdscript_time = gdscript_time.map(|t| t.as_secs_f32());
         let focused_run = gdscript_time.is_none();
 
-        let extra = if skipped > 0 {
+        let extra = if *skipped > 0 {
             format!(", {skipped} skipped")
         } else if focused_run {
             " (focused run)".to_string()
@@ -241,12 +365,12 @@ impl IntegrationTests {
         if !all_passed {
             println!("\n  Failed tests:");
             let max = 10;
-            for test in self.failed_list.iter().take(max) {
+            for test in stats.failed_list.iter().take(max) {
                 println!("  * {test}");
             }
 
-            if self.failed_list.len() > max {
-                println!("  * ... and {} more.", self.failed_list.len() - max);
+            if stats.failed_list.len() > max {
+                println!("  * ... and {} more.", stats.failed_list.len() - max);
             }
 
             println!();
@@ -271,7 +395,9 @@ impl IntegrationTests {
 
         let mut last_file = None;
         for bench in benchmarks {
-            print_bench_pre(bench.name, bench.file.to_string(), &mut last_file);
+            print_bench_pre(bench.name, bench.file, last_file.as_deref());
+            last_file = Some(bench.file.to_string());
+
             let result = bencher::run_benchmark(bench.function, bench.repetitions);
             print_bench_post(result);
         }
@@ -282,16 +408,21 @@ impl IntegrationTests {
         println!("\nBenchmarks completed in {secs:.2}s.");
     }
 
-    fn update_stats(&mut self, outcome: &TestOutcome, test_file: &str, test_name: &str) {
-        self.total += 1;
+    fn update_stats(
+        stats: &mut TestStats,
+        outcome: &TestOutcome,
+        test_file: &str,
+        test_name: &str,
+    ) {
+        stats.total += 1;
         match outcome {
-            TestOutcome::Passed => self.passed += 1,
-            TestOutcome::Failed => self.failed_list.push(format!(
+            TestOutcome::Passed => stats.passed += 1,
+            TestOutcome::Failed => stats.failed_list.push(format!(
                 "{} > {}",
                 extract_file_subtitle(test_file),
                 test_name
             )),
-            TestOutcome::Skipped => self.skipped += 1,
+            TestOutcome::Skipped => stats.skipped += 1,
         }
     }
 }
@@ -320,7 +451,72 @@ fn run_rust_test(test: &RustTestCase, ctx: &TestContext) -> TestOutcome {
     TestOutcome::from_bool(success.is_ok())
 }
 
-fn print_test_pre(test_case: &str, test_file: String, last_file: &mut Option<String>, flush: bool) {
+#[cfg(since_api = "4.2")]
+fn run_async_rust_test(
+    test: &AsyncRustTestCase,
+    ctx: &TestContext,
+    on_test_finished: impl FnOnce(TestOutcome) + 'static,
+) {
+    if test.skipped {
+        return on_test_finished(TestOutcome::Skipped);
+    }
+
+    // Explicit type to prevent tests from returning a value
+    let err_context = || format!("itest `{}` failed", test.name);
+    let success: Result<godot::task::TaskHandle, _> =
+        godot::private::handle_panic(err_context, || (test.function)(ctx));
+
+    let Ok(task_handle) = success else {
+        return on_test_finished(TestOutcome::Failed);
+    };
+
+    check_async_test_task(task_handle, on_test_finished, ctx);
+}
+
+#[cfg(since_api = "4.2")]
+fn check_async_test_task(
+    task_handle: godot::task::TaskHandle,
+    on_test_finished: impl FnOnce(TestOutcome) + 'static,
+    ctx: &TestContext,
+) {
+    use godot::classes::object::ConnectFlags;
+    use godot::obj::EngineBitfield;
+    use godot::task::has_godot_task_panicked;
+
+    if !task_handle.is_pending() {
+        on_test_finished(TestOutcome::from_bool(!has_godot_task_panicked(
+            task_handle,
+        )));
+
+        return;
+    }
+
+    let next_ctx = ctx.clone();
+    let mut callback = Some(on_test_finished);
+    let mut probably_task_handle = Some(task_handle);
+
+    let deferred = Callable::from_local_fn("run_async_rust_test", move |_| {
+        check_async_test_task(
+            probably_task_handle
+                .take()
+                .expect("Callable will only be called once!"),
+            callback
+                .take()
+                .expect("Callable should not be called multiple times!"),
+            &next_ctx,
+        );
+        Ok(Variant::nil())
+    });
+
+    ctx.scene_tree
+        .get_tree()
+        .expect("The itest scene tree node is part of a Godot SceneTree")
+        .connect_ex("process_frame", &deferred)
+        .flags(ConnectFlags::ONE_SHOT.ord() as u32)
+        .done();
+}
+
+fn print_test_pre(test_case: &str, test_file: &str, last_file: Option<&str>, flush: bool) {
     print_file_header(test_file, last_file);
 
     print!("   -- {test_case} ... ");
@@ -331,16 +527,13 @@ fn print_test_pre(test_case: &str, test_file: String, last_file: &mut Option<Str
     }
 }
 
-fn print_file_header(file: String, last_file: &mut Option<String>) {
+fn print_file_header(file: &str, last_file: Option<&str>) {
     // Check if we need to open a new category for a file.
-    let print_file = last_file.as_ref() != Some(&file);
+    let print_file = last_file != Some(file);
 
     if print_file {
-        println!("\n   {}:", extract_file_subtitle(&file));
+        println!("\n   {}:", extract_file_subtitle(file));
     }
-
-    // State update for file-category-print
-    *last_file = Some(file);
 }
 
 fn extract_file_subtitle(file: &str) -> &str {
@@ -364,7 +557,7 @@ fn print_test_post(test_case: &str, outcome: TestOutcome) {
     }
 }
 
-fn print_bench_pre(benchmark: &str, bench_file: String, last_file: &mut Option<String>) {
+fn print_bench_pre(benchmark: &str, bench_file: &str, last_file: Option<&str>) {
     print_file_header(bench_file, last_file);
 
     let benchmark = if benchmark.len() > 26 {
@@ -402,6 +595,53 @@ fn get_errors(test: &Variant) -> Array<GString> {
         .unwrap_or_default()
 }
 
+struct RustTestCases {
+    rust_tests: Vec<RustTestCase>,
+    #[cfg(since_api = "4.2")]
+    async_rust_tests: Vec<AsyncRustTestCase>,
+    rust_test_count: usize,
+    rust_file_count: usize,
+    focus_run: bool,
+}
+
+#[cfg(before_api = "4.2")]
+fn collect_rust_tests(filters: &[String]) -> RustTestCases {
+    let (rust_tests, rust_files, focus_run) = super::collect_rust_tests(filters);
+
+    let rust_test_count = rust_tests.len();
+
+    RustTestCases {
+        rust_tests,
+        rust_test_count,
+        rust_file_count: rust_files.len(),
+        focus_run,
+    }
+}
+
+#[cfg(since_api = "4.2")]
+fn collect_rust_tests(filters: &[String]) -> RustTestCases {
+    let (mut rust_tests, mut rust_files, focus_run) = super::collect_rust_tests(filters);
+
+    let (async_rust_tests, async_rust_files, async_focus_run) =
+        super::collect_async_rust_tests(filters, focus_run);
+
+    if !focus_run && async_focus_run {
+        rust_tests.clear();
+        rust_files.clear();
+    }
+
+    let rust_test_count = rust_tests.len() + async_rust_tests.len();
+    let rust_file_count = rust_files.union(&async_rust_files).count();
+
+    RustTestCases {
+        rust_tests,
+        async_rust_tests,
+        rust_test_count,
+        rust_file_count,
+        focus_run: focus_run || async_focus_run,
+    }
+}
+
 #[must_use]
 enum TestOutcome {
     Passed,


Reviews:
user: login: Bromeon
body: Thanks a lot, this is very cool!

From the title I was first worried this might cause many conflicts with #1000, but it seems like it's mostly orthogonal, which is nice :slightly_smiling_face: 

I have only seen the first 1-2 files, will review more at a later point. Is there maybe an example, or should we just check tests?
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-10T11:29:53Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-10T20:29:53Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-10T20:30:13Z
user: login: Bromeon
body: I've finally had some time to look more closely at this. Thanks so much for this great PR, outstanding work as always :heart: 

> Technically, we could unify the test execution of sync and async tasks, but I get the impression that it also would have some downsides. Keeping it separate adds a bit of duplication, but unifying it would force more complexity onto the execution of sync tasks.

I think you made the right choice here, it seems they're different enough to be treated differently. If it becomes bothersome in the future, we could always revise that decision; but I think keeping the sync tests simple is a good approach.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-15T12:00:17Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-15T12:11:50Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-15T12:17:47Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-15T12:25:27Z
user: login: Bromeon
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-15T12:52:18Z
user: login: Bromeon
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-15T12:54:40Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-15T13:56:19Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-15T14:21:14Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-15T14:24:34Z
user: login: Bromeon
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-15T18:26:31Z
user: login: Bromeon
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-15T18:29:38Z
user: login: Bromeon
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-15T18:33:57Z
user: login: Bromeon
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-15T18:38:23Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-15T20:14:24Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-16T19:49:35Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-16T19:56:31Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-16T20:00:23Z
user: login: Bromeon
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-16T20:02:01Z
user: login: Bromeon
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-16T20:04:11Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-16T22:41:46Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-16T22:43:17Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T22:40:58Z
user: login: Bromeon
body: Thanks again for the continuous effort. Another review iteration :slightly_smiling_face: 

Some organizational questions:
1. This PR adds a ton of functionality (1.2 kLOC and counting). I think at this point it makes sense to have a dedicated module instead of spilling all into `godot::builtin`.
    What about `godot::task`, similar to the previous [`gdnative::tasks`](https://docs.rs/gdnative/latest/gdnative/tasks/index.html)? We can always move it around if we find it fits better into another module, but that's much easier once it is its own module already.

2. `godot_task` should probably be called something that contains "spawn", just like with all other async runtimes and `std::thread`. Maybe even just `spawn`. Another reason for the module, it could then be qualified as `godot::task::spawn(...)`.
3. `TrySignalFuture` -- would it make sense to name it `FallibleSignalFuture` or so? A bit longer, but the type isn't named very often, and this seems to be the not-so-common case if I understood correctly.

state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-02T21:20:57Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-02T21:47:00Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-02T21:56:26Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-02T22:17:14Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-02T22:24:10Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-02T23:34:00Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-02T23:49:00Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-02T23:51:13Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-02T23:55:32Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-02T23:56:13Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T00:00:31Z
user: login: Bromeon
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-03T09:29:39Z
user: login: Bromeon
body: > I was thinking about `FallibleSignalFuture` as well, but it seemed a bit long. I'm fine with this name too.

Most of the time, people wouldn't name it directly, right? It may still appear as IDE hint on declarations, but even then, people may often directly `.await` on it...

I fixed the CI issues btw, the Godot-stable artifacts expired.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-03T13:17:13Z
user: login: Yarwin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T18:48:27Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T18:53:35Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T21:07:49Z
user: login: Bromeon
body: Thanks a lot! I also just checked the docs, the `task` module looks super clean!

Added some more comments, but really just small things this time. 

This starts to look very good -- if no other concerns are brought up, I'd suggest merging it on the weekend :rocket: 
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T22:57:07Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T23:01:44Z
user: login: Bromeon
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T23:03:19Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T23:09:30Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T23:10:14Z
user: login: Bromeon
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T13:15:38Z
user: login: TitanNano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T22:55:24Z
user: login: Bromeon
body: Thanks so much for this massive feature, @TitanNano!

The current implementation seems solid enough to be merged. Hopefully this allows more people to test it out -- thanks also to  everyone who has voiced their concerns in this thread, I encourage you to open new discussions where appropriate :slightly_smiling_face: regarding the `Gd` issue, I definitely think this should be discussed, but such changes can happen in follow-up PRs.

:rocket: 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-11T21:41:58Z

Review comments:
diff_hunk: @@ -0,0 +1,304 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::classes::Os;
+use crate::meta::ToGodot;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    let os = Os::singleton();
+
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    if os.get_thread_caller_id() != os.get_main_thread_id() {
+        panic!("godot_task can only be used on the main thread!");
+    }
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: We typically use Rust's thread APIs for this, see e.g. [here](https://github.com/search?q=repo%3Agodot-rust%2Fgdext+thread%3A%3Acurrent&type=code). 
created_at: 2025-02-10T11:25:54Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,304 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::classes::Os;
+use crate::meta::ToGodot;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    let os = Os::singleton();
+
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    if os.get_thread_caller_id() != os.get_main_thread_id() {
+        panic!("godot_task can only be used on the main thread!");
+    }
+
+    let (task_handle, waker): (_, Waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ))
+        .into();
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: What does this `into()` do? Can you write an explicit conversion instead (`TargetType::from`), maybe in a new expression?
created_at: 2025-02-10T11:26:58Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,304 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::classes::Os;
+use crate::meta::ToGodot;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    let os = Os::singleton();
+
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    if os.get_thread_caller_id() != os.get_main_thread_id() {
+        panic!("godot_task can only be used on the main thread!");
+    }
+
+    let (task_handle, waker): (_, Waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ))
+        .into();
+
+        (task_handle, waker)
+    });
+
+    waker.wake();
+    task_handle
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! { pub(crate) static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new()); }
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: ```suggestion
thread_local! {
    pub(crate) static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
}
```
created_at: 2025-02-10T11:27:26Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,304 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::classes::Os;
+use crate::meta::ToGodot;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    let os = Os::singleton();
+
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    if os.get_thread_caller_id() != os.get_main_thread_id() {
+        panic!("godot_task can only be used on the main thread!");
+    }
+
+    let (task_handle, waker): (_, Waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ))
+        .into();
+
+        (task_handle, waker)
+    });
+
+    waker.wake();
+    task_handle
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! { pub(crate) static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new()); }
+
+#[derive(Default)]
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// slot contains a future which is currently being polled.
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: ```suggestion
    /// Slot contains a future which is currently being polled.
```
created_at: 2025-02-10T11:27:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,304 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::classes::Os;
+use crate::meta::ToGodot;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    let os = Os::singleton();
+
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    if os.get_thread_caller_id() != os.get_main_thread_id() {
+        panic!("godot_task can only be used on the main thread!");
+    }
+
+    let (task_handle, waker): (_, Waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ))
+        .into();
path: godot-core/src/builtin/async_runtime.rs
user: login: TitanNano
body: I made it explicit.
created_at: 2025-02-10T20:29:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,304 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::classes::Os;
+use crate::meta::ToGodot;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    let os = Os::singleton();
+
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    if os.get_thread_caller_id() != os.get_main_thread_id() {
+        panic!("godot_task can only be used on the main thread!");
+    }
path: godot-core/src/builtin/async_runtime.rs
user: login: TitanNano
body: I can do that, but it needs #1045 
created_at: 2025-02-10T20:30:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,308 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::classes::Os;
+use crate::meta::ToGodot;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    let os = Os::singleton();
+
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    if os.get_thread_caller_id() != os.get_main_thread_id() {
+        panic!("godot_task can only be used on the main thread!");
+    }
+
+    let (task_handle, waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, Waker::from(godot_waker))
+    });
+
+    waker.wake();
+    task_handle
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    pub(crate) static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+#[derive(Default)]
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Empty;
+    }
+
+    fn cancel(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    fn take(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
+
+    fn park(&mut self, value: T) {
+        match self.value {
+            FutureSlotState::Empty | FutureSlotState::Gone => {
+                panic!("Future slot is currently unoccupied, future can not be parked here!");
+            }
+            FutureSlotState::Pending(_) => {
+                panic!("Future slot is already occupied by a different future!")
+            }
+            FutureSlotState::Polling => {
+                self.value = FutureSlotState::Pending(value);
+            }
+        }
+    }
+}
+
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty | FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.cancel_task(self.index);
+        })
+    }
+
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+#[derive(Default)]
+pub(crate) struct AsyncRuntime {
+    tasks: Vec<FutureSlot<Pin<Box<dyn Future<Output = ()>>>>>,
+    task_counter: u64,
+}
+
+impl AsyncRuntime {
+    fn new() -> Self {
+        Self {
+            tasks: Vec::with_capacity(10),
+            task_counter: 0,
+        }
+    }
+
+    fn next_id(&mut self) -> u64 {
+        let id = self.task_counter;
+        self.task_counter += 1;
+        id
+    }
+
+    fn add_task<F: Future<Output = ()> + 'static>(&mut self, future: F) -> TaskHandle {
+        let id = self.next_id();
+        let slot = self
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: Should maybe be named `index_slot`.
created_at: 2025-02-15T11:10:22Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,308 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::classes::Os;
+use crate::meta::ToGodot;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    let os = Os::singleton();
+
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    if os.get_thread_caller_id() != os.get_main_thread_id() {
+        panic!("godot_task can only be used on the main thread!");
+    }
+
+    let (task_handle, waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, Waker::from(godot_waker))
+    });
+
+    waker.wake();
+    task_handle
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    pub(crate) static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+#[derive(Default)]
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Empty;
+    }
+
+    fn cancel(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    fn take(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
+
+    fn park(&mut self, value: T) {
+        match self.value {
+            FutureSlotState::Empty | FutureSlotState::Gone => {
+                panic!("Future slot is currently unoccupied, future can not be parked here!");
+            }
+            FutureSlotState::Pending(_) => {
+                panic!("Future slot is already occupied by a different future!")
+            }
+            FutureSlotState::Polling => {
+                self.value = FutureSlotState::Pending(value);
+            }
+        }
+    }
+}
+
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty | FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.cancel_task(self.index);
+        })
+    }
+
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+#[derive(Default)]
+pub(crate) struct AsyncRuntime {
+    tasks: Vec<FutureSlot<Pin<Box<dyn Future<Output = ()>>>>>,
+    task_counter: u64,
+}
+
+impl AsyncRuntime {
+    fn new() -> Self {
+        Self {
+            tasks: Vec::with_capacity(10),
+            task_counter: 0,
+        }
+    }
+
+    fn next_id(&mut self) -> u64 {
+        let id = self.task_counter;
+        self.task_counter += 1;
+        id
+    }
+
+    fn add_task<F: Future<Output = ()> + 'static>(&mut self, future: F) -> TaskHandle {
+        let id = self.next_id();
+        let slot = self
+            .tasks
+            .iter_mut()
+            .enumerate()
+            .find(|(_, slot)| slot.is_empty());
+
+        let boxed = Box::pin(future);
+
+        let index = match slot {
+            Some((index, slot)) => {
+                *slot = FutureSlot::pending(id, boxed);
+                index
+            }
+            None => {
+                self.tasks.push(FutureSlot::pending(id, boxed));
+                self.tasks.len() - 1
+            }
+        };
+
+        TaskHandle::new(index, id)
+    }
+
+    fn get_task(
+        &mut self,
+        index: usize,
+        id: u64,
+    ) -> FutureSlotState<Pin<Box<dyn Future<Output = ()> + 'static>>> {
+        let slot = self.tasks.get_mut(index);
+
+        slot.map(|inner| inner.take(id)).unwrap_or_default()
+    }
+
+    fn clear_task(&mut self, index: usize) {
+        self.tasks[index].clear();
+    }
+
+    fn cancel_task(&mut self, index: usize) {
+        self.tasks[index].cancel();
+    }
+
+    fn park_task(&mut self, index: usize, future: Pin<Box<dyn Future<Output = ()>>>) {
+        self.tasks[index].park(future);
+    }
+}
+
+struct GodotWaker {
+    runtime_index: usize,
+    task_id: u64,
+    thread_id: ThreadId,
+}
+
+impl GodotWaker {
+    fn new(index: usize, task_id: u64, thread_id: ThreadId) -> Self {
+        Self {
+            runtime_index: index,
+            thread_id,
+            task_id,
+        }
+    }
+}
+
+impl Wake for GodotWaker {
+    fn wake(self: std::sync::Arc<Self>) {
+        let callable = Callable::from_local_fn("GodotWaker::wake", move |_args| {
+            let current_thread = thread::current().id();
+
+            if self.thread_id != current_thread {
+                panic!("trying to poll future on a different thread!\nCurrent Thread: {:?}, Future Thread: {:?}", current_thread, self.thread_id);
+            }
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: Could use `assert_ne!` here.

Also, maybe the string could be on multiple new lines, indented with 2 spaces and aligned:
```rs
"trying to poll future on a different thread!\n  Current thread: {:?}\n  Future thread:  {:?}"
```
created_at: 2025-02-15T11:13:53Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,308 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::classes::Os;
+use crate::meta::ToGodot;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    let os = Os::singleton();
+
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    if os.get_thread_caller_id() != os.get_main_thread_id() {
+        panic!("godot_task can only be used on the main thread!");
+    }
+
+    let (task_handle, waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, Waker::from(godot_waker))
+    });
+
+    waker.wake();
+    task_handle
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    pub(crate) static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+#[derive(Default)]
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Empty;
+    }
+
+    fn cancel(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    fn take(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
+
+    fn park(&mut self, value: T) {
+        match self.value {
+            FutureSlotState::Empty | FutureSlotState::Gone => {
+                panic!("Future slot is currently unoccupied, future can not be parked here!");
+            }
+            FutureSlotState::Pending(_) => {
+                panic!("Future slot is already occupied by a different future!")
+            }
+            FutureSlotState::Polling => {
+                self.value = FutureSlotState::Pending(value);
+            }
+        }
+    }
+}
+
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty | FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.cancel_task(self.index);
+        })
+    }
+
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+#[derive(Default)]
+pub(crate) struct AsyncRuntime {
+    tasks: Vec<FutureSlot<Pin<Box<dyn Future<Output = ()>>>>>,
+    task_counter: u64,
+}
+
+impl AsyncRuntime {
+    fn new() -> Self {
+        Self {
+            tasks: Vec::with_capacity(10),
+            task_counter: 0,
+        }
+    }
+
+    fn next_id(&mut self) -> u64 {
+        let id = self.task_counter;
+        self.task_counter += 1;
+        id
+    }
+
+    fn add_task<F: Future<Output = ()> + 'static>(&mut self, future: F) -> TaskHandle {
+        let id = self.next_id();
+        let slot = self
+            .tasks
+            .iter_mut()
+            .enumerate()
+            .find(|(_, slot)| slot.is_empty());
+
+        let boxed = Box::pin(future);
+
+        let index = match slot {
+            Some((index, slot)) => {
+                *slot = FutureSlot::pending(id, boxed);
+                index
+            }
+            None => {
+                self.tasks.push(FutureSlot::pending(id, boxed));
+                self.tasks.len() - 1
+            }
+        };
+
+        TaskHandle::new(index, id)
+    }
+
+    fn get_task(
+        &mut self,
+        index: usize,
+        id: u64,
+    ) -> FutureSlotState<Pin<Box<dyn Future<Output = ()> + 'static>>> {
+        let slot = self.tasks.get_mut(index);
+
+        slot.map(|inner| inner.take(id)).unwrap_or_default()
+    }
+
+    fn clear_task(&mut self, index: usize) {
+        self.tasks[index].clear();
+    }
+
+    fn cancel_task(&mut self, index: usize) {
+        self.tasks[index].cancel();
+    }
+
+    fn park_task(&mut self, index: usize, future: Pin<Box<dyn Future<Output = ()>>>) {
+        self.tasks[index].park(future);
+    }
+}
+
+struct GodotWaker {
+    runtime_index: usize,
+    task_id: u64,
+    thread_id: ThreadId,
+}
+
+impl GodotWaker {
+    fn new(index: usize, task_id: u64, thread_id: ThreadId) -> Self {
+        Self {
+            runtime_index: index,
+            thread_id,
+            task_id,
+        }
+    }
+}
+
+impl Wake for GodotWaker {
+    fn wake(self: std::sync::Arc<Self>) {
+        let callable = Callable::from_local_fn("GodotWaker::wake", move |_args| {
+            let current_thread = thread::current().id();
+
+            if self.thread_id != current_thread {
+                panic!("trying to poll future on a different thread!\nCurrent Thread: {:?}, Future Thread: {:?}", current_thread, self.thread_id);
+            }
+
+            let waker = Waker::from(self.clone());
+            let mut ctx = Context::from_waker(&waker);
+
+            // take future out of the runtime.
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: ```suggestion
            // Take future out of the runtime.
```
created_at: 2025-02-15T11:14:14Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,308 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::classes::Os;
+use crate::meta::ToGodot;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    let os = Os::singleton();
+
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    if os.get_thread_caller_id() != os.get_main_thread_id() {
+        panic!("godot_task can only be used on the main thread!");
+    }
+
+    let (task_handle, waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, Waker::from(godot_waker))
+    });
+
+    waker.wake();
+    task_handle
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    pub(crate) static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+#[derive(Default)]
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Empty;
+    }
+
+    fn cancel(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    fn take(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
+
+    fn park(&mut self, value: T) {
+        match self.value {
+            FutureSlotState::Empty | FutureSlotState::Gone => {
+                panic!("Future slot is currently unoccupied, future can not be parked here!");
+            }
+            FutureSlotState::Pending(_) => {
+                panic!("Future slot is already occupied by a different future!")
+            }
+            FutureSlotState::Polling => {
+                self.value = FutureSlotState::Pending(value);
+            }
+        }
+    }
+}
+
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty | FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.cancel_task(self.index);
+        })
+    }
+
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+#[derive(Default)]
+pub(crate) struct AsyncRuntime {
+    tasks: Vec<FutureSlot<Pin<Box<dyn Future<Output = ()>>>>>,
+    task_counter: u64,
+}
+
+impl AsyncRuntime {
+    fn new() -> Self {
+        Self {
+            tasks: Vec::with_capacity(10),
+            task_counter: 0,
+        }
+    }
+
+    fn next_id(&mut self) -> u64 {
+        let id = self.task_counter;
+        self.task_counter += 1;
+        id
+    }
+
+    fn add_task<F: Future<Output = ()> + 'static>(&mut self, future: F) -> TaskHandle {
+        let id = self.next_id();
+        let slot = self
+            .tasks
+            .iter_mut()
+            .enumerate()
+            .find(|(_, slot)| slot.is_empty());
+
+        let boxed = Box::pin(future);
+
+        let index = match slot {
+            Some((index, slot)) => {
+                *slot = FutureSlot::pending(id, boxed);
+                index
+            }
+            None => {
+                self.tasks.push(FutureSlot::pending(id, boxed));
+                self.tasks.len() - 1
+            }
+        };
+
+        TaskHandle::new(index, id)
+    }
+
+    fn get_task(
+        &mut self,
+        index: usize,
+        id: u64,
+    ) -> FutureSlotState<Pin<Box<dyn Future<Output = ()> + 'static>>> {
+        let slot = self.tasks.get_mut(index);
+
+        slot.map(|inner| inner.take(id)).unwrap_or_default()
+    }
+
+    fn clear_task(&mut self, index: usize) {
+        self.tasks[index].clear();
+    }
+
+    fn cancel_task(&mut self, index: usize) {
+        self.tasks[index].cancel();
+    }
+
+    fn park_task(&mut self, index: usize, future: Pin<Box<dyn Future<Output = ()>>>) {
+        self.tasks[index].park(future);
+    }
+}
+
+struct GodotWaker {
+    runtime_index: usize,
+    task_id: u64,
+    thread_id: ThreadId,
+}
+
+impl GodotWaker {
+    fn new(index: usize, task_id: u64, thread_id: ThreadId) -> Self {
+        Self {
+            runtime_index: index,
+            thread_id,
+            task_id,
+        }
+    }
+}
+
+impl Wake for GodotWaker {
+    fn wake(self: std::sync::Arc<Self>) {
+        let callable = Callable::from_local_fn("GodotWaker::wake", move |_args| {
+            let current_thread = thread::current().id();
+
+            if self.thread_id != current_thread {
+                panic!("trying to poll future on a different thread!\nCurrent Thread: {:?}, Future Thread: {:?}", current_thread, self.thread_id);
+            }
+
+            let waker = Waker::from(self.clone());
+            let mut ctx = Context::from_waker(&waker);
+
+            // take future out of the runtime.
+            let future = ASYNC_RUNTIME.with_borrow_mut(|rt| {
+                match rt.get_task(self.runtime_index, self.task_id) {
+                    FutureSlotState::Empty => {
+                        panic!("Future no longer exists when waking it! This is a bug!");
+                    },
+
+                    FutureSlotState::Gone => {
+                        None
+                    }
+
+                    FutureSlotState::Polling => {
+                        panic!("The same GodotWaker has been called recursively, this is not expected!");
+                    }
+
+                    FutureSlotState::Pending(future) => Some(future),
+                }
+            });
+
+            let Some(mut future) = future else {
+                // future has been canceled while the waker was already triggered.
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: ```suggestion
                // Future has been canceled while the waker was already triggered.
```
created_at: 2025-02-15T11:14:22Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,308 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::classes::Os;
+use crate::meta::ToGodot;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    let os = Os::singleton();
+
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    if os.get_thread_caller_id() != os.get_main_thread_id() {
+        panic!("godot_task can only be used on the main thread!");
+    }
+
+    let (task_handle, waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, Waker::from(godot_waker))
+    });
+
+    waker.wake();
+    task_handle
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    pub(crate) static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+#[derive(Default)]
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Empty;
+    }
+
+    fn cancel(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    fn take(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
+
+    fn park(&mut self, value: T) {
+        match self.value {
+            FutureSlotState::Empty | FutureSlotState::Gone => {
+                panic!("Future slot is currently unoccupied, future can not be parked here!");
+            }
+            FutureSlotState::Pending(_) => {
+                panic!("Future slot is already occupied by a different future!")
+            }
+            FutureSlotState::Polling => {
+                self.value = FutureSlotState::Pending(value);
+            }
+        }
+    }
+}
+
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty | FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.cancel_task(self.index);
+        })
+    }
+
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+#[derive(Default)]
+pub(crate) struct AsyncRuntime {
+    tasks: Vec<FutureSlot<Pin<Box<dyn Future<Output = ()>>>>>,
+    task_counter: u64,
+}
+
+impl AsyncRuntime {
+    fn new() -> Self {
+        Self {
+            tasks: Vec::with_capacity(10),
+            task_counter: 0,
+        }
+    }
+
+    fn next_id(&mut self) -> u64 {
+        let id = self.task_counter;
+        self.task_counter += 1;
+        id
+    }
+
+    fn add_task<F: Future<Output = ()> + 'static>(&mut self, future: F) -> TaskHandle {
+        let id = self.next_id();
+        let slot = self
+            .tasks
+            .iter_mut()
+            .enumerate()
+            .find(|(_, slot)| slot.is_empty());
+
+        let boxed = Box::pin(future);
+
+        let index = match slot {
+            Some((index, slot)) => {
+                *slot = FutureSlot::pending(id, boxed);
+                index
+            }
+            None => {
+                self.tasks.push(FutureSlot::pending(id, boxed));
+                self.tasks.len() - 1
+            }
+        };
+
+        TaskHandle::new(index, id)
+    }
+
+    fn get_task(
+        &mut self,
+        index: usize,
+        id: u64,
+    ) -> FutureSlotState<Pin<Box<dyn Future<Output = ()> + 'static>>> {
+        let slot = self.tasks.get_mut(index);
+
+        slot.map(|inner| inner.take(id)).unwrap_or_default()
+    }
+
+    fn clear_task(&mut self, index: usize) {
+        self.tasks[index].clear();
+    }
+
+    fn cancel_task(&mut self, index: usize) {
+        self.tasks[index].cancel();
+    }
+
+    fn park_task(&mut self, index: usize, future: Pin<Box<dyn Future<Output = ()>>>) {
+        self.tasks[index].park(future);
+    }
+}
+
+struct GodotWaker {
+    runtime_index: usize,
+    task_id: u64,
+    thread_id: ThreadId,
+}
+
+impl GodotWaker {
+    fn new(index: usize, task_id: u64, thread_id: ThreadId) -> Self {
+        Self {
+            runtime_index: index,
+            thread_id,
+            task_id,
+        }
+    }
+}
+
+impl Wake for GodotWaker {
+    fn wake(self: std::sync::Arc<Self>) {
+        let callable = Callable::from_local_fn("GodotWaker::wake", move |_args| {
+            let current_thread = thread::current().id();
+
+            if self.thread_id != current_thread {
+                panic!("trying to poll future on a different thread!\nCurrent Thread: {:?}, Future Thread: {:?}", current_thread, self.thread_id);
+            }
+
+            let waker = Waker::from(self.clone());
+            let mut ctx = Context::from_waker(&waker);
+
+            // take future out of the runtime.
+            let future = ASYNC_RUNTIME.with_borrow_mut(|rt| {
+                match rt.get_task(self.runtime_index, self.task_id) {
+                    FutureSlotState::Empty => {
+                        panic!("Future no longer exists when waking it! This is a bug!");
+                    },
+
+                    FutureSlotState::Gone => {
+                        None
+                    }
+
+                    FutureSlotState::Polling => {
+                        panic!("The same GodotWaker has been called recursively, this is not expected!");
+                    }
+
+                    FutureSlotState::Pending(future) => Some(future),
+                }
+            });
+
+            let Some(mut future) = future else {
+                // future has been canceled while the waker was already triggered.
+                return Ok(Variant::nil());
+            };
+
+            let result = future.as_mut().poll(&mut ctx);
+
+            // update runtime.
+            ASYNC_RUNTIME.with_borrow_mut(|rt| match result {
+                Poll::Pending => rt.park_task(self.runtime_index, future),
+                Poll::Ready(()) => rt.clear_task(self.runtime_index),
+            });
+
+            Ok(Variant::nil())
+        });
+
+        // shedule waker to poll the future on the end of the frame.
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: ```suggestion
        // Schedule waker to poll the future at the end of the frame.
```
created_at: 2025-02-15T11:14:38Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,308 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::classes::Os;
+use crate::meta::ToGodot;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    let os = Os::singleton();
+
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    if os.get_thread_caller_id() != os.get_main_thread_id() {
+        panic!("godot_task can only be used on the main thread!");
+    }
+
+    let (task_handle, waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, Waker::from(godot_waker))
+    });
+
+    waker.wake();
+    task_handle
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    pub(crate) static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+#[derive(Default)]
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Empty;
+    }
+
+    fn cancel(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    fn take(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
+
+    fn park(&mut self, value: T) {
+        match self.value {
+            FutureSlotState::Empty | FutureSlotState::Gone => {
+                panic!("Future slot is currently unoccupied, future can not be parked here!");
+            }
+            FutureSlotState::Pending(_) => {
+                panic!("Future slot is already occupied by a different future!")
+            }
+            FutureSlotState::Polling => {
+                self.value = FutureSlotState::Pending(value);
+            }
+        }
+    }
+}
+
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty | FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.cancel_task(self.index);
+        })
+    }
+
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+#[derive(Default)]
+pub(crate) struct AsyncRuntime {
+    tasks: Vec<FutureSlot<Pin<Box<dyn Future<Output = ()>>>>>,
+    task_counter: u64,
+}
+
+impl AsyncRuntime {
+    fn new() -> Self {
+        Self {
+            tasks: Vec::with_capacity(10),
+            task_counter: 0,
+        }
+    }
+
+    fn next_id(&mut self) -> u64 {
+        let id = self.task_counter;
+        self.task_counter += 1;
+        id
+    }
+
+    fn add_task<F: Future<Output = ()> + 'static>(&mut self, future: F) -> TaskHandle {
+        let id = self.next_id();
+        let slot = self
+            .tasks
+            .iter_mut()
+            .enumerate()
+            .find(|(_, slot)| slot.is_empty());
+
+        let boxed = Box::pin(future);
+
+        let index = match slot {
+            Some((index, slot)) => {
+                *slot = FutureSlot::pending(id, boxed);
+                index
+            }
+            None => {
+                self.tasks.push(FutureSlot::pending(id, boxed));
+                self.tasks.len() - 1
+            }
+        };
+
+        TaskHandle::new(index, id)
+    }
+
+    fn get_task(
+        &mut self,
+        index: usize,
+        id: u64,
+    ) -> FutureSlotState<Pin<Box<dyn Future<Output = ()> + 'static>>> {
+        let slot = self.tasks.get_mut(index);
+
+        slot.map(|inner| inner.take(id)).unwrap_or_default()
+    }
+
+    fn clear_task(&mut self, index: usize) {
+        self.tasks[index].clear();
+    }
+
+    fn cancel_task(&mut self, index: usize) {
+        self.tasks[index].cancel();
+    }
+
+    fn park_task(&mut self, index: usize, future: Pin<Box<dyn Future<Output = ()>>>) {
+        self.tasks[index].park(future);
+    }
+}
+
+struct GodotWaker {
+    runtime_index: usize,
+    task_id: u64,
+    thread_id: ThreadId,
+}
+
+impl GodotWaker {
+    fn new(index: usize, task_id: u64, thread_id: ThreadId) -> Self {
+        Self {
+            runtime_index: index,
+            thread_id,
+            task_id,
+        }
+    }
+}
+
+impl Wake for GodotWaker {
+    fn wake(self: std::sync::Arc<Self>) {
+        let callable = Callable::from_local_fn("GodotWaker::wake", move |_args| {
+            let current_thread = thread::current().id();
+
+            if self.thread_id != current_thread {
+                panic!("trying to poll future on a different thread!\nCurrent Thread: {:?}, Future Thread: {:?}", current_thread, self.thread_id);
+            }
+
+            let waker = Waker::from(self.clone());
+            let mut ctx = Context::from_waker(&waker);
+
+            // take future out of the runtime.
+            let future = ASYNC_RUNTIME.with_borrow_mut(|rt| {
+                match rt.get_task(self.runtime_index, self.task_id) {
+                    FutureSlotState::Empty => {
+                        panic!("Future no longer exists when waking it! This is a bug!");
+                    },
+
+                    FutureSlotState::Gone => {
+                        None
+                    }
+
+                    FutureSlotState::Polling => {
+                        panic!("The same GodotWaker has been called recursively, this is not expected!");
+                    }
+
+                    FutureSlotState::Pending(future) => Some(future),
+                }
+            });
+
+            let Some(mut future) = future else {
+                // future has been canceled while the waker was already triggered.
+                return Ok(Variant::nil());
+            };
+
+            let result = future.as_mut().poll(&mut ctx);
+
+            // update runtime.
+            ASYNC_RUNTIME.with_borrow_mut(|rt| match result {
+                Poll::Pending => rt.park_task(self.runtime_index, future),
+                Poll::Ready(()) => rt.clear_task(self.runtime_index),
+            });
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: Could you maybe make the comment slightly more descriptive, what exactly is updated?

(also uppercase)
created_at: 2025-02-15T11:15:07Z
author_association: MEMBER
diff_hunk: @@ -213,3 +216,310 @@ impl fmt::Display for Signal {
         write!(f, "{}", self.to_variant())
     }
 }
+
+// ----------------------------------------------------------------------------------------------------------------------------------------
+// Implementation of a rust future for Godot Signals
+#[cfg(since_api = "4.2")]
path: godot-core/src/builtin/signal.rs
user: login: Bromeon
body: Uppercase "Rust" + empty line after:
```suggestion
// ----------------------------------------------------------------------------------------------------------------------------------------
// Implementation of a Rust future for Godot signals

#[cfg(since_api = "4.2")]
```

(might not be needed with the next change)
created_at: 2025-02-15T11:15:39Z
author_association: MEMBER
diff_hunk: @@ -213,3 +216,310 @@ impl fmt::Display for Signal {
         write!(f, "{}", self.to_variant())
     }
 }
+
+// ----------------------------------------------------------------------------------------------------------------------------------------
+// Implementation of a rust future for Godot Signals
+#[cfg(since_api = "4.2")]
+mod futures {
+    use std::fmt::Display;
+    use std::future::Future;
path: godot-core/src/builtin/signal.rs
user: login: Bromeon
body: This could probably be extracted to its own file. The `#[cfg]` can then be placed on the import statement.
created_at: 2025-02-15T11:17:20Z
author_association: MEMBER
diff_hunk: @@ -213,3 +216,310 @@ impl fmt::Display for Signal {
         write!(f, "{}", self.to_variant())
     }
 }
+
+// ----------------------------------------------------------------------------------------------------------------------------------------
+// Implementation of a rust future for Godot Signals
+#[cfg(since_api = "4.2")]
+mod futures {
+    use std::fmt::Display;
+    use std::future::Future;
+    use std::pin::Pin;
+    use std::sync::{Arc, Mutex};
+    use std::task::{Context, Poll, Waker};
+
+    use crate::builtin::{Callable, RustCallable, Variant};
+    use crate::classes::object::ConnectFlags;
+    use crate::meta::FromGodot;
+    use crate::obj::EngineEnum;
+
+    use super::Signal;
+
+    pub struct SignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(Option<R>, Option<Waker>)>>,
+        callable: Callable,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> SignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new((None, Option::<Waker>::None)));
+            let callback_state = state.clone();
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = Callable::from_local_fn("async_task", move |args: &[&Variant]| {
+                let mut lock = callback_state.lock().unwrap();
+                let waker = lock.1.take();
+
+                lock.0.replace(R::from_args(args));
+                drop(lock);
+
+                if let Some(waker) = waker {
+                    waker.wake();
+                }
+
+                Ok(Variant::nil())
+            });
+
+            signal.connect(&callable, ConnectFlags::ONE_SHOT.ord() as i64);
+
+            Self {
+                state,
+                callable,
+                signal,
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Future for SignalFuture<R> {
+        type Output = R;
+
+        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+            let mut lock = self.state.lock().unwrap();
+
+            if let Some(result) = lock.0.take() {
+                return Poll::Ready(result);
+            }
+
+            lock.1.replace(cx.waker().clone());
+
+            Poll::Pending
+        }
+    }
+
+    impl<R: FromSignalArgs> Drop for SignalFuture<R> {
+        fn drop(&mut self) {
+            if !self.callable.is_valid() {
+                return;
+            }
+
+            if self.signal.object().is_none() {
+                return;
+            }
+
+            if self.signal.is_connected(&self.callable) {
+                self.signal.disconnect(&self.callable);
+            }
+        }
+    }
+
+    struct GuaranteedSignalFutureResolver<R> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+    }
+
+    impl<R> Clone for GuaranteedSignalFutureResolver<R> {
+        fn clone(&self) -> Self {
+            Self {
+                state: self.state.clone(),
+            }
+        }
+    }
+
+    impl<R> GuaranteedSignalFutureResolver<R> {
+        fn new(state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>) -> Self {
+            Self { state }
+        }
+    }
+
+    impl<R> std::hash::Hash for GuaranteedSignalFutureResolver<R> {
+        fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+            state.write_usize(Arc::as_ptr(&self.state) as usize);
+        }
+    }
+
+    impl<R> PartialEq for GuaranteedSignalFutureResolver<R> {
+        fn eq(&self, other: &Self) -> bool {
+            Arc::ptr_eq(&self.state, &other.state)
+        }
+    }
+
+    impl<R: FromSignalArgs> RustCallable for GuaranteedSignalFutureResolver<R> {
+        fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+            let mut lock = self.state.lock().unwrap();
+            let waker = lock.1.take();
+
+            lock.0 = GuaranteedSignalFutureState::Ready(R::from_args(args));
+            drop(lock);
+
+            if let Some(waker) = waker {
+                waker.wake();
+            }
+
+            Ok(Variant::nil())
+        }
+    }
+
+    impl<R> Display for GuaranteedSignalFutureResolver<R> {
+        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+            write!(
+                f,
+                "GuaranteedSignalFutureResolver::<{}>",
+                std::any::type_name::<R>()
+            )
+        }
+    }
+
+    // this resolver will resolve the future when it's being dropped (i.e. the engine removes all connected signal callables). This is very unusual.
+    impl<R> Drop for GuaranteedSignalFutureResolver<R> {
+        fn drop(&mut self) {
+            let mut lock = self.state.lock().unwrap();
+
+            if !matches!(lock.0, GuaranteedSignalFutureState::Pending) {
+                return;
+            }
+
+            lock.0 = GuaranteedSignalFutureState::Dead;
+
+            if let Some(ref waker) = lock.1 {
+                waker.wake_by_ref();
+            }
+        }
+    }
+
+    #[derive(Default)]
+    enum GuaranteedSignalFutureState<T> {
+        #[default]
+        Pending,
+        Ready(T),
+        Dead,
+        Dropped,
+    }
+
+    impl<T> GuaranteedSignalFutureState<T> {
+        fn take(&mut self) -> Self {
+            let new_value = match self {
+                Self::Pending => Self::Pending,
+                Self::Ready(_) | Self::Dead => Self::Dead,
+                Self::Dropped => Self::Dropped,
+            };
+
+            std::mem::replace(self, new_value)
+        }
+    }
+
+    /// The guaranteed signal future will always resolve, but might resolve to `None` if the owning object is freed
+    /// before the signal is emitted.
+    ///
+    /// This is inconsistent with how awaiting signals in Godot work and how async works in rust. The behavior was requested as part of some
+    /// user feedback for the initial POC.
+    pub struct GuaranteedSignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+        callable: GuaranteedSignalFutureResolver<R>,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> GuaranteedSignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new((
+                GuaranteedSignalFutureState::Pending,
+                Option::<Waker>::None,
+            )));
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = GuaranteedSignalFutureResolver::new(state.clone());
+
+            signal.connect(
+                &Callable::from_custom(callable.clone()),
+                ConnectFlags::ONE_SHOT.ord() as i64,
+            );
+
+            Self {
+                state,
+                callable,
+                signal,
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Future for GuaranteedSignalFuture<R> {
+        type Output = Option<R>;
+
+        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+            let mut lock = self.state.lock().unwrap();
+
+            lock.1.replace(cx.waker().clone());
+
+            let value = lock.0.take();
+
+            match value {
+                GuaranteedSignalFutureState::Pending => Poll::Pending,
+                GuaranteedSignalFutureState::Dropped => unreachable!(),
+                GuaranteedSignalFutureState::Dead => Poll::Ready(None),
+                GuaranteedSignalFutureState::Ready(value) => Poll::Ready(Some(value)),
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Drop for GuaranteedSignalFuture<R> {
+        fn drop(&mut self) {
+            if self.signal.object().is_none() {
+                return;
+            }
+
+            self.state.lock().unwrap().0 = GuaranteedSignalFutureState::Dropped;
+
+            let gd_callable = Callable::from_custom(self.callable.clone());
+
+            if self.signal.is_connected(&gd_callable) {
+                self.signal.disconnect(&gd_callable);
+            }
+        }
+    }
+
+    pub trait FromSignalArgs: Sync + Send + 'static {
+        fn from_args(args: &[&Variant]) -> Self;
+    }
+
+    impl<R: FromGodot + Sync + Send + 'static> FromSignalArgs for R {
+        fn from_args(args: &[&Variant]) -> Self {
+            args.first()
+                .map(|arg| (*arg).to_owned())
+                .unwrap_or_default()
+                .to()
+        }
+    }
+
+    // more of these should be generated via macro to support more than two signal arguments
+    impl<R1: FromGodot + Sync + Send + 'static, R2: FromGodot + Sync + Send + 'static>
+        FromSignalArgs for (R1, R2)
+    {
+        fn from_args(args: &[&Variant]) -> Self {
+            (args[0].to(), args[0].to())
+        }
+    }
path: godot-core/src/builtin/signal.rs
user: login: Bromeon
body: Note that I added a very similar trait here: https://github.com/godot-rust/gdext/pull/1000/files#diff-81650ce7b883570e6ace8688b6af757655b8dcfbb6841ca1decf96a12a49500cR52

Do you think that `ParamTuple::from_variant_array()` can be reused?

I can probably merge #1000 in the next few weeks, but we first need to release another v0.2 version.
created_at: 2025-02-15T11:19:40Z
author_association: MEMBER
diff_hunk: @@ -213,3 +216,310 @@ impl fmt::Display for Signal {
         write!(f, "{}", self.to_variant())
     }
 }
+
+// ----------------------------------------------------------------------------------------------------------------------------------------
+// Implementation of a rust future for Godot Signals
+#[cfg(since_api = "4.2")]
+mod futures {
+    use std::fmt::Display;
+    use std::future::Future;
+    use std::pin::Pin;
+    use std::sync::{Arc, Mutex};
+    use std::task::{Context, Poll, Waker};
+
+    use crate::builtin::{Callable, RustCallable, Variant};
+    use crate::classes::object::ConnectFlags;
+    use crate::meta::FromGodot;
+    use crate::obj::EngineEnum;
+
+    use super::Signal;
+
+    pub struct SignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(Option<R>, Option<Waker>)>>,
+        callable: Callable,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> SignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new((None, Option::<Waker>::None)));
+            let callback_state = state.clone();
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = Callable::from_local_fn("async_task", move |args: &[&Variant]| {
+                let mut lock = callback_state.lock().unwrap();
+                let waker = lock.1.take();
+
+                lock.0.replace(R::from_args(args));
+                drop(lock);
+
+                if let Some(waker) = waker {
+                    waker.wake();
+                }
+
+                Ok(Variant::nil())
+            });
+
+            signal.connect(&callable, ConnectFlags::ONE_SHOT.ord() as i64);
+
+            Self {
+                state,
+                callable,
+                signal,
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Future for SignalFuture<R> {
+        type Output = R;
+
+        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+            let mut lock = self.state.lock().unwrap();
+
+            if let Some(result) = lock.0.take() {
+                return Poll::Ready(result);
+            }
+
+            lock.1.replace(cx.waker().clone());
+
+            Poll::Pending
+        }
+    }
+
+    impl<R: FromSignalArgs> Drop for SignalFuture<R> {
+        fn drop(&mut self) {
+            if !self.callable.is_valid() {
+                return;
+            }
+
+            if self.signal.object().is_none() {
+                return;
+            }
+
+            if self.signal.is_connected(&self.callable) {
+                self.signal.disconnect(&self.callable);
+            }
+        }
+    }
+
+    struct GuaranteedSignalFutureResolver<R> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+    }
+
+    impl<R> Clone for GuaranteedSignalFutureResolver<R> {
+        fn clone(&self) -> Self {
+            Self {
+                state: self.state.clone(),
+            }
+        }
+    }
+
+    impl<R> GuaranteedSignalFutureResolver<R> {
+        fn new(state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>) -> Self {
+            Self { state }
+        }
+    }
+
+    impl<R> std::hash::Hash for GuaranteedSignalFutureResolver<R> {
+        fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+            state.write_usize(Arc::as_ptr(&self.state) as usize);
+        }
+    }
+
+    impl<R> PartialEq for GuaranteedSignalFutureResolver<R> {
+        fn eq(&self, other: &Self) -> bool {
+            Arc::ptr_eq(&self.state, &other.state)
+        }
+    }
+
+    impl<R: FromSignalArgs> RustCallable for GuaranteedSignalFutureResolver<R> {
+        fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+            let mut lock = self.state.lock().unwrap();
+            let waker = lock.1.take();
+
+            lock.0 = GuaranteedSignalFutureState::Ready(R::from_args(args));
+            drop(lock);
+
+            if let Some(waker) = waker {
+                waker.wake();
+            }
+
+            Ok(Variant::nil())
+        }
+    }
+
+    impl<R> Display for GuaranteedSignalFutureResolver<R> {
+        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+            write!(
+                f,
+                "GuaranteedSignalFutureResolver::<{}>",
+                std::any::type_name::<R>()
+            )
+        }
+    }
+
+    // this resolver will resolve the future when it's being dropped (i.e. the engine removes all connected signal callables). This is very unusual.
+    impl<R> Drop for GuaranteedSignalFutureResolver<R> {
+        fn drop(&mut self) {
+            let mut lock = self.state.lock().unwrap();
+
+            if !matches!(lock.0, GuaranteedSignalFutureState::Pending) {
+                return;
+            }
+
+            lock.0 = GuaranteedSignalFutureState::Dead;
+
+            if let Some(ref waker) = lock.1 {
+                waker.wake_by_ref();
+            }
+        }
+    }
+
+    #[derive(Default)]
+    enum GuaranteedSignalFutureState<T> {
+        #[default]
+        Pending,
+        Ready(T),
+        Dead,
+        Dropped,
+    }
+
+    impl<T> GuaranteedSignalFutureState<T> {
+        fn take(&mut self) -> Self {
+            let new_value = match self {
+                Self::Pending => Self::Pending,
+                Self::Ready(_) | Self::Dead => Self::Dead,
+                Self::Dropped => Self::Dropped,
+            };
+
+            std::mem::replace(self, new_value)
+        }
+    }
+
+    /// The guaranteed signal future will always resolve, but might resolve to `None` if the owning object is freed
+    /// before the signal is emitted.
+    ///
+    /// This is inconsistent with how awaiting signals in Godot work and how async works in rust. The behavior was requested as part of some
+    /// user feedback for the initial POC.
+    pub struct GuaranteedSignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+        callable: GuaranteedSignalFutureResolver<R>,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> GuaranteedSignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new((
+                GuaranteedSignalFutureState::Pending,
+                Option::<Waker>::None,
+            )));
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = GuaranteedSignalFutureResolver::new(state.clone());
+
+            signal.connect(
+                &Callable::from_custom(callable.clone()),
+                ConnectFlags::ONE_SHOT.ord() as i64,
+            );
+
+            Self {
+                state,
+                callable,
+                signal,
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Future for GuaranteedSignalFuture<R> {
+        type Output = Option<R>;
+
+        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+            let mut lock = self.state.lock().unwrap();
+
+            lock.1.replace(cx.waker().clone());
+
+            let value = lock.0.take();
+
+            match value {
+                GuaranteedSignalFutureState::Pending => Poll::Pending,
+                GuaranteedSignalFutureState::Dropped => unreachable!(),
+                GuaranteedSignalFutureState::Dead => Poll::Ready(None),
+                GuaranteedSignalFutureState::Ready(value) => Poll::Ready(Some(value)),
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Drop for GuaranteedSignalFuture<R> {
+        fn drop(&mut self) {
+            if self.signal.object().is_none() {
+                return;
+            }
+
+            self.state.lock().unwrap().0 = GuaranteedSignalFutureState::Dropped;
+
+            let gd_callable = Callable::from_custom(self.callable.clone());
+
+            if self.signal.is_connected(&gd_callable) {
+                self.signal.disconnect(&gd_callable);
+            }
+        }
+    }
+
+    pub trait FromSignalArgs: Sync + Send + 'static {
+        fn from_args(args: &[&Variant]) -> Self;
+    }
+
+    impl<R: FromGodot + Sync + Send + 'static> FromSignalArgs for R {
+        fn from_args(args: &[&Variant]) -> Self {
+            args.first()
+                .map(|arg| (*arg).to_owned())
+                .unwrap_or_default()
+                .to()
+        }
+    }
+
+    // more of these should be generated via macro to support more than two signal arguments
+    impl<R1: FromGodot + Sync + Send + 'static, R2: FromGodot + Sync + Send + 'static>
+        FromSignalArgs for (R1, R2)
+    {
+        fn from_args(args: &[&Variant]) -> Self {
+            (args[0].to(), args[0].to())
+        }
+    }
+
+    impl Signal {
+        pub fn to_guaranteed_future<R: FromSignalArgs>(&self) -> GuaranteedSignalFuture<R> {
+            GuaranteedSignalFuture::new(self.clone())
+        }
+
+        pub fn to_future<R: FromSignalArgs>(&self) -> SignalFuture<R> {
+            SignalFuture::new(self.clone())
+        }
+    }
+
+    #[cfg(test)]
+    mod tests {
+        use std::{
+            hash::{DefaultHasher, Hash, Hasher},
+            sync::Arc,
+        };
path: godot-core/src/builtin/signal.rs
user: login: Bromeon
body: Flat imports please :slightly_smiling_face: 
created_at: 2025-02-15T11:20:44Z
author_association: MEMBER
diff_hunk: @@ -213,3 +216,310 @@ impl fmt::Display for Signal {
         write!(f, "{}", self.to_variant())
     }
 }
+
+// ----------------------------------------------------------------------------------------------------------------------------------------
+// Implementation of a rust future for Godot Signals
+#[cfg(since_api = "4.2")]
+mod futures {
+    use std::fmt::Display;
+    use std::future::Future;
+    use std::pin::Pin;
+    use std::sync::{Arc, Mutex};
+    use std::task::{Context, Poll, Waker};
+
+    use crate::builtin::{Callable, RustCallable, Variant};
+    use crate::classes::object::ConnectFlags;
+    use crate::meta::FromGodot;
+    use crate::obj::EngineEnum;
+
+    use super::Signal;
+
+    pub struct SignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(Option<R>, Option<Waker>)>>,
+        callable: Callable,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> SignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new((None, Option::<Waker>::None)));
+            let callback_state = state.clone();
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = Callable::from_local_fn("async_task", move |args: &[&Variant]| {
+                let mut lock = callback_state.lock().unwrap();
+                let waker = lock.1.take();
+
+                lock.0.replace(R::from_args(args));
+                drop(lock);
+
+                if let Some(waker) = waker {
+                    waker.wake();
+                }
+
+                Ok(Variant::nil())
+            });
+
+            signal.connect(&callable, ConnectFlags::ONE_SHOT.ord() as i64);
+
+            Self {
+                state,
+                callable,
+                signal,
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Future for SignalFuture<R> {
+        type Output = R;
+
+        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+            let mut lock = self.state.lock().unwrap();
+
+            if let Some(result) = lock.0.take() {
+                return Poll::Ready(result);
+            }
+
+            lock.1.replace(cx.waker().clone());
+
+            Poll::Pending
+        }
+    }
+
+    impl<R: FromSignalArgs> Drop for SignalFuture<R> {
+        fn drop(&mut self) {
+            if !self.callable.is_valid() {
+                return;
+            }
+
+            if self.signal.object().is_none() {
+                return;
+            }
+
+            if self.signal.is_connected(&self.callable) {
+                self.signal.disconnect(&self.callable);
+            }
+        }
+    }
+
+    struct GuaranteedSignalFutureResolver<R> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+    }
+
+    impl<R> Clone for GuaranteedSignalFutureResolver<R> {
+        fn clone(&self) -> Self {
+            Self {
+                state: self.state.clone(),
+            }
+        }
+    }
+
+    impl<R> GuaranteedSignalFutureResolver<R> {
+        fn new(state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>) -> Self {
+            Self { state }
+        }
+    }
+
+    impl<R> std::hash::Hash for GuaranteedSignalFutureResolver<R> {
+        fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+            state.write_usize(Arc::as_ptr(&self.state) as usize);
+        }
+    }
+
+    impl<R> PartialEq for GuaranteedSignalFutureResolver<R> {
+        fn eq(&self, other: &Self) -> bool {
+            Arc::ptr_eq(&self.state, &other.state)
+        }
+    }
+
+    impl<R: FromSignalArgs> RustCallable for GuaranteedSignalFutureResolver<R> {
+        fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+            let mut lock = self.state.lock().unwrap();
+            let waker = lock.1.take();
+
+            lock.0 = GuaranteedSignalFutureState::Ready(R::from_args(args));
+            drop(lock);
+
+            if let Some(waker) = waker {
+                waker.wake();
+            }
+
+            Ok(Variant::nil())
+        }
+    }
+
+    impl<R> Display for GuaranteedSignalFutureResolver<R> {
+        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+            write!(
+                f,
+                "GuaranteedSignalFutureResolver::<{}>",
+                std::any::type_name::<R>()
+            )
+        }
+    }
+
+    // this resolver will resolve the future when it's being dropped (i.e. the engine removes all connected signal callables). This is very unusual.
+    impl<R> Drop for GuaranteedSignalFutureResolver<R> {
+        fn drop(&mut self) {
+            let mut lock = self.state.lock().unwrap();
+
+            if !matches!(lock.0, GuaranteedSignalFutureState::Pending) {
+                return;
+            }
+
+            lock.0 = GuaranteedSignalFutureState::Dead;
+
+            if let Some(ref waker) = lock.1 {
+                waker.wake_by_ref();
+            }
+        }
+    }
+
+    #[derive(Default)]
+    enum GuaranteedSignalFutureState<T> {
+        #[default]
+        Pending,
+        Ready(T),
+        Dead,
+        Dropped,
+    }
+
+    impl<T> GuaranteedSignalFutureState<T> {
+        fn take(&mut self) -> Self {
+            let new_value = match self {
+                Self::Pending => Self::Pending,
+                Self::Ready(_) | Self::Dead => Self::Dead,
+                Self::Dropped => Self::Dropped,
+            };
+
+            std::mem::replace(self, new_value)
+        }
+    }
+
+    /// The guaranteed signal future will always resolve, but might resolve to `None` if the owning object is freed
+    /// before the signal is emitted.
+    ///
+    /// This is inconsistent with how awaiting signals in Godot work and how async works in rust. The behavior was requested as part of some
+    /// user feedback for the initial POC.
+    pub struct GuaranteedSignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+        callable: GuaranteedSignalFutureResolver<R>,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> GuaranteedSignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new((
+                GuaranteedSignalFutureState::Pending,
+                Option::<Waker>::None,
+            )));
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = GuaranteedSignalFutureResolver::new(state.clone());
+
+            signal.connect(
+                &Callable::from_custom(callable.clone()),
+                ConnectFlags::ONE_SHOT.ord() as i64,
+            );
+
+            Self {
+                state,
+                callable,
+                signal,
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Future for GuaranteedSignalFuture<R> {
+        type Output = Option<R>;
+
+        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+            let mut lock = self.state.lock().unwrap();
+
+            lock.1.replace(cx.waker().clone());
+
+            let value = lock.0.take();
+
+            match value {
+                GuaranteedSignalFutureState::Pending => Poll::Pending,
+                GuaranteedSignalFutureState::Dropped => unreachable!(),
+                GuaranteedSignalFutureState::Dead => Poll::Ready(None),
+                GuaranteedSignalFutureState::Ready(value) => Poll::Ready(Some(value)),
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Drop for GuaranteedSignalFuture<R> {
+        fn drop(&mut self) {
+            if self.signal.object().is_none() {
+                return;
+            }
+
+            self.state.lock().unwrap().0 = GuaranteedSignalFutureState::Dropped;
+
+            let gd_callable = Callable::from_custom(self.callable.clone());
+
+            if self.signal.is_connected(&gd_callable) {
+                self.signal.disconnect(&gd_callable);
+            }
path: godot-core/src/builtin/signal.rs
user: login: Bromeon
body: I'm not 100% sure if the `RustCallable` equality is used here, and `is_connected()` would match two separate callables created this way.

It can work, but might be good to test? Also in case the `RustCallable` impl is ever changed.
created_at: 2025-02-15T11:22:10Z
author_association: MEMBER
diff_hunk: @@ -213,3 +216,310 @@ impl fmt::Display for Signal {
         write!(f, "{}", self.to_variant())
     }
 }
+
+// ----------------------------------------------------------------------------------------------------------------------------------------
+// Implementation of a rust future for Godot Signals
+#[cfg(since_api = "4.2")]
+mod futures {
+    use std::fmt::Display;
+    use std::future::Future;
+    use std::pin::Pin;
+    use std::sync::{Arc, Mutex};
+    use std::task::{Context, Poll, Waker};
+
+    use crate::builtin::{Callable, RustCallable, Variant};
+    use crate::classes::object::ConnectFlags;
+    use crate::meta::FromGodot;
+    use crate::obj::EngineEnum;
+
+    use super::Signal;
+
+    pub struct SignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(Option<R>, Option<Waker>)>>,
+        callable: Callable,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> SignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new((None, Option::<Waker>::None)));
+            let callback_state = state.clone();
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = Callable::from_local_fn("async_task", move |args: &[&Variant]| {
+                let mut lock = callback_state.lock().unwrap();
+                let waker = lock.1.take();
+
+                lock.0.replace(R::from_args(args));
+                drop(lock);
+
+                if let Some(waker) = waker {
+                    waker.wake();
+                }
+
+                Ok(Variant::nil())
+            });
+
+            signal.connect(&callable, ConnectFlags::ONE_SHOT.ord() as i64);
+
+            Self {
+                state,
+                callable,
+                signal,
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Future for SignalFuture<R> {
+        type Output = R;
+
+        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+            let mut lock = self.state.lock().unwrap();
+
+            if let Some(result) = lock.0.take() {
+                return Poll::Ready(result);
+            }
+
+            lock.1.replace(cx.waker().clone());
+
+            Poll::Pending
+        }
+    }
+
+    impl<R: FromSignalArgs> Drop for SignalFuture<R> {
+        fn drop(&mut self) {
+            if !self.callable.is_valid() {
+                return;
+            }
+
+            if self.signal.object().is_none() {
+                return;
+            }
+
+            if self.signal.is_connected(&self.callable) {
+                self.signal.disconnect(&self.callable);
+            }
+        }
+    }
+
+    struct GuaranteedSignalFutureResolver<R> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+    }
+
+    impl<R> Clone for GuaranteedSignalFutureResolver<R> {
+        fn clone(&self) -> Self {
+            Self {
+                state: self.state.clone(),
+            }
+        }
+    }
+
+    impl<R> GuaranteedSignalFutureResolver<R> {
+        fn new(state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>) -> Self {
+            Self { state }
+        }
+    }
+
+    impl<R> std::hash::Hash for GuaranteedSignalFutureResolver<R> {
+        fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+            state.write_usize(Arc::as_ptr(&self.state) as usize);
+        }
+    }
+
+    impl<R> PartialEq for GuaranteedSignalFutureResolver<R> {
+        fn eq(&self, other: &Self) -> bool {
+            Arc::ptr_eq(&self.state, &other.state)
+        }
+    }
+
+    impl<R: FromSignalArgs> RustCallable for GuaranteedSignalFutureResolver<R> {
+        fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+            let mut lock = self.state.lock().unwrap();
+            let waker = lock.1.take();
+
+            lock.0 = GuaranteedSignalFutureState::Ready(R::from_args(args));
+            drop(lock);
+
+            if let Some(waker) = waker {
+                waker.wake();
+            }
+
+            Ok(Variant::nil())
+        }
+    }
+
+    impl<R> Display for GuaranteedSignalFutureResolver<R> {
+        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+            write!(
+                f,
+                "GuaranteedSignalFutureResolver::<{}>",
+                std::any::type_name::<R>()
+            )
+        }
+    }
+
+    // this resolver will resolve the future when it's being dropped (i.e. the engine removes all connected signal callables). This is very unusual.
+    impl<R> Drop for GuaranteedSignalFutureResolver<R> {
+        fn drop(&mut self) {
+            let mut lock = self.state.lock().unwrap();
+
+            if !matches!(lock.0, GuaranteedSignalFutureState::Pending) {
+                return;
+            }
+
+            lock.0 = GuaranteedSignalFutureState::Dead;
+
+            if let Some(ref waker) = lock.1 {
+                waker.wake_by_ref();
+            }
+        }
+    }
+
+    #[derive(Default)]
+    enum GuaranteedSignalFutureState<T> {
+        #[default]
+        Pending,
+        Ready(T),
+        Dead,
+        Dropped,
+    }
+
+    impl<T> GuaranteedSignalFutureState<T> {
+        fn take(&mut self) -> Self {
+            let new_value = match self {
+                Self::Pending => Self::Pending,
+                Self::Ready(_) | Self::Dead => Self::Dead,
+                Self::Dropped => Self::Dropped,
+            };
+
+            std::mem::replace(self, new_value)
+        }
+    }
+
+    /// The guaranteed signal future will always resolve, but might resolve to `None` if the owning object is freed
+    /// before the signal is emitted.
+    ///
+    /// This is inconsistent with how awaiting signals in Godot work and how async works in rust. The behavior was requested as part of some
+    /// user feedback for the initial POC.
+    pub struct GuaranteedSignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+        callable: GuaranteedSignalFutureResolver<R>,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> GuaranteedSignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new((
+                GuaranteedSignalFutureState::Pending,
+                Option::<Waker>::None,
+            )));
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = GuaranteedSignalFutureResolver::new(state.clone());
+
+            signal.connect(
+                &Callable::from_custom(callable.clone()),
+                ConnectFlags::ONE_SHOT.ord() as i64,
+            );
+
+            Self {
+                state,
+                callable,
+                signal,
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Future for GuaranteedSignalFuture<R> {
+        type Output = Option<R>;
+
+        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+            let mut lock = self.state.lock().unwrap();
+
+            lock.1.replace(cx.waker().clone());
+
+            let value = lock.0.take();
+
+            match value {
+                GuaranteedSignalFutureState::Pending => Poll::Pending,
+                GuaranteedSignalFutureState::Dropped => unreachable!(),
+                GuaranteedSignalFutureState::Dead => Poll::Ready(None),
+                GuaranteedSignalFutureState::Ready(value) => Poll::Ready(Some(value)),
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Drop for GuaranteedSignalFuture<R> {
+        fn drop(&mut self) {
+            if self.signal.object().is_none() {
+                return;
+            }
path: godot-core/src/builtin/signal.rs
user: login: Bromeon
body: Could you add a brief comment in which situation the object can be null?
created_at: 2025-02-15T11:23:03Z
author_association: MEMBER
diff_hunk: @@ -213,3 +216,310 @@ impl fmt::Display for Signal {
         write!(f, "{}", self.to_variant())
     }
 }
+
+// ----------------------------------------------------------------------------------------------------------------------------------------
+// Implementation of a rust future for Godot Signals
+#[cfg(since_api = "4.2")]
+mod futures {
+    use std::fmt::Display;
+    use std::future::Future;
+    use std::pin::Pin;
+    use std::sync::{Arc, Mutex};
+    use std::task::{Context, Poll, Waker};
+
+    use crate::builtin::{Callable, RustCallable, Variant};
+    use crate::classes::object::ConnectFlags;
+    use crate::meta::FromGodot;
+    use crate::obj::EngineEnum;
+
+    use super::Signal;
+
+    pub struct SignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(Option<R>, Option<Waker>)>>,
+        callable: Callable,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> SignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new((None, Option::<Waker>::None)));
+            let callback_state = state.clone();
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = Callable::from_local_fn("async_task", move |args: &[&Variant]| {
+                let mut lock = callback_state.lock().unwrap();
+                let waker = lock.1.take();
+
+                lock.0.replace(R::from_args(args));
+                drop(lock);
+
+                if let Some(waker) = waker {
+                    waker.wake();
+                }
+
+                Ok(Variant::nil())
+            });
+
+            signal.connect(&callable, ConnectFlags::ONE_SHOT.ord() as i64);
+
+            Self {
+                state,
+                callable,
+                signal,
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Future for SignalFuture<R> {
+        type Output = R;
+
+        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+            let mut lock = self.state.lock().unwrap();
+
+            if let Some(result) = lock.0.take() {
+                return Poll::Ready(result);
+            }
+
+            lock.1.replace(cx.waker().clone());
+
+            Poll::Pending
+        }
+    }
+
+    impl<R: FromSignalArgs> Drop for SignalFuture<R> {
+        fn drop(&mut self) {
+            if !self.callable.is_valid() {
+                return;
+            }
+
+            if self.signal.object().is_none() {
+                return;
+            }
+
+            if self.signal.is_connected(&self.callable) {
+                self.signal.disconnect(&self.callable);
+            }
+        }
+    }
+
+    struct GuaranteedSignalFutureResolver<R> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+    }
+
+    impl<R> Clone for GuaranteedSignalFutureResolver<R> {
+        fn clone(&self) -> Self {
+            Self {
+                state: self.state.clone(),
+            }
+        }
+    }
+
+    impl<R> GuaranteedSignalFutureResolver<R> {
+        fn new(state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>) -> Self {
+            Self { state }
+        }
+    }
+
+    impl<R> std::hash::Hash for GuaranteedSignalFutureResolver<R> {
+        fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+            state.write_usize(Arc::as_ptr(&self.state) as usize);
+        }
+    }
+
+    impl<R> PartialEq for GuaranteedSignalFutureResolver<R> {
+        fn eq(&self, other: &Self) -> bool {
+            Arc::ptr_eq(&self.state, &other.state)
+        }
+    }
+
+    impl<R: FromSignalArgs> RustCallable for GuaranteedSignalFutureResolver<R> {
+        fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+            let mut lock = self.state.lock().unwrap();
+            let waker = lock.1.take();
+
+            lock.0 = GuaranteedSignalFutureState::Ready(R::from_args(args));
+            drop(lock);
+
+            if let Some(waker) = waker {
+                waker.wake();
+            }
+
+            Ok(Variant::nil())
+        }
+    }
+
+    impl<R> Display for GuaranteedSignalFutureResolver<R> {
+        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+            write!(
+                f,
+                "GuaranteedSignalFutureResolver::<{}>",
+                std::any::type_name::<R>()
+            )
+        }
+    }
+
+    // this resolver will resolve the future when it's being dropped (i.e. the engine removes all connected signal callables). This is very unusual.
+    impl<R> Drop for GuaranteedSignalFutureResolver<R> {
+        fn drop(&mut self) {
+            let mut lock = self.state.lock().unwrap();
+
+            if !matches!(lock.0, GuaranteedSignalFutureState::Pending) {
+                return;
+            }
+
+            lock.0 = GuaranteedSignalFutureState::Dead;
+
+            if let Some(ref waker) = lock.1 {
+                waker.wake_by_ref();
+            }
+        }
+    }
+
+    #[derive(Default)]
+    enum GuaranteedSignalFutureState<T> {
+        #[default]
+        Pending,
+        Ready(T),
+        Dead,
+        Dropped,
+    }
+
+    impl<T> GuaranteedSignalFutureState<T> {
+        fn take(&mut self) -> Self {
+            let new_value = match self {
+                Self::Pending => Self::Pending,
+                Self::Ready(_) | Self::Dead => Self::Dead,
+                Self::Dropped => Self::Dropped,
+            };
+
+            std::mem::replace(self, new_value)
+        }
+    }
+
+    /// The guaranteed signal future will always resolve, but might resolve to `None` if the owning object is freed
+    /// before the signal is emitted.
+    ///
+    /// This is inconsistent with how awaiting signals in Godot work and how async works in rust. The behavior was requested as part of some
+    /// user feedback for the initial POC.
+    pub struct GuaranteedSignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+        callable: GuaranteedSignalFutureResolver<R>,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> GuaranteedSignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new((
+                GuaranteedSignalFutureState::Pending,
+                Option::<Waker>::None,
+            )));
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = GuaranteedSignalFutureResolver::new(state.clone());
+
+            signal.connect(
+                &Callable::from_custom(callable.clone()),
+                ConnectFlags::ONE_SHOT.ord() as i64,
+            );
+
+            Self {
+                state,
+                callable,
+                signal,
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Future for GuaranteedSignalFuture<R> {
+        type Output = Option<R>;
+
+        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+            let mut lock = self.state.lock().unwrap();
+
+            lock.1.replace(cx.waker().clone());
+
+            let value = lock.0.take();
path: godot-core/src/builtin/signal.rs
user: login: Bromeon
body: Might be good to destructure into `(future_state, waker)` here.

`lock.1` and `lock.0` are very hard to identify.
created_at: 2025-02-15T11:24:04Z
author_association: MEMBER
diff_hunk: @@ -213,3 +216,310 @@ impl fmt::Display for Signal {
         write!(f, "{}", self.to_variant())
     }
 }
+
+// ----------------------------------------------------------------------------------------------------------------------------------------
+// Implementation of a rust future for Godot Signals
+#[cfg(since_api = "4.2")]
+mod futures {
+    use std::fmt::Display;
+    use std::future::Future;
+    use std::pin::Pin;
+    use std::sync::{Arc, Mutex};
+    use std::task::{Context, Poll, Waker};
+
+    use crate::builtin::{Callable, RustCallable, Variant};
+    use crate::classes::object::ConnectFlags;
+    use crate::meta::FromGodot;
+    use crate::obj::EngineEnum;
+
+    use super::Signal;
+
+    pub struct SignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(Option<R>, Option<Waker>)>>,
+        callable: Callable,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> SignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new((None, Option::<Waker>::None)));
+            let callback_state = state.clone();
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = Callable::from_local_fn("async_task", move |args: &[&Variant]| {
+                let mut lock = callback_state.lock().unwrap();
+                let waker = lock.1.take();
+
+                lock.0.replace(R::from_args(args));
+                drop(lock);
+
+                if let Some(waker) = waker {
+                    waker.wake();
+                }
+
+                Ok(Variant::nil())
+            });
+
+            signal.connect(&callable, ConnectFlags::ONE_SHOT.ord() as i64);
+
+            Self {
+                state,
+                callable,
+                signal,
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Future for SignalFuture<R> {
+        type Output = R;
+
+        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+            let mut lock = self.state.lock().unwrap();
+
+            if let Some(result) = lock.0.take() {
+                return Poll::Ready(result);
+            }
+
+            lock.1.replace(cx.waker().clone());
+
+            Poll::Pending
+        }
+    }
+
+    impl<R: FromSignalArgs> Drop for SignalFuture<R> {
+        fn drop(&mut self) {
+            if !self.callable.is_valid() {
+                return;
+            }
+
+            if self.signal.object().is_none() {
+                return;
+            }
+
+            if self.signal.is_connected(&self.callable) {
+                self.signal.disconnect(&self.callable);
+            }
+        }
+    }
+
+    struct GuaranteedSignalFutureResolver<R> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+    }
+
+    impl<R> Clone for GuaranteedSignalFutureResolver<R> {
+        fn clone(&self) -> Self {
+            Self {
+                state: self.state.clone(),
+            }
+        }
+    }
+
+    impl<R> GuaranteedSignalFutureResolver<R> {
+        fn new(state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>) -> Self {
+            Self { state }
+        }
+    }
+
+    impl<R> std::hash::Hash for GuaranteedSignalFutureResolver<R> {
+        fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+            state.write_usize(Arc::as_ptr(&self.state) as usize);
+        }
+    }
+
+    impl<R> PartialEq for GuaranteedSignalFutureResolver<R> {
+        fn eq(&self, other: &Self) -> bool {
+            Arc::ptr_eq(&self.state, &other.state)
+        }
+    }
+
+    impl<R: FromSignalArgs> RustCallable for GuaranteedSignalFutureResolver<R> {
+        fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+            let mut lock = self.state.lock().unwrap();
+            let waker = lock.1.take();
+
+            lock.0 = GuaranteedSignalFutureState::Ready(R::from_args(args));
+            drop(lock);
+
+            if let Some(waker) = waker {
+                waker.wake();
+            }
+
+            Ok(Variant::nil())
+        }
+    }
+
+    impl<R> Display for GuaranteedSignalFutureResolver<R> {
+        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+            write!(
+                f,
+                "GuaranteedSignalFutureResolver::<{}>",
+                std::any::type_name::<R>()
+            )
+        }
+    }
+
+    // this resolver will resolve the future when it's being dropped (i.e. the engine removes all connected signal callables). This is very unusual.
+    impl<R> Drop for GuaranteedSignalFutureResolver<R> {
+        fn drop(&mut self) {
+            let mut lock = self.state.lock().unwrap();
+
+            if !matches!(lock.0, GuaranteedSignalFutureState::Pending) {
+                return;
+            }
+
+            lock.0 = GuaranteedSignalFutureState::Dead;
+
+            if let Some(ref waker) = lock.1 {
+                waker.wake_by_ref();
+            }
+        }
+    }
+
+    #[derive(Default)]
+    enum GuaranteedSignalFutureState<T> {
+        #[default]
+        Pending,
+        Ready(T),
+        Dead,
+        Dropped,
+    }
+
+    impl<T> GuaranteedSignalFutureState<T> {
+        fn take(&mut self) -> Self {
+            let new_value = match self {
+                Self::Pending => Self::Pending,
+                Self::Ready(_) | Self::Dead => Self::Dead,
+                Self::Dropped => Self::Dropped,
+            };
+
+            std::mem::replace(self, new_value)
+        }
+    }
+
+    /// The guaranteed signal future will always resolve, but might resolve to `None` if the owning object is freed
+    /// before the signal is emitted.
+    ///
+    /// This is inconsistent with how awaiting signals in Godot work and how async works in rust. The behavior was requested as part of some
+    /// user feedback for the initial POC.
+    pub struct GuaranteedSignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+        callable: GuaranteedSignalFutureResolver<R>,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> GuaranteedSignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new((
+                GuaranteedSignalFutureState::Pending,
+                Option::<Waker>::None,
+            )));
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = GuaranteedSignalFutureResolver::new(state.clone());
path: godot-core/src/builtin/signal.rs
user: login: Bromeon
body: ```suggestion
            // The callable currently requires that the return value is Sync + Send.
            let callable = GuaranteedSignalFutureResolver::new(state.clone());
```
created_at: 2025-02-15T11:27:18Z
author_association: MEMBER
diff_hunk: @@ -213,3 +216,310 @@ impl fmt::Display for Signal {
         write!(f, "{}", self.to_variant())
     }
 }
+
+// ----------------------------------------------------------------------------------------------------------------------------------------
+// Implementation of a rust future for Godot Signals
+#[cfg(since_api = "4.2")]
+mod futures {
+    use std::fmt::Display;
+    use std::future::Future;
+    use std::pin::Pin;
+    use std::sync::{Arc, Mutex};
+    use std::task::{Context, Poll, Waker};
+
+    use crate::builtin::{Callable, RustCallable, Variant};
+    use crate::classes::object::ConnectFlags;
+    use crate::meta::FromGodot;
+    use crate::obj::EngineEnum;
+
+    use super::Signal;
+
+    pub struct SignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(Option<R>, Option<Waker>)>>,
+        callable: Callable,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> SignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new((None, Option::<Waker>::None)));
+            let callback_state = state.clone();
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = Callable::from_local_fn("async_task", move |args: &[&Variant]| {
+                let mut lock = callback_state.lock().unwrap();
+                let waker = lock.1.take();
+
+                lock.0.replace(R::from_args(args));
+                drop(lock);
+
+                if let Some(waker) = waker {
+                    waker.wake();
+                }
+
+                Ok(Variant::nil())
+            });
+
+            signal.connect(&callable, ConnectFlags::ONE_SHOT.ord() as i64);
+
+            Self {
+                state,
+                callable,
+                signal,
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Future for SignalFuture<R> {
+        type Output = R;
+
+        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+            let mut lock = self.state.lock().unwrap();
+
+            if let Some(result) = lock.0.take() {
+                return Poll::Ready(result);
+            }
+
+            lock.1.replace(cx.waker().clone());
+
+            Poll::Pending
+        }
+    }
+
+    impl<R: FromSignalArgs> Drop for SignalFuture<R> {
+        fn drop(&mut self) {
+            if !self.callable.is_valid() {
+                return;
+            }
+
+            if self.signal.object().is_none() {
+                return;
+            }
+
+            if self.signal.is_connected(&self.callable) {
+                self.signal.disconnect(&self.callable);
+            }
+        }
+    }
+
+    struct GuaranteedSignalFutureResolver<R> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+    }
+
+    impl<R> Clone for GuaranteedSignalFutureResolver<R> {
+        fn clone(&self) -> Self {
+            Self {
+                state: self.state.clone(),
+            }
+        }
+    }
+
+    impl<R> GuaranteedSignalFutureResolver<R> {
+        fn new(state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>) -> Self {
+            Self { state }
+        }
+    }
+
+    impl<R> std::hash::Hash for GuaranteedSignalFutureResolver<R> {
+        fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+            state.write_usize(Arc::as_ptr(&self.state) as usize);
+        }
+    }
+
+    impl<R> PartialEq for GuaranteedSignalFutureResolver<R> {
+        fn eq(&self, other: &Self) -> bool {
+            Arc::ptr_eq(&self.state, &other.state)
+        }
+    }
+
+    impl<R: FromSignalArgs> RustCallable for GuaranteedSignalFutureResolver<R> {
+        fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+            let mut lock = self.state.lock().unwrap();
+            let waker = lock.1.take();
+
+            lock.0 = GuaranteedSignalFutureState::Ready(R::from_args(args));
+            drop(lock);
+
+            if let Some(waker) = waker {
+                waker.wake();
+            }
+
+            Ok(Variant::nil())
+        }
+    }
+
+    impl<R> Display for GuaranteedSignalFutureResolver<R> {
+        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+            write!(
+                f,
+                "GuaranteedSignalFutureResolver::<{}>",
+                std::any::type_name::<R>()
+            )
+        }
+    }
+
+    // this resolver will resolve the future when it's being dropped (i.e. the engine removes all connected signal callables). This is very unusual.
+    impl<R> Drop for GuaranteedSignalFutureResolver<R> {
+        fn drop(&mut self) {
+            let mut lock = self.state.lock().unwrap();
+
+            if !matches!(lock.0, GuaranteedSignalFutureState::Pending) {
+                return;
+            }
+
+            lock.0 = GuaranteedSignalFutureState::Dead;
+
+            if let Some(ref waker) = lock.1 {
+                waker.wake_by_ref();
+            }
+        }
+    }
+
+    #[derive(Default)]
+    enum GuaranteedSignalFutureState<T> {
+        #[default]
+        Pending,
+        Ready(T),
+        Dead,
+        Dropped,
+    }
+
+    impl<T> GuaranteedSignalFutureState<T> {
+        fn take(&mut self) -> Self {
+            let new_value = match self {
+                Self::Pending => Self::Pending,
+                Self::Ready(_) | Self::Dead => Self::Dead,
+                Self::Dropped => Self::Dropped,
+            };
+
+            std::mem::replace(self, new_value)
+        }
+    }
+
+    /// The guaranteed signal future will always resolve, but might resolve to `None` if the owning object is freed
+    /// before the signal is emitted.
+    ///
+    /// This is inconsistent with how awaiting signals in Godot work and how async works in rust. The behavior was requested as part of some
+    /// user feedback for the initial POC.
+    pub struct GuaranteedSignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+        callable: GuaranteedSignalFutureResolver<R>,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> GuaranteedSignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new((
+                GuaranteedSignalFutureState::Pending,
+                Option::<Waker>::None,
+            )));
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = GuaranteedSignalFutureResolver::new(state.clone());
+
+            signal.connect(
+                &Callable::from_custom(callable.clone()),
+                ConnectFlags::ONE_SHOT.ord() as i64,
+            );
+
+            Self {
+                state,
+                callable,
+                signal,
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Future for GuaranteedSignalFuture<R> {
+        type Output = Option<R>;
+
+        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+            let mut lock = self.state.lock().unwrap();
+
+            lock.1.replace(cx.waker().clone());
+
+            let value = lock.0.take();
+
+            match value {
+                GuaranteedSignalFutureState::Pending => Poll::Pending,
+                GuaranteedSignalFutureState::Dropped => unreachable!(),
+                GuaranteedSignalFutureState::Dead => Poll::Ready(None),
+                GuaranteedSignalFutureState::Ready(value) => Poll::Ready(Some(value)),
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Drop for GuaranteedSignalFuture<R> {
+        fn drop(&mut self) {
+            if self.signal.object().is_none() {
+                return;
+            }
+
+            self.state.lock().unwrap().0 = GuaranteedSignalFutureState::Dropped;
+
+            let gd_callable = Callable::from_custom(self.callable.clone());
+
+            if self.signal.is_connected(&gd_callable) {
+                self.signal.disconnect(&gd_callable);
+            }
+        }
+    }
+
+    pub trait FromSignalArgs: Sync + Send + 'static {
+        fn from_args(args: &[&Variant]) -> Self;
+    }
+
+    impl<R: FromGodot + Sync + Send + 'static> FromSignalArgs for R {
+        fn from_args(args: &[&Variant]) -> Self {
+            args.first()
+                .map(|arg| (*arg).to_owned())
+                .unwrap_or_default()
+                .to()
+        }
+    }
+
+    // more of these should be generated via macro to support more than two signal arguments
+    impl<R1: FromGodot + Sync + Send + 'static, R2: FromGodot + Sync + Send + 'static>
+        FromSignalArgs for (R1, R2)
+    {
+        fn from_args(args: &[&Variant]) -> Self {
+            (args[0].to(), args[0].to())
+        }
+    }
+
+    impl Signal {
+        pub fn to_guaranteed_future<R: FromSignalArgs>(&self) -> GuaranteedSignalFuture<R> {
+            GuaranteedSignalFuture::new(self.clone())
+        }
+
+        pub fn to_future<R: FromSignalArgs>(&self) -> SignalFuture<R> {
+            SignalFuture::new(self.clone())
+        }
+    }
+
+    #[cfg(test)]
+    mod tests {
+        use std::{
+            hash::{DefaultHasher, Hash, Hasher},
+            sync::Arc,
+        };
+
+        use super::GuaranteedSignalFutureResolver;
+
+        #[test]
+        fn guaranteed_future_waker_cloned_hash() {
+            let waker_a = GuaranteedSignalFutureResolver::<u8>::new(Arc::default());
+            let waker_b = waker_a.clone();
+
+            let mut hasher = DefaultHasher::new();
+            waker_a.hash(&mut hasher);
+            let hash_a = hasher.finish();
+
+            let mut hasher = DefaultHasher::new();
+            waker_b.hash(&mut hasher);
+            let hash_b = hasher.finish();
+
+            assert_eq!(hash_a, hash_b);
+        }
path: godot-core/src/builtin/signal.rs
user: login: Bromeon
body: It tests that the hash of two wakers are equal, but can you add a comment that describes what this means on a semantic level (why it matters, or why they could _not_ be equal).

Also, you can use this:
https://github.com/godot-rust/gdext/blob/b2bff8808bc8cac1a31784a905df8d44155e8d8d/godot-ffi/src/toolbox.rs#L143-L148
created_at: 2025-02-15T11:31:18Z
author_association: MEMBER
diff_hunk: @@ -89,4 +89,8 @@ pub use property_info::{PropertyHintInfo, PropertyInfo};
 /// Must not use meta facilities (e.g. `ClassName`) after this call.
 pub(crate) unsafe fn cleanup() {
     class_name::cleanup();
+    // drop all pending tasks from async runtime. Futures have to be dropped before deinit.
+    // Should be moved somewhere more appropriate.
+    #[cfg(since_api = "4.2")]
+    crate::builtin::ASYNC_RUNTIME.take();
path: godot-core/src/meta/mod.rs
user: login: Bromeon
body: Empty line, sentence structure:
```suggestion

    // Drop all pending tasks from async runtime. Futures have to be dropped before deinit.
    // Should be moved somewhere more appropriate.
    #[cfg(since_api = "4.2")]
    crate::builtin::ASYNC_RUNTIME.take();
```


Any concrete thoughts on "more appropriate"? Or is it OK here?
created_at: 2025-02-15T11:32:48Z
author_association: MEMBER
diff_hunk: @@ -89,4 +89,8 @@ pub use property_info::{PropertyHintInfo, PropertyInfo};
 /// Must not use meta facilities (e.g. `ClassName`) after this call.
 pub(crate) unsafe fn cleanup() {
     class_name::cleanup();
+    // drop all pending tasks from async runtime. Futures have to be dropped before deinit.
+    // Should be moved somewhere more appropriate.
+    #[cfg(since_api = "4.2")]
+    crate::builtin::ASYNC_RUNTIME.take();
path: godot-core/src/meta/mod.rs
user: login: Bromeon
body: Also, this only drops the tasks on the thread it's invoked (main thread in this case) -- could there be any left on other threads?
created_at: 2025-02-15T11:33:23Z
author_association: MEMBER
diff_hunk: @@ -47,24 +48,53 @@ pub fn attribute_itest(input_item: venial::Item) -> ParseResult<TokenStream> {
             // Correct parameter type (crude macro check) -> reuse parameter name
             if path_ends_with(&param.ty.tokens, "TestContext") {
                 param.to_token_stream()
+            } else if is_async {
+                return bad_async_signature(&func);
             } else {
                 return bad_signature(&func);
             }
+        } else if is_async {
+            return bad_async_signature(&func);
         } else {
             return bad_signature(&func);
         }
     } else {
         quote! { __unused_context: &crate::framework::TestContext }
     };
 
+    if is_async
+        && func
+            .return_ty
+            .as_ref()
+            .and_then(|return_ty| return_ty.as_path())
+            .and_then(|mut ty| ty.segments.pop())
+            .map_or(true, |segment| segment.ident != "TaskHandle")
+    {
+        return bad_async_signature(&func);
+    }
path: godot-macros/src/itest.rs
user: login: Bromeon
body: There's this utility here:

https://github.com/godot-rust/gdext/blob/b2bff8808bc8cac1a31784a905df8d44155e8d8d/godot-macros/src/util/mod.rs#L216-L222
created_at: 2025-02-15T11:34:49Z
author_association: MEMBER
diff_hunk: @@ -47,24 +48,53 @@ pub fn attribute_itest(input_item: venial::Item) -> ParseResult<TokenStream> {
             // Correct parameter type (crude macro check) -> reuse parameter name
             if path_ends_with(&param.ty.tokens, "TestContext") {
                 param.to_token_stream()
+            } else if is_async {
+                return bad_async_signature(&func);
             } else {
                 return bad_signature(&func);
             }
+        } else if is_async {
+            return bad_async_signature(&func);
         } else {
             return bad_signature(&func);
         }
     } else {
         quote! { __unused_context: &crate::framework::TestContext }
     };
 
+    if is_async
+        && func
+            .return_ty
+            .as_ref()
+            .and_then(|return_ty| return_ty.as_path())
+            .and_then(|mut ty| ty.segments.pop())
+            .map_or(true, |segment| segment.ident != "TaskHandle")
+    {
+        return bad_async_signature(&func);
+    }
+
     let body = &func.body;
 
+    let (test_fn_return, plugin_name, test_case_ty) = if is_async {
+        (
+            quote!(::godot::builtin::TaskHandle),
+            quote!(__GODOT_ASYNC_ITEST),
+            quote!(crate::framework::AsyncRustTestCase),
+        )
+    } else {
+        (
+            quote!(()),
+            quote!(__GODOT_ITEST),
+            quote!(crate::framework::RustTestCase),
+        )
+    };
path: godot-macros/src/itest.rs
user: login: Bromeon
body: - Please use `quote! { ... }` syntax -- curly braces, and space in between.
- Second argument could be simple `ident("__GODOT_ASYNC_ITEST")` instead of `TokenStream`?
- Return type I would omit entirely for non-async itests.
- A matter of style, but I often destructure assignments when there are 2-3 or more initializations, so that it's obvious which value maps to which variable.
- Might group the two similar initializations and move the plugin name to the end.
 
```suggestion
    let (return_tokens, test_case_ty, plugin_name);
    if is_async {
        return_tokens = quote! { -> ::godot::builtin::TaskHandle };
        test_case_ty = quote! { crate::framework::AsyncRustTestCase };
        plugin_name = ident("__GODOT_ASYNC_ITEST");
    } else {
        return_tokens = TokenStream::new();
        test_case_ty = quote! { crate::framework::RustTestCase };
        plugin_name = ident("__ASYNC_ITEST");
    };
```
created_at: 2025-02-15T11:41:28Z
author_association: MEMBER
diff_hunk: @@ -66,20 +66,24 @@ func _ready():
 
 	var property_tests = load("res://gen/GenPropertyTests.gd").new()
 
-	var success: bool = rust_runner.run_all_tests(
+	# Run benchmarks after all synchronous and asynchronous tests have completed.
+	var run_benchmarks = func (success: bool):
+		if success:
+			rust_runner.run_all_benchmarks(self)
+
+		var exit_code: int = 0 if success else 1
+		get_tree().quit(exit_code)
+
+	rust_runner.run_all_tests(
 		gdscript_tests,
 		gdscript_suites.size(),
 		allow_focus,
 		self,
 		filters,
-		property_tests
+		property_tests,
+		run_benchmarks
 	)
path: itest/godot/TestRunner.gd
user: login: Bromeon
body: Instead of a callback, would it be possible to await execution of all tests somehow, to keep the code synchronous and easier to follow? Or would that need bigger changes?
created_at: 2025-02-15T11:45:23Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,70 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use godot::builtin::godot_task;
+use godot::builtin::Signal;
+use godot::classes::Object;
+use godot::obj::NewAlloc;
+
+use crate::framework::itest;
+use crate::framework::TestContext;
+
+async fn call_async_fn(signal: Signal) -> u8 {
+    let value = 5;
+
+    let _: () = signal.to_future().await;
+
+    value + 5
+}
path: itest/rust/src/engine_tests/async_test.rs
user: login: Bromeon
body: Could this take `&Signal` or would that make borrow checks more complicated?

Also, I don't really see the point of declaring a value before the `await` and then adding it later, rather than just returning `10` directly, or nothing at all? If anything, we test Rust `async`/`.await` doesn't mess with a local stack, but I don't see how this relates to anything added in the PR :thinking: 

Would this function be equally valuable with just
```suggestion
async fn await_signal(signal: &Signal) {
    let _: () = signal.to_future().await;
}
```
created_at: 2025-02-15T11:49:58Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,70 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use godot::builtin::godot_task;
+use godot::builtin::Signal;
+use godot::classes::Object;
+use godot::obj::NewAlloc;
+
+use crate::framework::itest;
+use crate::framework::TestContext;
+
+async fn call_async_fn(signal: Signal) -> u8 {
+    let value = 5;
+
+    let _: () = signal.to_future().await;
+
+    value + 5
+}
+
+#[itest(async)]
+fn start_async_task(ctx: &TestContext) -> TaskHandle {
+    let tree = ctx.scene_tree.get_tree().unwrap();
+    let signal = Signal::from_object_signal(&tree, "process_frame");
+
+    godot_task(async move {
+        let inner_signal = signal.clone();
+        godot_task(async move {
+            let _: () = inner_signal.to_future().await;
+        });
+
+        let result = call_async_fn(signal.clone()).await;
+
+        assert_eq!(result, 10);
+    })
+}
+
+#[itest]
+fn cancel_async_task(ctx: &TestContext) {
+    let tree = ctx.scene_tree.get_tree().unwrap();
+    let signal = Signal::from_object_signal(&tree, "process_frame");
+
+    let handle = godot_task(async move {
+        let _: () = signal.to_future().await;
+
+        unreachable!();
+    });
+
+    handle.cancel();
+}
path: itest/rust/src/engine_tests/async_test.rs
user: login: Bromeon
body: After the `handle.cancel();` statement, is there anyhow we can check that it was actually canceled? 

Or is the `unreachable!` sufficient here? If yes, maybe add a message "future must be cancelled before reaching this point" or so...
created_at: 2025-02-15T11:52:16Z
author_association: MEMBER
diff_hunk: @@ -213,3 +216,330 @@ impl fmt::Display for Signal {
         write!(f, "{}", self.to_variant())
     }
 }
+
+// ----------------------------------------------------------------------------------------------------------------------------------------
+// Implementation of a rust future for Godot Signals
+#[cfg(since_api = "4.2")]
+mod futures {
+    use std::fmt::Display;
+    use std::future::Future;
+    use std::pin::Pin;
+    use std::sync::{Arc, Mutex};
+    use std::task::{Context, Poll, Waker};
+
+    use crate::builtin::{Callable, RustCallable, Variant};
+    use crate::classes::object::ConnectFlags;
+    use crate::meta::FromGodot;
+    use crate::obj::EngineEnum;
+
+    use super::Signal;
+
+    struct SignalFutureState<Output> {
+        output: Option<Output>,
+        waker: Option<Waker>,
+    }
+
+    impl<Output> Default for SignalFutureState<Output> {
+        fn default() -> Self {
+            Self {
+                output: None,
+                waker: None,
+            }
+        }
+    }
+
+    pub struct SignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<SignalFutureState<R>>>,
+        callable: Callable,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> SignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new(SignalFutureState::<R>::default()));
+            let callback_state = state.clone();
+
+            #[cfg(not(feature = "experimental-threads"))]
+            let create_callable = Callable::from_local_fn;
+
+            #[cfg(feature = "experimental-threads")]
+            let create_callable = Callable::from_sync_fn;
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = create_callable("async_task", move |args: &[&Variant]| {
+                let mut lock = callback_state.lock().unwrap();
+                let waker = lock.waker.take();
+
+                lock.output.replace(R::from_args(args));
+                drop(lock);
+
+                if let Some(waker) = waker {
+                    waker.wake();
+                }
+
+                Ok(Variant::nil())
+            });
+
+            signal.connect(&callable, ConnectFlags::ONE_SHOT.ord() as i64);
+
+            Self {
+                state,
+                callable,
+                signal,
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Future for SignalFuture<R> {
+        type Output = R;
+
+        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+            let mut lock = self.state.lock().unwrap();
+
+            if let Some(result) = lock.output.take() {
+                return Poll::Ready(result);
+            }
+
+            lock.waker.replace(cx.waker().clone());
+
+            Poll::Pending
+        }
+    }
+
+    impl<R: FromSignalArgs> Drop for SignalFuture<R> {
+        fn drop(&mut self) {
+            if !self.callable.is_valid() {
+                return;
+            }
+
+            if self.signal.object().is_none() {
+                return;
+            }
+
+            if self.signal.is_connected(&self.callable) {
+                self.signal.disconnect(&self.callable);
+            }
+        }
+    }
+
+    struct GuaranteedSignalFutureResolver<R> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+    }
+
+    impl<R> Clone for GuaranteedSignalFutureResolver<R> {
+        fn clone(&self) -> Self {
+            Self {
+                state: self.state.clone(),
+            }
+        }
+    }
+
+    impl<R> GuaranteedSignalFutureResolver<R> {
+        fn new(state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>) -> Self {
+            Self { state }
+        }
+    }
+
+    impl<R> std::hash::Hash for GuaranteedSignalFutureResolver<R> {
+        fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+            state.write_usize(Arc::as_ptr(&self.state) as usize);
+        }
+    }
+
+    impl<R> PartialEq for GuaranteedSignalFutureResolver<R> {
+        fn eq(&self, other: &Self) -> bool {
+            Arc::ptr_eq(&self.state, &other.state)
+        }
+    }
+
+    impl<R: FromSignalArgs> RustCallable for GuaranteedSignalFutureResolver<R> {
+        fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+            let mut lock = self.state.lock().unwrap();
+            let waker = lock.1.take();
+
+            lock.0 = GuaranteedSignalFutureState::Ready(R::from_args(args));
+            drop(lock);
+
+            if let Some(waker) = waker {
+                waker.wake();
+            }
+
+            Ok(Variant::nil())
+        }
+    }
+
+    impl<R> Display for GuaranteedSignalFutureResolver<R> {
+        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+            write!(
+                f,
+                "GuaranteedSignalFutureResolver::<{}>",
+                std::any::type_name::<R>()
+            )
+        }
+    }
+
+    // this resolver will resolve the future when it's being dropped (i.e. the engine removes all connected signal callables). This is very unusual.
+    impl<R> Drop for GuaranteedSignalFutureResolver<R> {
+        fn drop(&mut self) {
+            let mut lock = self.state.lock().unwrap();
+
+            if !matches!(lock.0, GuaranteedSignalFutureState::Pending) {
+                return;
+            }
+
+            lock.0 = GuaranteedSignalFutureState::Dead;
+
+            if let Some(ref waker) = lock.1 {
+                waker.wake_by_ref();
+            }
+        }
+    }
+
+    #[derive(Default)]
+    enum GuaranteedSignalFutureState<T> {
+        #[default]
+        Pending,
+        Ready(T),
+        Dead,
+        Dropped,
+    }
+
+    impl<T> GuaranteedSignalFutureState<T> {
+        fn take(&mut self) -> Self {
+            let new_value = match self {
+                Self::Pending => Self::Pending,
+                Self::Ready(_) | Self::Dead => Self::Dead,
+                Self::Dropped => Self::Dropped,
+            };
+
+            std::mem::replace(self, new_value)
+        }
+    }
+
+    /// The guaranteed signal future will always resolve, but might resolve to `None` if the owning object is freed
+    /// before the signal is emitted.
+    ///
+    /// This is inconsistent with how awaiting signals in Godot work and how async works in rust. The behavior was requested as part of some
+    /// user feedback for the initial POC.
+    pub struct GuaranteedSignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+        callable: GuaranteedSignalFutureResolver<R>,
+        signal: Signal,
+    }
path: godot-core/src/builtin/signal.rs
user: login: Bromeon
body: Please keep the "brief" part limited to 1 line and as concise as possible, so that it fits into module-level doc overviews.

---

> The behavior was requested as part of some feedback for the initial POC"

This is too vague, and makes it very hard for readers to track those reasons down. Could you instead document the design rationale here? You can also include a link to this PR, but generally, the information should be self-contained.

---

Capitalization nitpick: "Rust" is always uppercase.
created_at: 2025-02-15T11:56:27Z
author_association: MEMBER
diff_hunk: @@ -213,3 +216,330 @@ impl fmt::Display for Signal {
         write!(f, "{}", self.to_variant())
     }
 }
+
+// ----------------------------------------------------------------------------------------------------------------------------------------
+// Implementation of a rust future for Godot Signals
+#[cfg(since_api = "4.2")]
+mod futures {
+    use std::fmt::Display;
+    use std::future::Future;
+    use std::pin::Pin;
+    use std::sync::{Arc, Mutex};
+    use std::task::{Context, Poll, Waker};
+
+    use crate::builtin::{Callable, RustCallable, Variant};
+    use crate::classes::object::ConnectFlags;
+    use crate::meta::FromGodot;
+    use crate::obj::EngineEnum;
+
+    use super::Signal;
+
+    struct SignalFutureState<Output> {
+        output: Option<Output>,
+        waker: Option<Waker>,
+    }
+
+    impl<Output> Default for SignalFutureState<Output> {
+        fn default() -> Self {
+            Self {
+                output: None,
+                waker: None,
+            }
+        }
+    }
path: godot-core/src/builtin/signal.rs
user: login: Bromeon
body: Can this be derived?
created_at: 2025-02-15T11:56:53Z
author_association: MEMBER
diff_hunk: @@ -72,18 +95,61 @@ impl IntegrationTests {
         }
 
         let clock = Instant::now();
-        self.run_rust_tests(rust_tests, scene_tree, property_tests.clone());
+        self.run_rust_tests(rust_tests, scene_tree.clone(), property_tests.clone());
         let rust_time = clock.elapsed();
-        property_tests.free();
 
         let gdscript_time = if !focus_run {
             let extra_duration = self.run_gdscript_tests(gdscript_tests);
-            Some((clock.elapsed() - rust_time) + extra_duration)
+            Some((clock.elapsed() - rust_time, extra_duration))
         } else {
             None
         };
 
-        self.conclude_tests(rust_time, gdscript_time, allow_focus)
+        #[cfg(before_api = "4.2")]
+        {
+            property_tests.free();
+
+            let result = Self::conclude_tests(
+                &self.stats,
+                rust_time,
+                gdscript_time.map(|(elapsed, extra)| elapsed + extra),
+                allow_focus,
+            );
+
+            callback.callv(&[result.to_variant()].to_godot());
+        }
+
+        #[cfg(since_api = "4.2")]
+        {
+            let stats = self.stats.clone();
+
+            Self::run_async_rust_tests(
+                stats.clone(),
+                async_rust_tests,
+                scene_tree.clone(),
+                property_tests.clone(),
+                move || {
+                    let gdscript_elapsed = gdscript_time
+                        .as_ref()
+                        .map(|gdtime| gdtime.0)
+                        .unwrap_or_default();
+
+                    let rust_async_time = clock.elapsed() - rust_time - gdscript_elapsed;
+
+                    property_tests.free();
+
+                    let result = Self::conclude_tests(
+                        &stats,
+                        rust_time + rust_async_time,
+                        gdscript_time.map(|(elapsed, extra)| elapsed + extra),
+                        allow_focus,
+                    );
+
+                    // calling deferred to break call stack.
+                    callback.call_deferred(&[result.to_variant()]);
+                },
path: itest/rust/src/framework/runner.rs
user: login: Bromeon
body: Should be its own variable. Generally, avoid overly deeply nested/complex expressions :slightly_smiling_face: 
created_at: 2025-02-15T11:58:07Z
author_association: MEMBER
diff_hunk: @@ -213,3 +216,330 @@ impl fmt::Display for Signal {
         write!(f, "{}", self.to_variant())
     }
 }
+
+// ----------------------------------------------------------------------------------------------------------------------------------------
+// Implementation of a rust future for Godot Signals
+#[cfg(since_api = "4.2")]
+mod futures {
+    use std::fmt::Display;
+    use std::future::Future;
+    use std::pin::Pin;
+    use std::sync::{Arc, Mutex};
+    use std::task::{Context, Poll, Waker};
+
+    use crate::builtin::{Callable, RustCallable, Variant};
+    use crate::classes::object::ConnectFlags;
+    use crate::meta::FromGodot;
+    use crate::obj::EngineEnum;
+
+    use super::Signal;
+
+    struct SignalFutureState<Output> {
+        output: Option<Output>,
+        waker: Option<Waker>,
+    }
+
+    impl<Output> Default for SignalFutureState<Output> {
+        fn default() -> Self {
+            Self {
+                output: None,
+                waker: None,
+            }
+        }
+    }
path: godot-core/src/builtin/signal.rs
user: login: TitanNano
body: If we derive this, `Output` must implement `Default` as well, that is otherwise not necessary, though.
created_at: 2025-02-15T12:11:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -213,3 +216,330 @@ impl fmt::Display for Signal {
         write!(f, "{}", self.to_variant())
     }
 }
+
+// ----------------------------------------------------------------------------------------------------------------------------------------
+// Implementation of a rust future for Godot Signals
+#[cfg(since_api = "4.2")]
+mod futures {
+    use std::fmt::Display;
+    use std::future::Future;
+    use std::pin::Pin;
+    use std::sync::{Arc, Mutex};
+    use std::task::{Context, Poll, Waker};
+
+    use crate::builtin::{Callable, RustCallable, Variant};
+    use crate::classes::object::ConnectFlags;
+    use crate::meta::FromGodot;
+    use crate::obj::EngineEnum;
+
+    use super::Signal;
+
+    struct SignalFutureState<Output> {
+        output: Option<Output>,
+        waker: Option<Waker>,
+    }
+
+    impl<Output> Default for SignalFutureState<Output> {
+        fn default() -> Self {
+            Self {
+                output: None,
+                waker: None,
+            }
+        }
+    }
+
+    pub struct SignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<SignalFutureState<R>>>,
+        callable: Callable,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> SignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new(SignalFutureState::<R>::default()));
+            let callback_state = state.clone();
+
+            #[cfg(not(feature = "experimental-threads"))]
+            let create_callable = Callable::from_local_fn;
+
+            #[cfg(feature = "experimental-threads")]
+            let create_callable = Callable::from_sync_fn;
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = create_callable("async_task", move |args: &[&Variant]| {
+                let mut lock = callback_state.lock().unwrap();
+                let waker = lock.waker.take();
+
+                lock.output.replace(R::from_args(args));
+                drop(lock);
+
+                if let Some(waker) = waker {
+                    waker.wake();
+                }
+
+                Ok(Variant::nil())
+            });
+
+            signal.connect(&callable, ConnectFlags::ONE_SHOT.ord() as i64);
+
+            Self {
+                state,
+                callable,
+                signal,
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Future for SignalFuture<R> {
+        type Output = R;
+
+        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+            let mut lock = self.state.lock().unwrap();
+
+            if let Some(result) = lock.output.take() {
+                return Poll::Ready(result);
+            }
+
+            lock.waker.replace(cx.waker().clone());
+
+            Poll::Pending
+        }
+    }
+
+    impl<R: FromSignalArgs> Drop for SignalFuture<R> {
+        fn drop(&mut self) {
+            if !self.callable.is_valid() {
+                return;
+            }
+
+            if self.signal.object().is_none() {
+                return;
+            }
+
+            if self.signal.is_connected(&self.callable) {
+                self.signal.disconnect(&self.callable);
+            }
+        }
+    }
+
+    struct GuaranteedSignalFutureResolver<R> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+    }
+
+    impl<R> Clone for GuaranteedSignalFutureResolver<R> {
+        fn clone(&self) -> Self {
+            Self {
+                state: self.state.clone(),
+            }
+        }
+    }
+
+    impl<R> GuaranteedSignalFutureResolver<R> {
+        fn new(state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>) -> Self {
+            Self { state }
+        }
+    }
+
+    impl<R> std::hash::Hash for GuaranteedSignalFutureResolver<R> {
+        fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+            state.write_usize(Arc::as_ptr(&self.state) as usize);
+        }
+    }
+
+    impl<R> PartialEq for GuaranteedSignalFutureResolver<R> {
+        fn eq(&self, other: &Self) -> bool {
+            Arc::ptr_eq(&self.state, &other.state)
+        }
+    }
+
+    impl<R: FromSignalArgs> RustCallable for GuaranteedSignalFutureResolver<R> {
+        fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+            let mut lock = self.state.lock().unwrap();
+            let waker = lock.1.take();
+
+            lock.0 = GuaranteedSignalFutureState::Ready(R::from_args(args));
+            drop(lock);
+
+            if let Some(waker) = waker {
+                waker.wake();
+            }
+
+            Ok(Variant::nil())
+        }
+    }
+
+    impl<R> Display for GuaranteedSignalFutureResolver<R> {
+        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+            write!(
+                f,
+                "GuaranteedSignalFutureResolver::<{}>",
+                std::any::type_name::<R>()
+            )
+        }
+    }
+
+    // this resolver will resolve the future when it's being dropped (i.e. the engine removes all connected signal callables). This is very unusual.
+    impl<R> Drop for GuaranteedSignalFutureResolver<R> {
+        fn drop(&mut self) {
+            let mut lock = self.state.lock().unwrap();
+
+            if !matches!(lock.0, GuaranteedSignalFutureState::Pending) {
+                return;
+            }
+
+            lock.0 = GuaranteedSignalFutureState::Dead;
+
+            if let Some(ref waker) = lock.1 {
+                waker.wake_by_ref();
+            }
+        }
+    }
+
+    #[derive(Default)]
+    enum GuaranteedSignalFutureState<T> {
+        #[default]
+        Pending,
+        Ready(T),
+        Dead,
+        Dropped,
+    }
+
+    impl<T> GuaranteedSignalFutureState<T> {
+        fn take(&mut self) -> Self {
+            let new_value = match self {
+                Self::Pending => Self::Pending,
+                Self::Ready(_) | Self::Dead => Self::Dead,
+                Self::Dropped => Self::Dropped,
+            };
+
+            std::mem::replace(self, new_value)
+        }
+    }
+
+    /// The guaranteed signal future will always resolve, but might resolve to `None` if the owning object is freed
+    /// before the signal is emitted.
+    ///
+    /// This is inconsistent with how awaiting signals in Godot work and how async works in rust. The behavior was requested as part of some
+    /// user feedback for the initial POC.
+    pub struct GuaranteedSignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+        callable: GuaranteedSignalFutureResolver<R>,
+        signal: Signal,
+    }
path: godot-core/src/builtin/signal.rs
user: login: TitanNano
body: This definitely needs to be revised, but I would like to first determine if we really want to keep it. Personally, I don't see a real-world use case for it. @jrb0001 requested it, so I'm currently waiting for more feedback from them (See https://github.com/godot-rust/gdext/pull/1043#issuecomment-2652279996). But please let me know if you have an opinion on it.
created_at: 2025-02-15T12:17:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,70 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use godot::builtin::godot_task;
+use godot::builtin::Signal;
+use godot::classes::Object;
+use godot::obj::NewAlloc;
+
+use crate::framework::itest;
+use crate::framework::TestContext;
+
+async fn call_async_fn(signal: Signal) -> u8 {
+    let value = 5;
+
+    let _: () = signal.to_future().await;
+
+    value + 5
+}
+
+#[itest(async)]
+fn start_async_task(ctx: &TestContext) -> TaskHandle {
+    let tree = ctx.scene_tree.get_tree().unwrap();
+    let signal = Signal::from_object_signal(&tree, "process_frame");
+
+    godot_task(async move {
+        let inner_signal = signal.clone();
+        godot_task(async move {
+            let _: () = inner_signal.to_future().await;
+        });
+
+        let result = call_async_fn(signal.clone()).await;
+
+        assert_eq!(result, 10);
+    })
+}
+
+#[itest]
+fn cancel_async_task(ctx: &TestContext) {
+    let tree = ctx.scene_tree.get_tree().unwrap();
+    let signal = Signal::from_object_signal(&tree, "process_frame");
+
+    let handle = godot_task(async move {
+        let _: () = signal.to_future().await;
+
+        unreachable!();
+    });
+
+    handle.cancel();
+}
path: itest/rust/src/engine_tests/async_test.rs
user: login: TitanNano
body: > After the handle.cancel(); statement, is there anyhow we can check that it was actually canceled?

We could add some internal API to the `AsyncRuntime` to check the state of the Future there, but I'd like to avoid adding special APIs.

> Or is the unreachable! sufficient here? If yes, maybe add a message "future must be cancelled before reaching this point" or so...

I would say yes, the previous test should prove that the future completes when not canceled. So if we cancel here and the `unreachable!()` is not tripped, then we should be good. I can add the comment.
created_at: 2025-02-15T12:25:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -213,3 +216,330 @@ impl fmt::Display for Signal {
         write!(f, "{}", self.to_variant())
     }
 }
+
+// ----------------------------------------------------------------------------------------------------------------------------------------
+// Implementation of a rust future for Godot Signals
+#[cfg(since_api = "4.2")]
+mod futures {
+    use std::fmt::Display;
+    use std::future::Future;
+    use std::pin::Pin;
+    use std::sync::{Arc, Mutex};
+    use std::task::{Context, Poll, Waker};
+
+    use crate::builtin::{Callable, RustCallable, Variant};
+    use crate::classes::object::ConnectFlags;
+    use crate::meta::FromGodot;
+    use crate::obj::EngineEnum;
+
+    use super::Signal;
+
+    struct SignalFutureState<Output> {
+        output: Option<Output>,
+        waker: Option<Waker>,
+    }
+
+    impl<Output> Default for SignalFutureState<Output> {
+        fn default() -> Self {
+            Self {
+                output: None,
+                waker: None,
+            }
+        }
+    }
path: godot-core/src/builtin/signal.rs
user: login: Bromeon
body: Thanks! Maybe add a comment (first line):
```suggestion
    // Not derived, otherwise an extra bound `Output: Default` is required.
    impl<Output> Default for SignalFutureState<Output> {
        fn default() -> Self {
            Self {
                output: None,
                waker: None,
            }
        }
    }
```
created_at: 2025-02-15T12:52:18Z
author_association: MEMBER
diff_hunk: @@ -213,3 +216,330 @@ impl fmt::Display for Signal {
         write!(f, "{}", self.to_variant())
     }
 }
+
+// ----------------------------------------------------------------------------------------------------------------------------------------
+// Implementation of a rust future for Godot Signals
+#[cfg(since_api = "4.2")]
+mod futures {
+    use std::fmt::Display;
+    use std::future::Future;
+    use std::pin::Pin;
+    use std::sync::{Arc, Mutex};
+    use std::task::{Context, Poll, Waker};
+
+    use crate::builtin::{Callable, RustCallable, Variant};
+    use crate::classes::object::ConnectFlags;
+    use crate::meta::FromGodot;
+    use crate::obj::EngineEnum;
+
+    use super::Signal;
+
+    struct SignalFutureState<Output> {
+        output: Option<Output>,
+        waker: Option<Waker>,
+    }
+
+    impl<Output> Default for SignalFutureState<Output> {
+        fn default() -> Self {
+            Self {
+                output: None,
+                waker: None,
+            }
+        }
+    }
+
+    pub struct SignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<SignalFutureState<R>>>,
+        callable: Callable,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> SignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new(SignalFutureState::<R>::default()));
+            let callback_state = state.clone();
+
+            #[cfg(not(feature = "experimental-threads"))]
+            let create_callable = Callable::from_local_fn;
+
+            #[cfg(feature = "experimental-threads")]
+            let create_callable = Callable::from_sync_fn;
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = create_callable("async_task", move |args: &[&Variant]| {
+                let mut lock = callback_state.lock().unwrap();
+                let waker = lock.waker.take();
+
+                lock.output.replace(R::from_args(args));
+                drop(lock);
+
+                if let Some(waker) = waker {
+                    waker.wake();
+                }
+
+                Ok(Variant::nil())
+            });
+
+            signal.connect(&callable, ConnectFlags::ONE_SHOT.ord() as i64);
+
+            Self {
+                state,
+                callable,
+                signal,
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Future for SignalFuture<R> {
+        type Output = R;
+
+        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+            let mut lock = self.state.lock().unwrap();
+
+            if let Some(result) = lock.output.take() {
+                return Poll::Ready(result);
+            }
+
+            lock.waker.replace(cx.waker().clone());
+
+            Poll::Pending
+        }
+    }
+
+    impl<R: FromSignalArgs> Drop for SignalFuture<R> {
+        fn drop(&mut self) {
+            if !self.callable.is_valid() {
+                return;
+            }
+
+            if self.signal.object().is_none() {
+                return;
+            }
+
+            if self.signal.is_connected(&self.callable) {
+                self.signal.disconnect(&self.callable);
+            }
+        }
+    }
+
+    struct GuaranteedSignalFutureResolver<R> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+    }
+
+    impl<R> Clone for GuaranteedSignalFutureResolver<R> {
+        fn clone(&self) -> Self {
+            Self {
+                state: self.state.clone(),
+            }
+        }
+    }
+
+    impl<R> GuaranteedSignalFutureResolver<R> {
+        fn new(state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>) -> Self {
+            Self { state }
+        }
+    }
+
+    impl<R> std::hash::Hash for GuaranteedSignalFutureResolver<R> {
+        fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+            state.write_usize(Arc::as_ptr(&self.state) as usize);
+        }
+    }
+
+    impl<R> PartialEq for GuaranteedSignalFutureResolver<R> {
+        fn eq(&self, other: &Self) -> bool {
+            Arc::ptr_eq(&self.state, &other.state)
+        }
+    }
+
+    impl<R: FromSignalArgs> RustCallable for GuaranteedSignalFutureResolver<R> {
+        fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+            let mut lock = self.state.lock().unwrap();
+            let waker = lock.1.take();
+
+            lock.0 = GuaranteedSignalFutureState::Ready(R::from_args(args));
+            drop(lock);
+
+            if let Some(waker) = waker {
+                waker.wake();
+            }
+
+            Ok(Variant::nil())
+        }
+    }
+
+    impl<R> Display for GuaranteedSignalFutureResolver<R> {
+        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+            write!(
+                f,
+                "GuaranteedSignalFutureResolver::<{}>",
+                std::any::type_name::<R>()
+            )
+        }
+    }
+
+    // this resolver will resolve the future when it's being dropped (i.e. the engine removes all connected signal callables). This is very unusual.
+    impl<R> Drop for GuaranteedSignalFutureResolver<R> {
+        fn drop(&mut self) {
+            let mut lock = self.state.lock().unwrap();
+
+            if !matches!(lock.0, GuaranteedSignalFutureState::Pending) {
+                return;
+            }
+
+            lock.0 = GuaranteedSignalFutureState::Dead;
+
+            if let Some(ref waker) = lock.1 {
+                waker.wake_by_ref();
+            }
+        }
+    }
+
+    #[derive(Default)]
+    enum GuaranteedSignalFutureState<T> {
+        #[default]
+        Pending,
+        Ready(T),
+        Dead,
+        Dropped,
+    }
+
+    impl<T> GuaranteedSignalFutureState<T> {
+        fn take(&mut self) -> Self {
+            let new_value = match self {
+                Self::Pending => Self::Pending,
+                Self::Ready(_) | Self::Dead => Self::Dead,
+                Self::Dropped => Self::Dropped,
+            };
+
+            std::mem::replace(self, new_value)
+        }
+    }
+
+    /// The guaranteed signal future will always resolve, but might resolve to `None` if the owning object is freed
+    /// before the signal is emitted.
+    ///
+    /// This is inconsistent with how awaiting signals in Godot work and how async works in rust. The behavior was requested as part of some
+    /// user feedback for the initial POC.
+    pub struct GuaranteedSignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+        callable: GuaranteedSignalFutureResolver<R>,
+        signal: Signal,
+    }
path: godot-core/src/builtin/signal.rs
user: login: Bromeon
body: Agreed. Also, for new features, it's often better to start minimal and then extend over time, once there's real user demand.

But let's see what @jrb0001 has to say :slightly_smiling_face: 
created_at: 2025-02-15T12:54:40Z
author_association: MEMBER
diff_hunk: @@ -213,3 +216,310 @@ impl fmt::Display for Signal {
         write!(f, "{}", self.to_variant())
     }
 }
+
+// ----------------------------------------------------------------------------------------------------------------------------------------
+// Implementation of a rust future for Godot Signals
+#[cfg(since_api = "4.2")]
+mod futures {
+    use std::fmt::Display;
+    use std::future::Future;
+    use std::pin::Pin;
+    use std::sync::{Arc, Mutex};
+    use std::task::{Context, Poll, Waker};
+
+    use crate::builtin::{Callable, RustCallable, Variant};
+    use crate::classes::object::ConnectFlags;
+    use crate::meta::FromGodot;
+    use crate::obj::EngineEnum;
+
+    use super::Signal;
+
+    pub struct SignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(Option<R>, Option<Waker>)>>,
+        callable: Callable,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> SignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new((None, Option::<Waker>::None)));
+            let callback_state = state.clone();
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = Callable::from_local_fn("async_task", move |args: &[&Variant]| {
+                let mut lock = callback_state.lock().unwrap();
+                let waker = lock.1.take();
+
+                lock.0.replace(R::from_args(args));
+                drop(lock);
+
+                if let Some(waker) = waker {
+                    waker.wake();
+                }
+
+                Ok(Variant::nil())
+            });
+
+            signal.connect(&callable, ConnectFlags::ONE_SHOT.ord() as i64);
+
+            Self {
+                state,
+                callable,
+                signal,
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Future for SignalFuture<R> {
+        type Output = R;
+
+        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+            let mut lock = self.state.lock().unwrap();
+
+            if let Some(result) = lock.0.take() {
+                return Poll::Ready(result);
+            }
+
+            lock.1.replace(cx.waker().clone());
+
+            Poll::Pending
+        }
+    }
+
+    impl<R: FromSignalArgs> Drop for SignalFuture<R> {
+        fn drop(&mut self) {
+            if !self.callable.is_valid() {
+                return;
+            }
+
+            if self.signal.object().is_none() {
+                return;
+            }
+
+            if self.signal.is_connected(&self.callable) {
+                self.signal.disconnect(&self.callable);
+            }
+        }
+    }
+
+    struct GuaranteedSignalFutureResolver<R> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+    }
+
+    impl<R> Clone for GuaranteedSignalFutureResolver<R> {
+        fn clone(&self) -> Self {
+            Self {
+                state: self.state.clone(),
+            }
+        }
+    }
+
+    impl<R> GuaranteedSignalFutureResolver<R> {
+        fn new(state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>) -> Self {
+            Self { state }
+        }
+    }
+
+    impl<R> std::hash::Hash for GuaranteedSignalFutureResolver<R> {
+        fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+            state.write_usize(Arc::as_ptr(&self.state) as usize);
+        }
+    }
+
+    impl<R> PartialEq for GuaranteedSignalFutureResolver<R> {
+        fn eq(&self, other: &Self) -> bool {
+            Arc::ptr_eq(&self.state, &other.state)
+        }
+    }
+
+    impl<R: FromSignalArgs> RustCallable for GuaranteedSignalFutureResolver<R> {
+        fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+            let mut lock = self.state.lock().unwrap();
+            let waker = lock.1.take();
+
+            lock.0 = GuaranteedSignalFutureState::Ready(R::from_args(args));
+            drop(lock);
+
+            if let Some(waker) = waker {
+                waker.wake();
+            }
+
+            Ok(Variant::nil())
+        }
+    }
+
+    impl<R> Display for GuaranteedSignalFutureResolver<R> {
+        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+            write!(
+                f,
+                "GuaranteedSignalFutureResolver::<{}>",
+                std::any::type_name::<R>()
+            )
+        }
+    }
+
+    // this resolver will resolve the future when it's being dropped (i.e. the engine removes all connected signal callables). This is very unusual.
+    impl<R> Drop for GuaranteedSignalFutureResolver<R> {
+        fn drop(&mut self) {
+            let mut lock = self.state.lock().unwrap();
+
+            if !matches!(lock.0, GuaranteedSignalFutureState::Pending) {
+                return;
+            }
+
+            lock.0 = GuaranteedSignalFutureState::Dead;
+
+            if let Some(ref waker) = lock.1 {
+                waker.wake_by_ref();
+            }
+        }
+    }
+
+    #[derive(Default)]
+    enum GuaranteedSignalFutureState<T> {
+        #[default]
+        Pending,
+        Ready(T),
+        Dead,
+        Dropped,
+    }
+
+    impl<T> GuaranteedSignalFutureState<T> {
+        fn take(&mut self) -> Self {
+            let new_value = match self {
+                Self::Pending => Self::Pending,
+                Self::Ready(_) | Self::Dead => Self::Dead,
+                Self::Dropped => Self::Dropped,
+            };
+
+            std::mem::replace(self, new_value)
+        }
+    }
+
+    /// The guaranteed signal future will always resolve, but might resolve to `None` if the owning object is freed
+    /// before the signal is emitted.
+    ///
+    /// This is inconsistent with how awaiting signals in Godot work and how async works in rust. The behavior was requested as part of some
+    /// user feedback for the initial POC.
+    pub struct GuaranteedSignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+        callable: GuaranteedSignalFutureResolver<R>,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> GuaranteedSignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new((
+                GuaranteedSignalFutureState::Pending,
+                Option::<Waker>::None,
+            )));
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = GuaranteedSignalFutureResolver::new(state.clone());
+
+            signal.connect(
+                &Callable::from_custom(callable.clone()),
+                ConnectFlags::ONE_SHOT.ord() as i64,
+            );
+
+            Self {
+                state,
+                callable,
+                signal,
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Future for GuaranteedSignalFuture<R> {
+        type Output = Option<R>;
+
+        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+            let mut lock = self.state.lock().unwrap();
+
+            lock.1.replace(cx.waker().clone());
+
+            let value = lock.0.take();
+
+            match value {
+                GuaranteedSignalFutureState::Pending => Poll::Pending,
+                GuaranteedSignalFutureState::Dropped => unreachable!(),
+                GuaranteedSignalFutureState::Dead => Poll::Ready(None),
+                GuaranteedSignalFutureState::Ready(value) => Poll::Ready(Some(value)),
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Drop for GuaranteedSignalFuture<R> {
+        fn drop(&mut self) {
+            if self.signal.object().is_none() {
+                return;
+            }
+
+            self.state.lock().unwrap().0 = GuaranteedSignalFutureState::Dropped;
+
+            let gd_callable = Callable::from_custom(self.callable.clone());
+
+            if self.signal.is_connected(&gd_callable) {
+                self.signal.disconnect(&gd_callable);
+            }
+        }
+    }
+
+    pub trait FromSignalArgs: Sync + Send + 'static {
+        fn from_args(args: &[&Variant]) -> Self;
+    }
+
+    impl<R: FromGodot + Sync + Send + 'static> FromSignalArgs for R {
+        fn from_args(args: &[&Variant]) -> Self {
+            args.first()
+                .map(|arg| (*arg).to_owned())
+                .unwrap_or_default()
+                .to()
+        }
+    }
+
+    // more of these should be generated via macro to support more than two signal arguments
+    impl<R1: FromGodot + Sync + Send + 'static, R2: FromGodot + Sync + Send + 'static>
+        FromSignalArgs for (R1, R2)
+    {
+        fn from_args(args: &[&Variant]) -> Self {
+            (args[0].to(), args[0].to())
+        }
+    }
path: godot-core/src/builtin/signal.rs
user: login: TitanNano
body: Yes, I think it's pretty much the same. I think it would be better to have `impl ParamTuple for T` for single arguments instead of `impl ParamTuple for (T,)`. That seems more ergonomic to use as i.e. a future output 

Having the `TypedSignal` would also greatly benefit this PR as we would no longer have to infer the future output from the variable it's being bound to. Currently, one has to do the following to await a signal without any arguments:
```rs
let _: () = signal.to_future().await;
```

and with the signal arguments as part of the type we could go to:
```rs
signal.await;
```
We would know what the output will be, which also enables us to implement [`IntoFuture`](https://doc.rust-lang.org/std/future/trait.IntoFuture.html)
created_at: 2025-02-15T13:56:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,20 +66,24 @@ func _ready():
 
 	var property_tests = load("res://gen/GenPropertyTests.gd").new()
 
-	var success: bool = rust_runner.run_all_tests(
+	# Run benchmarks after all synchronous and asynchronous tests have completed.
+	var run_benchmarks = func (success: bool):
+		if success:
+			rust_runner.run_all_benchmarks(self)
+
+		var exit_code: int = 0 if success else 1
+		get_tree().quit(exit_code)
+
+	rust_runner.run_all_tests(
 		gdscript_tests,
 		gdscript_suites.size(),
 		allow_focus,
 		self,
 		filters,
-		property_tests
+		property_tests,
+		run_benchmarks
 	)
path: itest/godot/TestRunner.gd
user: login: TitanNano
body: Yes, I can either introduce a new Godot class like `TestHandle` that emits a signal once execution is complete. Or simply add the signal to the `IntegrationTests` class.
created_at: 2025-02-15T14:21:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,70 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use godot::builtin::godot_task;
+use godot::builtin::Signal;
+use godot::classes::Object;
+use godot::obj::NewAlloc;
+
+use crate::framework::itest;
+use crate::framework::TestContext;
+
+async fn call_async_fn(signal: Signal) -> u8 {
+    let value = 5;
+
+    let _: () = signal.to_future().await;
+
+    value + 5
+}
path: itest/rust/src/engine_tests/async_test.rs
user: login: TitanNano
body: I think I wanted a future that resolves to a value, but I agree that this is a bit pointless. I'll just return a constant value.
created_at: 2025-02-15T14:24:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -213,3 +216,310 @@ impl fmt::Display for Signal {
         write!(f, "{}", self.to_variant())
     }
 }
+
+// ----------------------------------------------------------------------------------------------------------------------------------------
+// Implementation of a rust future for Godot Signals
+#[cfg(since_api = "4.2")]
+mod futures {
+    use std::fmt::Display;
+    use std::future::Future;
+    use std::pin::Pin;
+    use std::sync::{Arc, Mutex};
+    use std::task::{Context, Poll, Waker};
+
+    use crate::builtin::{Callable, RustCallable, Variant};
+    use crate::classes::object::ConnectFlags;
+    use crate::meta::FromGodot;
+    use crate::obj::EngineEnum;
+
+    use super::Signal;
+
+    pub struct SignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(Option<R>, Option<Waker>)>>,
+        callable: Callable,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> SignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new((None, Option::<Waker>::None)));
+            let callback_state = state.clone();
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = Callable::from_local_fn("async_task", move |args: &[&Variant]| {
+                let mut lock = callback_state.lock().unwrap();
+                let waker = lock.1.take();
+
+                lock.0.replace(R::from_args(args));
+                drop(lock);
+
+                if let Some(waker) = waker {
+                    waker.wake();
+                }
+
+                Ok(Variant::nil())
+            });
+
+            signal.connect(&callable, ConnectFlags::ONE_SHOT.ord() as i64);
+
+            Self {
+                state,
+                callable,
+                signal,
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Future for SignalFuture<R> {
+        type Output = R;
+
+        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+            let mut lock = self.state.lock().unwrap();
+
+            if let Some(result) = lock.0.take() {
+                return Poll::Ready(result);
+            }
+
+            lock.1.replace(cx.waker().clone());
+
+            Poll::Pending
+        }
+    }
+
+    impl<R: FromSignalArgs> Drop for SignalFuture<R> {
+        fn drop(&mut self) {
+            if !self.callable.is_valid() {
+                return;
+            }
+
+            if self.signal.object().is_none() {
+                return;
+            }
+
+            if self.signal.is_connected(&self.callable) {
+                self.signal.disconnect(&self.callable);
+            }
+        }
+    }
+
+    struct GuaranteedSignalFutureResolver<R> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+    }
+
+    impl<R> Clone for GuaranteedSignalFutureResolver<R> {
+        fn clone(&self) -> Self {
+            Self {
+                state: self.state.clone(),
+            }
+        }
+    }
+
+    impl<R> GuaranteedSignalFutureResolver<R> {
+        fn new(state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>) -> Self {
+            Self { state }
+        }
+    }
+
+    impl<R> std::hash::Hash for GuaranteedSignalFutureResolver<R> {
+        fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+            state.write_usize(Arc::as_ptr(&self.state) as usize);
+        }
+    }
+
+    impl<R> PartialEq for GuaranteedSignalFutureResolver<R> {
+        fn eq(&self, other: &Self) -> bool {
+            Arc::ptr_eq(&self.state, &other.state)
+        }
+    }
+
+    impl<R: FromSignalArgs> RustCallable for GuaranteedSignalFutureResolver<R> {
+        fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+            let mut lock = self.state.lock().unwrap();
+            let waker = lock.1.take();
+
+            lock.0 = GuaranteedSignalFutureState::Ready(R::from_args(args));
+            drop(lock);
+
+            if let Some(waker) = waker {
+                waker.wake();
+            }
+
+            Ok(Variant::nil())
+        }
+    }
+
+    impl<R> Display for GuaranteedSignalFutureResolver<R> {
+        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+            write!(
+                f,
+                "GuaranteedSignalFutureResolver::<{}>",
+                std::any::type_name::<R>()
+            )
+        }
+    }
+
+    // this resolver will resolve the future when it's being dropped (i.e. the engine removes all connected signal callables). This is very unusual.
+    impl<R> Drop for GuaranteedSignalFutureResolver<R> {
+        fn drop(&mut self) {
+            let mut lock = self.state.lock().unwrap();
+
+            if !matches!(lock.0, GuaranteedSignalFutureState::Pending) {
+                return;
+            }
+
+            lock.0 = GuaranteedSignalFutureState::Dead;
+
+            if let Some(ref waker) = lock.1 {
+                waker.wake_by_ref();
+            }
+        }
+    }
+
+    #[derive(Default)]
+    enum GuaranteedSignalFutureState<T> {
+        #[default]
+        Pending,
+        Ready(T),
+        Dead,
+        Dropped,
+    }
+
+    impl<T> GuaranteedSignalFutureState<T> {
+        fn take(&mut self) -> Self {
+            let new_value = match self {
+                Self::Pending => Self::Pending,
+                Self::Ready(_) | Self::Dead => Self::Dead,
+                Self::Dropped => Self::Dropped,
+            };
+
+            std::mem::replace(self, new_value)
+        }
+    }
+
+    /// The guaranteed signal future will always resolve, but might resolve to `None` if the owning object is freed
+    /// before the signal is emitted.
+    ///
+    /// This is inconsistent with how awaiting signals in Godot work and how async works in rust. The behavior was requested as part of some
+    /// user feedback for the initial POC.
+    pub struct GuaranteedSignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+        callable: GuaranteedSignalFutureResolver<R>,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> GuaranteedSignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new((
+                GuaranteedSignalFutureState::Pending,
+                Option::<Waker>::None,
+            )));
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = GuaranteedSignalFutureResolver::new(state.clone());
+
+            signal.connect(
+                &Callable::from_custom(callable.clone()),
+                ConnectFlags::ONE_SHOT.ord() as i64,
+            );
+
+            Self {
+                state,
+                callable,
+                signal,
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Future for GuaranteedSignalFuture<R> {
+        type Output = Option<R>;
+
+        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+            let mut lock = self.state.lock().unwrap();
+
+            lock.1.replace(cx.waker().clone());
+
+            let value = lock.0.take();
+
+            match value {
+                GuaranteedSignalFutureState::Pending => Poll::Pending,
+                GuaranteedSignalFutureState::Dropped => unreachable!(),
+                GuaranteedSignalFutureState::Dead => Poll::Ready(None),
+                GuaranteedSignalFutureState::Ready(value) => Poll::Ready(Some(value)),
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Drop for GuaranteedSignalFuture<R> {
+        fn drop(&mut self) {
+            if self.signal.object().is_none() {
+                return;
+            }
+
+            self.state.lock().unwrap().0 = GuaranteedSignalFutureState::Dropped;
+
+            let gd_callable = Callable::from_custom(self.callable.clone());
+
+            if self.signal.is_connected(&gd_callable) {
+                self.signal.disconnect(&gd_callable);
+            }
+        }
+    }
+
+    pub trait FromSignalArgs: Sync + Send + 'static {
+        fn from_args(args: &[&Variant]) -> Self;
+    }
+
+    impl<R: FromGodot + Sync + Send + 'static> FromSignalArgs for R {
+        fn from_args(args: &[&Variant]) -> Self {
+            args.first()
+                .map(|arg| (*arg).to_owned())
+                .unwrap_or_default()
+                .to()
+        }
+    }
+
+    // more of these should be generated via macro to support more than two signal arguments
+    impl<R1: FromGodot + Sync + Send + 'static, R2: FromGodot + Sync + Send + 'static>
+        FromSignalArgs for (R1, R2)
+    {
+        fn from_args(args: &[&Variant]) -> Self {
+            (args[0].to(), args[0].to())
+        }
+    }
path: godot-core/src/builtin/signal.rs
user: login: Bromeon
body: > Yes, I think it's pretty much the same. I think it would be better to have `impl ParamTuple for T` for single arguments instead of `impl ParamTuple for (T,)`. That seems more ergonomic to use as i.e. a future output

At least in #1000, the `ParamTuple` trait isn't very user-facing, users would mostly interact with proc-macro generated methods that have individual parameters, not the tuple. And for generic arguments, I personally prefer the symmetry of using actual tuples all the type, otherwise you have 
- `()` for 0-ary
- `T` for 1-ary
- `(T, U)` for 2-ary

which is strange and may also complicate implementation. Ergonomics could be provided via proc-macro codegen, but we should probably get the basic mechanics working, this PR is already quite big as-is :slightly_smiling_face: 

I'd recommend you subscribe to #1000, then you'll be notified once it's merged, and could then rebase this on `master`? At the moment I still plan to do one or the other change.
created_at: 2025-02-15T18:26:31Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,70 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use godot::builtin::godot_task;
+use godot::builtin::Signal;
+use godot::classes::Object;
+use godot::obj::NewAlloc;
+
+use crate::framework::itest;
+use crate::framework::TestContext;
+
+async fn call_async_fn(signal: Signal) -> u8 {
+    let value = 5;
+
+    let _: () = signal.to_future().await;
+
+    value + 5
+}
path: itest/rust/src/engine_tests/async_test.rs
user: login: Bromeon
body: But I still don't see what returning a value achieves -- we are not going to test Rust `async` functions, are we? :wink: 
And signals can by definition not return a value...
created_at: 2025-02-15T18:29:38Z
author_association: MEMBER
diff_hunk: @@ -66,20 +66,24 @@ func _ready():
 
 	var property_tests = load("res://gen/GenPropertyTests.gd").new()
 
-	var success: bool = rust_runner.run_all_tests(
+	# Run benchmarks after all synchronous and asynchronous tests have completed.
+	var run_benchmarks = func (success: bool):
+		if success:
+			rust_runner.run_all_benchmarks(self)
+
+		var exit_code: int = 0 if success else 1
+		get_tree().quit(exit_code)
+
+	rust_runner.run_all_tests(
 		gdscript_tests,
 		gdscript_suites.size(),
 		allow_focus,
 		self,
 		filters,
-		property_tests
+		property_tests,
+		run_benchmarks
 	)
path: itest/godot/TestRunner.gd
user: login: Bromeon
body: I originally tried to avoid depending on too many godot-rust features during the `IntegrationTest` setup, because that should be as infallible as possible. If any of the involved mechanisms break, then the whole test suite won't even start (as opposed to just a corresponding test failing), and it's hard to pinpoint the problem.

A signal here wouldn't need any of the functionality added by this PR, but just a plain GDScript signal, right? But even then, maybe your suggestion of a `Callable` passed in as a callback may still be the "lower tech" option :thinking: maybe that's actually the simplest, I didn't think of this before.
created_at: 2025-02-15T18:33:57Z
author_association: MEMBER
diff_hunk: @@ -213,3 +216,310 @@ impl fmt::Display for Signal {
         write!(f, "{}", self.to_variant())
     }
 }
+
+// ----------------------------------------------------------------------------------------------------------------------------------------
+// Implementation of a rust future for Godot Signals
+#[cfg(since_api = "4.2")]
+mod futures {
+    use std::fmt::Display;
+    use std::future::Future;
+    use std::pin::Pin;
+    use std::sync::{Arc, Mutex};
+    use std::task::{Context, Poll, Waker};
+
+    use crate::builtin::{Callable, RustCallable, Variant};
+    use crate::classes::object::ConnectFlags;
+    use crate::meta::FromGodot;
+    use crate::obj::EngineEnum;
+
+    use super::Signal;
+
+    pub struct SignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(Option<R>, Option<Waker>)>>,
+        callable: Callable,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> SignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new((None, Option::<Waker>::None)));
+            let callback_state = state.clone();
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = Callable::from_local_fn("async_task", move |args: &[&Variant]| {
+                let mut lock = callback_state.lock().unwrap();
+                let waker = lock.1.take();
+
+                lock.0.replace(R::from_args(args));
+                drop(lock);
+
+                if let Some(waker) = waker {
+                    waker.wake();
+                }
+
+                Ok(Variant::nil())
+            });
+
+            signal.connect(&callable, ConnectFlags::ONE_SHOT.ord() as i64);
+
+            Self {
+                state,
+                callable,
+                signal,
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Future for SignalFuture<R> {
+        type Output = R;
+
+        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+            let mut lock = self.state.lock().unwrap();
+
+            if let Some(result) = lock.0.take() {
+                return Poll::Ready(result);
+            }
+
+            lock.1.replace(cx.waker().clone());
+
+            Poll::Pending
+        }
+    }
+
+    impl<R: FromSignalArgs> Drop for SignalFuture<R> {
+        fn drop(&mut self) {
+            if !self.callable.is_valid() {
+                return;
+            }
+
+            if self.signal.object().is_none() {
+                return;
+            }
+
+            if self.signal.is_connected(&self.callable) {
+                self.signal.disconnect(&self.callable);
+            }
+        }
+    }
+
+    struct GuaranteedSignalFutureResolver<R> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+    }
+
+    impl<R> Clone for GuaranteedSignalFutureResolver<R> {
+        fn clone(&self) -> Self {
+            Self {
+                state: self.state.clone(),
+            }
+        }
+    }
+
+    impl<R> GuaranteedSignalFutureResolver<R> {
+        fn new(state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>) -> Self {
+            Self { state }
+        }
+    }
+
+    impl<R> std::hash::Hash for GuaranteedSignalFutureResolver<R> {
+        fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+            state.write_usize(Arc::as_ptr(&self.state) as usize);
+        }
+    }
+
+    impl<R> PartialEq for GuaranteedSignalFutureResolver<R> {
+        fn eq(&self, other: &Self) -> bool {
+            Arc::ptr_eq(&self.state, &other.state)
+        }
+    }
+
+    impl<R: FromSignalArgs> RustCallable for GuaranteedSignalFutureResolver<R> {
+        fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+            let mut lock = self.state.lock().unwrap();
+            let waker = lock.1.take();
+
+            lock.0 = GuaranteedSignalFutureState::Ready(R::from_args(args));
+            drop(lock);
+
+            if let Some(waker) = waker {
+                waker.wake();
+            }
+
+            Ok(Variant::nil())
+        }
+    }
+
+    impl<R> Display for GuaranteedSignalFutureResolver<R> {
+        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+            write!(
+                f,
+                "GuaranteedSignalFutureResolver::<{}>",
+                std::any::type_name::<R>()
+            )
+        }
+    }
+
+    // this resolver will resolve the future when it's being dropped (i.e. the engine removes all connected signal callables). This is very unusual.
+    impl<R> Drop for GuaranteedSignalFutureResolver<R> {
+        fn drop(&mut self) {
+            let mut lock = self.state.lock().unwrap();
+
+            if !matches!(lock.0, GuaranteedSignalFutureState::Pending) {
+                return;
+            }
+
+            lock.0 = GuaranteedSignalFutureState::Dead;
+
+            if let Some(ref waker) = lock.1 {
+                waker.wake_by_ref();
+            }
+        }
+    }
+
+    #[derive(Default)]
+    enum GuaranteedSignalFutureState<T> {
+        #[default]
+        Pending,
+        Ready(T),
+        Dead,
+        Dropped,
+    }
+
+    impl<T> GuaranteedSignalFutureState<T> {
+        fn take(&mut self) -> Self {
+            let new_value = match self {
+                Self::Pending => Self::Pending,
+                Self::Ready(_) | Self::Dead => Self::Dead,
+                Self::Dropped => Self::Dropped,
+            };
+
+            std::mem::replace(self, new_value)
+        }
+    }
+
+    /// The guaranteed signal future will always resolve, but might resolve to `None` if the owning object is freed
+    /// before the signal is emitted.
+    ///
+    /// This is inconsistent with how awaiting signals in Godot work and how async works in rust. The behavior was requested as part of some
+    /// user feedback for the initial POC.
+    pub struct GuaranteedSignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+        callable: GuaranteedSignalFutureResolver<R>,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> GuaranteedSignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new((
+                GuaranteedSignalFutureState::Pending,
+                Option::<Waker>::None,
+            )));
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = GuaranteedSignalFutureResolver::new(state.clone());
+
+            signal.connect(
+                &Callable::from_custom(callable.clone()),
+                ConnectFlags::ONE_SHOT.ord() as i64,
+            );
+
+            Self {
+                state,
+                callable,
+                signal,
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Future for GuaranteedSignalFuture<R> {
+        type Output = Option<R>;
+
+        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+            let mut lock = self.state.lock().unwrap();
+
+            lock.1.replace(cx.waker().clone());
+
+            let value = lock.0.take();
+
+            match value {
+                GuaranteedSignalFutureState::Pending => Poll::Pending,
+                GuaranteedSignalFutureState::Dropped => unreachable!(),
+                GuaranteedSignalFutureState::Dead => Poll::Ready(None),
+                GuaranteedSignalFutureState::Ready(value) => Poll::Ready(Some(value)),
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Drop for GuaranteedSignalFuture<R> {
+        fn drop(&mut self) {
+            if self.signal.object().is_none() {
+                return;
+            }
+
+            self.state.lock().unwrap().0 = GuaranteedSignalFutureState::Dropped;
+
+            let gd_callable = Callable::from_custom(self.callable.clone());
+
+            if self.signal.is_connected(&gd_callable) {
+                self.signal.disconnect(&gd_callable);
+            }
+        }
+    }
+
+    pub trait FromSignalArgs: Sync + Send + 'static {
+        fn from_args(args: &[&Variant]) -> Self;
+    }
+
+    impl<R: FromGodot + Sync + Send + 'static> FromSignalArgs for R {
+        fn from_args(args: &[&Variant]) -> Self {
+            args.first()
+                .map(|arg| (*arg).to_owned())
+                .unwrap_or_default()
+                .to()
+        }
+    }
+
+    // more of these should be generated via macro to support more than two signal arguments
+    impl<R1: FromGodot + Sync + Send + 'static, R2: FromGodot + Sync + Send + 'static>
+        FromSignalArgs for (R1, R2)
+    {
+        fn from_args(args: &[&Variant]) -> Self {
+            (args[0].to(), args[0].to())
+        }
+    }
path: godot-core/src/builtin/signal.rs
user: login: Bromeon
body: Also note that initial version of #1000 will likely just provide typing user-defined signals via `#[signal]`. For Godot's own signals, I plan to type them later, but maybe this PR should be able to work with both typed and untyped signals :thinking:

Users may also pass a raw `Signal` around, and it would be nice if that could be awaited as well -- mid-term, we could maybe add `TypedSignal::from_untyped()` though...
created_at: 2025-02-15T18:38:23Z
author_association: MEMBER
diff_hunk: @@ -213,3 +216,310 @@ impl fmt::Display for Signal {
         write!(f, "{}", self.to_variant())
     }
 }
+
+// ----------------------------------------------------------------------------------------------------------------------------------------
+// Implementation of a rust future for Godot Signals
+#[cfg(since_api = "4.2")]
+mod futures {
+    use std::fmt::Display;
+    use std::future::Future;
+    use std::pin::Pin;
+    use std::sync::{Arc, Mutex};
+    use std::task::{Context, Poll, Waker};
+
+    use crate::builtin::{Callable, RustCallable, Variant};
+    use crate::classes::object::ConnectFlags;
+    use crate::meta::FromGodot;
+    use crate::obj::EngineEnum;
+
+    use super::Signal;
+
+    pub struct SignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(Option<R>, Option<Waker>)>>,
+        callable: Callable,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> SignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new((None, Option::<Waker>::None)));
+            let callback_state = state.clone();
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = Callable::from_local_fn("async_task", move |args: &[&Variant]| {
+                let mut lock = callback_state.lock().unwrap();
+                let waker = lock.1.take();
+
+                lock.0.replace(R::from_args(args));
+                drop(lock);
+
+                if let Some(waker) = waker {
+                    waker.wake();
+                }
+
+                Ok(Variant::nil())
+            });
+
+            signal.connect(&callable, ConnectFlags::ONE_SHOT.ord() as i64);
+
+            Self {
+                state,
+                callable,
+                signal,
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Future for SignalFuture<R> {
+        type Output = R;
+
+        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+            let mut lock = self.state.lock().unwrap();
+
+            if let Some(result) = lock.0.take() {
+                return Poll::Ready(result);
+            }
+
+            lock.1.replace(cx.waker().clone());
+
+            Poll::Pending
+        }
+    }
+
+    impl<R: FromSignalArgs> Drop for SignalFuture<R> {
+        fn drop(&mut self) {
+            if !self.callable.is_valid() {
+                return;
+            }
+
+            if self.signal.object().is_none() {
+                return;
+            }
+
+            if self.signal.is_connected(&self.callable) {
+                self.signal.disconnect(&self.callable);
+            }
+        }
+    }
+
+    struct GuaranteedSignalFutureResolver<R> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+    }
+
+    impl<R> Clone for GuaranteedSignalFutureResolver<R> {
+        fn clone(&self) -> Self {
+            Self {
+                state: self.state.clone(),
+            }
+        }
+    }
+
+    impl<R> GuaranteedSignalFutureResolver<R> {
+        fn new(state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>) -> Self {
+            Self { state }
+        }
+    }
+
+    impl<R> std::hash::Hash for GuaranteedSignalFutureResolver<R> {
+        fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+            state.write_usize(Arc::as_ptr(&self.state) as usize);
+        }
+    }
+
+    impl<R> PartialEq for GuaranteedSignalFutureResolver<R> {
+        fn eq(&self, other: &Self) -> bool {
+            Arc::ptr_eq(&self.state, &other.state)
+        }
+    }
+
+    impl<R: FromSignalArgs> RustCallable for GuaranteedSignalFutureResolver<R> {
+        fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+            let mut lock = self.state.lock().unwrap();
+            let waker = lock.1.take();
+
+            lock.0 = GuaranteedSignalFutureState::Ready(R::from_args(args));
+            drop(lock);
+
+            if let Some(waker) = waker {
+                waker.wake();
+            }
+
+            Ok(Variant::nil())
+        }
+    }
+
+    impl<R> Display for GuaranteedSignalFutureResolver<R> {
+        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+            write!(
+                f,
+                "GuaranteedSignalFutureResolver::<{}>",
+                std::any::type_name::<R>()
+            )
+        }
+    }
+
+    // this resolver will resolve the future when it's being dropped (i.e. the engine removes all connected signal callables). This is very unusual.
+    impl<R> Drop for GuaranteedSignalFutureResolver<R> {
+        fn drop(&mut self) {
+            let mut lock = self.state.lock().unwrap();
+
+            if !matches!(lock.0, GuaranteedSignalFutureState::Pending) {
+                return;
+            }
+
+            lock.0 = GuaranteedSignalFutureState::Dead;
+
+            if let Some(ref waker) = lock.1 {
+                waker.wake_by_ref();
+            }
+        }
+    }
+
+    #[derive(Default)]
+    enum GuaranteedSignalFutureState<T> {
+        #[default]
+        Pending,
+        Ready(T),
+        Dead,
+        Dropped,
+    }
+
+    impl<T> GuaranteedSignalFutureState<T> {
+        fn take(&mut self) -> Self {
+            let new_value = match self {
+                Self::Pending => Self::Pending,
+                Self::Ready(_) | Self::Dead => Self::Dead,
+                Self::Dropped => Self::Dropped,
+            };
+
+            std::mem::replace(self, new_value)
+        }
+    }
+
+    /// The guaranteed signal future will always resolve, but might resolve to `None` if the owning object is freed
+    /// before the signal is emitted.
+    ///
+    /// This is inconsistent with how awaiting signals in Godot work and how async works in rust. The behavior was requested as part of some
+    /// user feedback for the initial POC.
+    pub struct GuaranteedSignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+        callable: GuaranteedSignalFutureResolver<R>,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> GuaranteedSignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new((
+                GuaranteedSignalFutureState::Pending,
+                Option::<Waker>::None,
+            )));
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = GuaranteedSignalFutureResolver::new(state.clone());
+
+            signal.connect(
+                &Callable::from_custom(callable.clone()),
+                ConnectFlags::ONE_SHOT.ord() as i64,
+            );
+
+            Self {
+                state,
+                callable,
+                signal,
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Future for GuaranteedSignalFuture<R> {
+        type Output = Option<R>;
+
+        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+            let mut lock = self.state.lock().unwrap();
+
+            lock.1.replace(cx.waker().clone());
+
+            let value = lock.0.take();
+
+            match value {
+                GuaranteedSignalFutureState::Pending => Poll::Pending,
+                GuaranteedSignalFutureState::Dropped => unreachable!(),
+                GuaranteedSignalFutureState::Dead => Poll::Ready(None),
+                GuaranteedSignalFutureState::Ready(value) => Poll::Ready(Some(value)),
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Drop for GuaranteedSignalFuture<R> {
+        fn drop(&mut self) {
+            if self.signal.object().is_none() {
+                return;
+            }
+
+            self.state.lock().unwrap().0 = GuaranteedSignalFutureState::Dropped;
+
+            let gd_callable = Callable::from_custom(self.callable.clone());
+
+            if self.signal.is_connected(&gd_callable) {
+                self.signal.disconnect(&gd_callable);
+            }
+        }
+    }
+
+    pub trait FromSignalArgs: Sync + Send + 'static {
+        fn from_args(args: &[&Variant]) -> Self;
+    }
+
+    impl<R: FromGodot + Sync + Send + 'static> FromSignalArgs for R {
+        fn from_args(args: &[&Variant]) -> Self {
+            args.first()
+                .map(|arg| (*arg).to_owned())
+                .unwrap_or_default()
+                .to()
+        }
+    }
+
+    // more of these should be generated via macro to support more than two signal arguments
+    impl<R1: FromGodot + Sync + Send + 'static, R2: FromGodot + Sync + Send + 'static>
+        FromSignalArgs for (R1, R2)
+    {
+        fn from_args(args: &[&Variant]) -> Self {
+            (args[0].to(), args[0].to())
+        }
+    }
path: godot-core/src/builtin/signal.rs
user: login: TitanNano
body: > maybe this PR should be able to work with both typed and untyped signals 🤔

I will implement the signal future for both untyped and typed signals in this PR once they landed on master. 
created_at: 2025-02-15T20:14:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -89,4 +89,8 @@ pub use property_info::{PropertyHintInfo, PropertyInfo};
 /// Must not use meta facilities (e.g. `ClassName`) after this call.
 pub(crate) unsafe fn cleanup() {
     class_name::cleanup();
+    // drop all pending tasks from async runtime. Futures have to be dropped before deinit.
+    // Should be moved somewhere more appropriate.
+    #[cfg(since_api = "4.2")]
+    crate::builtin::ASYNC_RUNTIME.take();
path: godot-core/src/meta/mod.rs
user: login: TitanNano
body: The `godot_task` function is the only one that can put new futures into the `ASYNC_RUNTIME` thread_local. This function enforces that it's only called from the main thread and panics otherwise. So there should never be a task on another thread. 
created_at: 2025-02-16T19:49:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -89,4 +89,8 @@ pub use property_info::{PropertyHintInfo, PropertyInfo};
 /// Must not use meta facilities (e.g. `ClassName`) after this call.
 pub(crate) unsafe fn cleanup() {
     class_name::cleanup();
+    // drop all pending tasks from async runtime. Futures have to be dropped before deinit.
+    // Should be moved somewhere more appropriate.
+    #[cfg(since_api = "4.2")]
+    crate::builtin::ASYNC_RUNTIME.take();
path: godot-core/src/meta/mod.rs
user: login: TitanNano
body: > Any concrete thoughts on "more appropriate"? Or is it OK here?

My primary concern is that cleaning up the runtime is not unsafe, so it feels out of place here.

Should I add a `cleanup` function to `async_runtime.rs` and call it from `gdext_on_level_deinit`?
created_at: 2025-02-16T19:56:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,20 +66,24 @@ func _ready():
 
 	var property_tests = load("res://gen/GenPropertyTests.gd").new()
 
-	var success: bool = rust_runner.run_all_tests(
+	# Run benchmarks after all synchronous and asynchronous tests have completed.
+	var run_benchmarks = func (success: bool):
+		if success:
+			rust_runner.run_all_benchmarks(self)
+
+		var exit_code: int = 0 if success else 1
+		get_tree().quit(exit_code)
+
+	rust_runner.run_all_tests(
 		gdscript_tests,
 		gdscript_suites.size(),
 		allow_focus,
 		self,
 		filters,
-		property_tests
+		property_tests,
+		run_benchmarks
 	)
path: itest/godot/TestRunner.gd
user: login: TitanNano
body: > A signal here wouldn't need any of the functionality added by this PR, but just a plain GDScript signal, right?

right. 
created_at: 2025-02-16T20:00:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -89,4 +89,8 @@ pub use property_info::{PropertyHintInfo, PropertyInfo};
 /// Must not use meta facilities (e.g. `ClassName`) after this call.
 pub(crate) unsafe fn cleanup() {
     class_name::cleanup();
+    // drop all pending tasks from async runtime. Futures have to be dropped before deinit.
+    // Should be moved somewhere more appropriate.
+    #[cfg(since_api = "4.2")]
+    crate::builtin::ASYNC_RUNTIME.take();
path: godot-core/src/meta/mod.rs
user: login: Bromeon
body: Yes, if it can be guaranteed that it's only called once and safe, that sounds good! :slightly_smiling_face: 
created_at: 2025-02-16T20:02:01Z
author_association: MEMBER
diff_hunk: @@ -66,20 +66,24 @@ func _ready():
 
 	var property_tests = load("res://gen/GenPropertyTests.gd").new()
 
-	var success: bool = rust_runner.run_all_tests(
+	# Run benchmarks after all synchronous and asynchronous tests have completed.
+	var run_benchmarks = func (success: bool):
+		if success:
+			rust_runner.run_all_benchmarks(self)
+
+		var exit_code: int = 0 if success else 1
+		get_tree().quit(exit_code)
+
+	rust_runner.run_all_tests(
 		gdscript_tests,
 		gdscript_suites.size(),
 		allow_focus,
 		self,
 		filters,
-		property_tests
+		property_tests,
+		run_benchmarks
 	)
path: itest/godot/TestRunner.gd
user: login: Bromeon
body: In a way, a signal is just a callable with extra steps -- so maybe your current approach with passing in `Callable` is indeed simpler. You could however call it not just `callback` inside the test, but something like `on_finished_callback` or so...
created_at: 2025-02-16T20:04:11Z
author_association: MEMBER
diff_hunk: @@ -213,3 +216,310 @@ impl fmt::Display for Signal {
         write!(f, "{}", self.to_variant())
     }
 }
+
+// ----------------------------------------------------------------------------------------------------------------------------------------
+// Implementation of a rust future for Godot Signals
+#[cfg(since_api = "4.2")]
+mod futures {
+    use std::fmt::Display;
+    use std::future::Future;
+    use std::pin::Pin;
+    use std::sync::{Arc, Mutex};
+    use std::task::{Context, Poll, Waker};
+
+    use crate::builtin::{Callable, RustCallable, Variant};
+    use crate::classes::object::ConnectFlags;
+    use crate::meta::FromGodot;
+    use crate::obj::EngineEnum;
+
+    use super::Signal;
+
+    pub struct SignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(Option<R>, Option<Waker>)>>,
+        callable: Callable,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> SignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new((None, Option::<Waker>::None)));
+            let callback_state = state.clone();
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = Callable::from_local_fn("async_task", move |args: &[&Variant]| {
+                let mut lock = callback_state.lock().unwrap();
+                let waker = lock.1.take();
+
+                lock.0.replace(R::from_args(args));
+                drop(lock);
+
+                if let Some(waker) = waker {
+                    waker.wake();
+                }
+
+                Ok(Variant::nil())
+            });
+
+            signal.connect(&callable, ConnectFlags::ONE_SHOT.ord() as i64);
+
+            Self {
+                state,
+                callable,
+                signal,
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Future for SignalFuture<R> {
+        type Output = R;
+
+        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+            let mut lock = self.state.lock().unwrap();
+
+            if let Some(result) = lock.0.take() {
+                return Poll::Ready(result);
+            }
+
+            lock.1.replace(cx.waker().clone());
+
+            Poll::Pending
+        }
+    }
+
+    impl<R: FromSignalArgs> Drop for SignalFuture<R> {
+        fn drop(&mut self) {
+            if !self.callable.is_valid() {
+                return;
+            }
+
+            if self.signal.object().is_none() {
+                return;
+            }
+
+            if self.signal.is_connected(&self.callable) {
+                self.signal.disconnect(&self.callable);
+            }
+        }
+    }
+
+    struct GuaranteedSignalFutureResolver<R> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+    }
+
+    impl<R> Clone for GuaranteedSignalFutureResolver<R> {
+        fn clone(&self) -> Self {
+            Self {
+                state: self.state.clone(),
+            }
+        }
+    }
+
+    impl<R> GuaranteedSignalFutureResolver<R> {
+        fn new(state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>) -> Self {
+            Self { state }
+        }
+    }
+
+    impl<R> std::hash::Hash for GuaranteedSignalFutureResolver<R> {
+        fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+            state.write_usize(Arc::as_ptr(&self.state) as usize);
+        }
+    }
+
+    impl<R> PartialEq for GuaranteedSignalFutureResolver<R> {
+        fn eq(&self, other: &Self) -> bool {
+            Arc::ptr_eq(&self.state, &other.state)
+        }
+    }
+
+    impl<R: FromSignalArgs> RustCallable for GuaranteedSignalFutureResolver<R> {
+        fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+            let mut lock = self.state.lock().unwrap();
+            let waker = lock.1.take();
+
+            lock.0 = GuaranteedSignalFutureState::Ready(R::from_args(args));
+            drop(lock);
+
+            if let Some(waker) = waker {
+                waker.wake();
+            }
+
+            Ok(Variant::nil())
+        }
+    }
+
+    impl<R> Display for GuaranteedSignalFutureResolver<R> {
+        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+            write!(
+                f,
+                "GuaranteedSignalFutureResolver::<{}>",
+                std::any::type_name::<R>()
+            )
+        }
+    }
+
+    // this resolver will resolve the future when it's being dropped (i.e. the engine removes all connected signal callables). This is very unusual.
+    impl<R> Drop for GuaranteedSignalFutureResolver<R> {
+        fn drop(&mut self) {
+            let mut lock = self.state.lock().unwrap();
+
+            if !matches!(lock.0, GuaranteedSignalFutureState::Pending) {
+                return;
+            }
+
+            lock.0 = GuaranteedSignalFutureState::Dead;
+
+            if let Some(ref waker) = lock.1 {
+                waker.wake_by_ref();
+            }
+        }
+    }
+
+    #[derive(Default)]
+    enum GuaranteedSignalFutureState<T> {
+        #[default]
+        Pending,
+        Ready(T),
+        Dead,
+        Dropped,
+    }
+
+    impl<T> GuaranteedSignalFutureState<T> {
+        fn take(&mut self) -> Self {
+            let new_value = match self {
+                Self::Pending => Self::Pending,
+                Self::Ready(_) | Self::Dead => Self::Dead,
+                Self::Dropped => Self::Dropped,
+            };
+
+            std::mem::replace(self, new_value)
+        }
+    }
+
+    /// The guaranteed signal future will always resolve, but might resolve to `None` if the owning object is freed
+    /// before the signal is emitted.
+    ///
+    /// This is inconsistent with how awaiting signals in Godot work and how async works in rust. The behavior was requested as part of some
+    /// user feedback for the initial POC.
+    pub struct GuaranteedSignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+        callable: GuaranteedSignalFutureResolver<R>,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> GuaranteedSignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new((
+                GuaranteedSignalFutureState::Pending,
+                Option::<Waker>::None,
+            )));
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = GuaranteedSignalFutureResolver::new(state.clone());
+
+            signal.connect(
+                &Callable::from_custom(callable.clone()),
+                ConnectFlags::ONE_SHOT.ord() as i64,
+            );
+
+            Self {
+                state,
+                callable,
+                signal,
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Future for GuaranteedSignalFuture<R> {
+        type Output = Option<R>;
+
+        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+            let mut lock = self.state.lock().unwrap();
+
+            lock.1.replace(cx.waker().clone());
+
+            let value = lock.0.take();
+
+            match value {
+                GuaranteedSignalFutureState::Pending => Poll::Pending,
+                GuaranteedSignalFutureState::Dropped => unreachable!(),
+                GuaranteedSignalFutureState::Dead => Poll::Ready(None),
+                GuaranteedSignalFutureState::Ready(value) => Poll::Ready(Some(value)),
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Drop for GuaranteedSignalFuture<R> {
+        fn drop(&mut self) {
+            if self.signal.object().is_none() {
+                return;
+            }
+
+            self.state.lock().unwrap().0 = GuaranteedSignalFutureState::Dropped;
+
+            let gd_callable = Callable::from_custom(self.callable.clone());
+
+            if self.signal.is_connected(&gd_callable) {
+                self.signal.disconnect(&gd_callable);
+            }
path: godot-core/src/builtin/signal.rs
user: login: TitanNano
body: I added an itest that checks the equality of the callable in rust. The rust `impl PartialEq for Callable`  is based on the engine callable equality function, so I think this should be sufficient.
created_at: 2025-02-16T22:41:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,70 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use godot::builtin::godot_task;
+use godot::builtin::Signal;
+use godot::classes::Object;
+use godot::obj::NewAlloc;
+
+use crate::framework::itest;
+use crate::framework::TestContext;
+
+async fn call_async_fn(signal: Signal) -> u8 {
+    let value = 5;
+
+    let _: () = signal.to_future().await;
+
+    value + 5
+}
path: itest/rust/src/engine_tests/async_test.rs
user: login: TitanNano
body: I reworked the test to emit a custom signal with a scalar argument. This seems much more concise.
created_at: 2025-02-16T22:43:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -213,3 +216,310 @@ impl fmt::Display for Signal {
         write!(f, "{}", self.to_variant())
     }
 }
+
+// ----------------------------------------------------------------------------------------------------------------------------------------
+// Implementation of a rust future for Godot Signals
+#[cfg(since_api = "4.2")]
+mod futures {
+    use std::fmt::Display;
+    use std::future::Future;
+    use std::pin::Pin;
+    use std::sync::{Arc, Mutex};
+    use std::task::{Context, Poll, Waker};
+
+    use crate::builtin::{Callable, RustCallable, Variant};
+    use crate::classes::object::ConnectFlags;
+    use crate::meta::FromGodot;
+    use crate::obj::EngineEnum;
+
+    use super::Signal;
+
+    pub struct SignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(Option<R>, Option<Waker>)>>,
+        callable: Callable,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> SignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new((None, Option::<Waker>::None)));
+            let callback_state = state.clone();
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = Callable::from_local_fn("async_task", move |args: &[&Variant]| {
+                let mut lock = callback_state.lock().unwrap();
+                let waker = lock.1.take();
+
+                lock.0.replace(R::from_args(args));
+                drop(lock);
+
+                if let Some(waker) = waker {
+                    waker.wake();
+                }
+
+                Ok(Variant::nil())
+            });
+
+            signal.connect(&callable, ConnectFlags::ONE_SHOT.ord() as i64);
+
+            Self {
+                state,
+                callable,
+                signal,
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Future for SignalFuture<R> {
+        type Output = R;
+
+        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+            let mut lock = self.state.lock().unwrap();
+
+            if let Some(result) = lock.0.take() {
+                return Poll::Ready(result);
+            }
+
+            lock.1.replace(cx.waker().clone());
+
+            Poll::Pending
+        }
+    }
+
+    impl<R: FromSignalArgs> Drop for SignalFuture<R> {
+        fn drop(&mut self) {
+            if !self.callable.is_valid() {
+                return;
+            }
+
+            if self.signal.object().is_none() {
+                return;
+            }
+
+            if self.signal.is_connected(&self.callable) {
+                self.signal.disconnect(&self.callable);
+            }
+        }
+    }
+
+    struct GuaranteedSignalFutureResolver<R> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+    }
+
+    impl<R> Clone for GuaranteedSignalFutureResolver<R> {
+        fn clone(&self) -> Self {
+            Self {
+                state: self.state.clone(),
+            }
+        }
+    }
+
+    impl<R> GuaranteedSignalFutureResolver<R> {
+        fn new(state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>) -> Self {
+            Self { state }
+        }
+    }
+
+    impl<R> std::hash::Hash for GuaranteedSignalFutureResolver<R> {
+        fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+            state.write_usize(Arc::as_ptr(&self.state) as usize);
+        }
+    }
+
+    impl<R> PartialEq for GuaranteedSignalFutureResolver<R> {
+        fn eq(&self, other: &Self) -> bool {
+            Arc::ptr_eq(&self.state, &other.state)
+        }
+    }
+
+    impl<R: FromSignalArgs> RustCallable for GuaranteedSignalFutureResolver<R> {
+        fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+            let mut lock = self.state.lock().unwrap();
+            let waker = lock.1.take();
+
+            lock.0 = GuaranteedSignalFutureState::Ready(R::from_args(args));
+            drop(lock);
+
+            if let Some(waker) = waker {
+                waker.wake();
+            }
+
+            Ok(Variant::nil())
+        }
+    }
+
+    impl<R> Display for GuaranteedSignalFutureResolver<R> {
+        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+            write!(
+                f,
+                "GuaranteedSignalFutureResolver::<{}>",
+                std::any::type_name::<R>()
+            )
+        }
+    }
+
+    // this resolver will resolve the future when it's being dropped (i.e. the engine removes all connected signal callables). This is very unusual.
+    impl<R> Drop for GuaranteedSignalFutureResolver<R> {
+        fn drop(&mut self) {
+            let mut lock = self.state.lock().unwrap();
+
+            if !matches!(lock.0, GuaranteedSignalFutureState::Pending) {
+                return;
+            }
+
+            lock.0 = GuaranteedSignalFutureState::Dead;
+
+            if let Some(ref waker) = lock.1 {
+                waker.wake_by_ref();
+            }
+        }
+    }
+
+    #[derive(Default)]
+    enum GuaranteedSignalFutureState<T> {
+        #[default]
+        Pending,
+        Ready(T),
+        Dead,
+        Dropped,
+    }
+
+    impl<T> GuaranteedSignalFutureState<T> {
+        fn take(&mut self) -> Self {
+            let new_value = match self {
+                Self::Pending => Self::Pending,
+                Self::Ready(_) | Self::Dead => Self::Dead,
+                Self::Dropped => Self::Dropped,
+            };
+
+            std::mem::replace(self, new_value)
+        }
+    }
+
+    /// The guaranteed signal future will always resolve, but might resolve to `None` if the owning object is freed
+    /// before the signal is emitted.
+    ///
+    /// This is inconsistent with how awaiting signals in Godot work and how async works in rust. The behavior was requested as part of some
+    /// user feedback for the initial POC.
+    pub struct GuaranteedSignalFuture<R: FromSignalArgs> {
+        state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>,
+        callable: GuaranteedSignalFutureResolver<R>,
+        signal: Signal,
+    }
+
+    impl<R: FromSignalArgs> GuaranteedSignalFuture<R> {
+        fn new(signal: Signal) -> Self {
+            let state = Arc::new(Mutex::new((
+                GuaranteedSignalFutureState::Pending,
+                Option::<Waker>::None,
+            )));
+
+            // the callable currently requires that the return value is Sync + Send
+            let callable = GuaranteedSignalFutureResolver::new(state.clone());
+
+            signal.connect(
+                &Callable::from_custom(callable.clone()),
+                ConnectFlags::ONE_SHOT.ord() as i64,
+            );
+
+            Self {
+                state,
+                callable,
+                signal,
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Future for GuaranteedSignalFuture<R> {
+        type Output = Option<R>;
+
+        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+            let mut lock = self.state.lock().unwrap();
+
+            lock.1.replace(cx.waker().clone());
+
+            let value = lock.0.take();
+
+            match value {
+                GuaranteedSignalFutureState::Pending => Poll::Pending,
+                GuaranteedSignalFutureState::Dropped => unreachable!(),
+                GuaranteedSignalFutureState::Dead => Poll::Ready(None),
+                GuaranteedSignalFutureState::Ready(value) => Poll::Ready(Some(value)),
+            }
+        }
+    }
+
+    impl<R: FromSignalArgs> Drop for GuaranteedSignalFuture<R> {
+        fn drop(&mut self) {
+            if self.signal.object().is_none() {
+                return;
+            }
+
+            self.state.lock().unwrap().0 = GuaranteedSignalFutureState::Dropped;
+
+            let gd_callable = Callable::from_custom(self.callable.clone());
+
+            if self.signal.is_connected(&gd_callable) {
+                self.signal.disconnect(&gd_callable);
+            }
+        }
+    }
+
+    pub trait FromSignalArgs: Sync + Send + 'static {
+        fn from_args(args: &[&Variant]) -> Self;
+    }
+
+    impl<R: FromGodot + Sync + Send + 'static> FromSignalArgs for R {
+        fn from_args(args: &[&Variant]) -> Self {
+            args.first()
+                .map(|arg| (*arg).to_owned())
+                .unwrap_or_default()
+                .to()
+        }
+    }
+
+    // more of these should be generated via macro to support more than two signal arguments
+    impl<R1: FromGodot + Sync + Send + 'static, R2: FromGodot + Sync + Send + 'static>
+        FromSignalArgs for (R1, R2)
+    {
+        fn from_args(args: &[&Variant]) -> Self {
+            (args[0].to(), args[0].to())
+        }
+    }
path: godot-core/src/builtin/signal.rs
user: login: TitanNano
body: `TypedSignal` is now also supported.
created_at: 2025-02-26T22:40:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,409 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_try_future`](crate::builtin::Signal::to_try_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+/// Will be called during engine shudown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.take();
+}
+
+#[cfg(feature = "trace")]
+pub fn is_godot_task_panicked(task_handle: TaskHandle) -> bool {
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: ```suggestion
pub fn has_godot_task_panicked(task_handle: TaskHandle) -> bool {
```
created_at: 2025-03-02T20:06:14Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,409 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_try_future`](crate::builtin::Signal::to_try_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+/// Will be called during engine shudown.
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: ```suggestion
/// Will be called during engine shutdown.
```
created_at: 2025-03-02T20:06:54Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,409 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_try_future`](crate::builtin::Signal::to_try_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+/// Will be called during engine shudown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.take();
+}
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: [`LocalKey<T>::take()`](https://doc.rust-lang.org/std/thread/struct.LocalKey.html#method.take-1) just replaces the `T` with `T::default()`, i.e. initializes the runtime to a new value. 

This is not really detectable from anything potentially using the runtime, but it may interfere with its state (e.g. task handle IDs are restarting at 0). Obviously no one is supposed to use the runtime after `cleanup`, but the problem is that there are no safeguards against errors like this in place.

Would `RefCell<Option<AsyncRuntime>>` be a possibility here? There could be a convenience method to access `with_runtime(|rt| ...)`, which could then panic with a good message if the runtime has already been deinitialized.

This would also no longer require the derived `Default` impl.
created_at: 2025-03-02T20:16:44Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,409 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_try_future`](crate::builtin::Signal::to_try_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: It would be good to express the idea of this `PhantomData`, maybe even in the field name.
```suggestion
    _no_send_sync: PhantomData<*const ()>,
```
(assuming disabling those traits is indeed the purpose)
created_at: 2025-03-02T20:18:17Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,409 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_try_future`](crate::builtin::Signal::to_try_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: Could you add a comment when this return condition holds true (and how it differs from the error states already handled in the match below)?
created_at: 2025-03-02T20:25:53Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,409 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_try_future`](crate::builtin::Signal::to_try_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+/// Will be called during engine shudown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.take();
+}
+
+#[cfg(feature = "trace")]
+pub fn is_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_borrow(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+#[derive(Default)]
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: Is there a real need in `Default`? Otherwise I'd just write `FutureSlotState::Empty` explicitly, it removes an indirection when reading the code.

In general, I'm rather against implementing traits "out of habit", which may not be the most common position in the Rust community :wink: 
created_at: 2025-03-02T20:26:59Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,409 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_try_future`](crate::builtin::Signal::to_try_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+/// Will be called during engine shudown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.take();
+}
+
+#[cfg(feature = "trace")]
+pub fn is_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_borrow(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+#[derive(Default)]
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: Does this require certain state(s) in the FSM to be active now, i.e. a check like you did in `park`?

"Clear" and "park" are more or less standard terminology in this context of async runtimes?

(It's nice that it's symmetric with the outer `park_task`/`clear_task` :+1: )
created_at: 2025-03-02T20:32:46Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,409 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_try_future`](crate::builtin::Signal::to_try_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+/// Will be called during engine shudown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.take();
+}
+
+#[cfg(feature = "trace")]
+pub fn is_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_borrow(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+#[derive(Default)]
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    /// Attempts to extract the future with the given ID from the slot.
+    ///
+    /// Puts the slot into [`FutureSlotState::Polling`] state after taking the future out. It is expected that the future is either parked
+    /// again or the slot is cleared.
+    fn take(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: Also here, is `take` a good name or could it be more concrete? I find it hard to find occurrences since there's `Option::take`, `LocalKey::take`, etc. which have completely different meaning.

Furthermore, the doc "Attempts to extract" should be accompanied (in the doc body) by a description what happens if it "fails" -- as there is no obvious `Result` return type or so.
created_at: 2025-03-02T20:34:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,409 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_try_future`](crate::builtin::Signal::to_try_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+/// Will be called during engine shudown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.take();
+}
+
+#[cfg(feature = "trace")]
+pub fn is_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_borrow(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+#[derive(Default)]
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    /// Attempts to extract the future with the given ID from the slot.
+    ///
+    /// Puts the slot into [`FutureSlotState::Polling`] state after taking the future out. It is expected that the future is either parked
+    /// again or the slot is cleared.
+    fn take(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
+
+    /// Parks the future in this slot again.
+    ///
+    /// # Panics
+    /// - If the slot is not in state [`FutureSlotState::Polling`].
+    fn park(&mut self, value: T) {
+        match self.value {
+            FutureSlotState::Empty | FutureSlotState::Gone => {
+                panic!("Future slot is currently unoccupied, future can not be parked here!");
+            }
+            FutureSlotState::Pending(_) => {
+                panic!("Future slot is already occupied by a different future!")
+            }
+            FutureSlotState::Polling => {
+                self.value = FutureSlotState::Pending(value);
+            }
+        }
+    }
+}
+
+#[derive(Default)]
+/// The storage for the pending tasks of the async runtime.
+struct AsyncRuntime {
+    tasks: Vec<FutureSlot<Pin<Box<dyn Future<Output = ()>>>>>,
+    task_counter: u64,
+    #[cfg(feature = "trace")]
+    panicked_tasks: std::collections::HashSet<u64>,
+}
+
+impl AsyncRuntime {
+    fn new() -> Self {
+        Self {
+            tasks: Vec::with_capacity(10),
+            task_counter: 0,
+            #[cfg(feature = "trace")]
+            panicked_tasks: std::collections::HashSet::default(),
+        }
+    }
+
+    /// Get the next task ID.
+    fn next_id(&mut self) -> u64 {
+        let id = self.task_counter;
+        self.task_counter += 1;
+        id
+    }
+
+    /// Store a new async task in the runtime.
+    ///
+    /// First, a linear search is performed to locate an already existing but currently unoccupied slot in the task buffer. If there is no
+    /// free slot, a new slot is added which may grow the underlying [`Vec`].
+    ///
+    /// The future storage always starts out with a capacity of 10 tasks.
+    fn add_task<F: Future<Output = ()> + 'static>(&mut self, future: F) -> TaskHandle {
+        let id = self.next_id();
+        let index_slot = self
+            .tasks
+            .iter_mut()
+            .enumerate()
+            .find(|(_, slot)| slot.is_empty());
+
+        let boxed = Box::pin(future);
+
+        let index = match index_slot {
+            Some((index, slot)) => {
+                *slot = FutureSlot::pending(id, boxed);
+                index
+            }
+            None => {
+                self.tasks.push(FutureSlot::pending(id, boxed));
+                self.tasks.len() - 1
+            }
+        };
+
+        TaskHandle::new(index, id)
+    }
+
+    /// Extract a pending task from the storage.
+    ///
+    /// Attempts to extract a future with the given ID from the specified index and leaves the slot in state [`FutureSlotState::Polling`].
+    fn get_task(
+        &mut self,
+        index: usize,
+        id: u64,
+    ) -> FutureSlotState<Pin<Box<dyn Future<Output = ()> + 'static>>> {
+        let slot = self.tasks.get_mut(index);
+
+        slot.map(|inner| inner.take(id)).unwrap_or_default()
+    }
+
+    /// Remove a future from the storage and free up its slot.
+    ///
+    /// The slot is left in the [`FutureSlotState::Gone`] state.
+    fn clear_task(&mut self, index: usize) {
+        self.tasks[index].clear();
+    }
+
+    /// Move a future back into its slot.
+    ///
+    /// # Panic
+    /// - If the underlying slot is not in the [`FutureSlotState::Polling`] state.
+    fn park_task(&mut self, index: usize, future: Pin<Box<dyn Future<Output = ()>>>) {
+        self.tasks[index].park(future);
+    }
+
+    /// Track that a future caused a panic.
+    ///
+    /// This is only available for itest.
+    #[cfg(feature = "trace")]
+    fn track_panic(&mut self, task_id: u64) {
+        self.panicked_tasks.insert(task_id);
+    }
+}
+
+/// Use a godot waker to poll it's associated future.
+///
+/// # Panics
+/// - If called from a thread other than the main-thread.
+fn poll_future(godot_waker: Arc<GodotWaker>) {
+    let current_thread = thread::current().id();
+
+    assert_eq!(
+                godot_waker.thread_id,
+                current_thread,
+                "trying to poll future on a different thread!\n  Current thread: {:?}\n  Future thread: {:?}",
+                current_thread,
+                godot_waker.thread_id,
+            );
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: Formatting here :slightly_smiling_face: 
created_at: 2025-03-02T20:35:52Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,409 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_try_future`](crate::builtin::Signal::to_try_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+/// Will be called during engine shudown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.take();
+}
+
+#[cfg(feature = "trace")]
+pub fn is_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_borrow(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+#[derive(Default)]
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    /// Attempts to extract the future with the given ID from the slot.
+    ///
+    /// Puts the slot into [`FutureSlotState::Polling`] state after taking the future out. It is expected that the future is either parked
+    /// again or the slot is cleared.
+    fn take(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
+
+    /// Parks the future in this slot again.
+    ///
+    /// # Panics
+    /// - If the slot is not in state [`FutureSlotState::Polling`].
+    fn park(&mut self, value: T) {
+        match self.value {
+            FutureSlotState::Empty | FutureSlotState::Gone => {
+                panic!("Future slot is currently unoccupied, future can not be parked here!");
+            }
+            FutureSlotState::Pending(_) => {
+                panic!("Future slot is already occupied by a different future!")
+            }
+            FutureSlotState::Polling => {
+                self.value = FutureSlotState::Pending(value);
+            }
+        }
+    }
+}
+
+#[derive(Default)]
+/// The storage for the pending tasks of the async runtime.
+struct AsyncRuntime {
+    tasks: Vec<FutureSlot<Pin<Box<dyn Future<Output = ()>>>>>,
+    task_counter: u64,
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: ```suggestion
    next_task_id: u64,
```
is clearer, as it answers the question "is this the current or next ID"
created_at: 2025-03-02T20:37:27Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,409 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_try_future`](crate::builtin::Signal::to_try_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+/// Will be called during engine shudown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.take();
+}
+
+#[cfg(feature = "trace")]
+pub fn is_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_borrow(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+#[derive(Default)]
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    /// Attempts to extract the future with the given ID from the slot.
+    ///
+    /// Puts the slot into [`FutureSlotState::Polling`] state after taking the future out. It is expected that the future is either parked
+    /// again or the slot is cleared.
+    fn take(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
+
+    /// Parks the future in this slot again.
+    ///
+    /// # Panics
+    /// - If the slot is not in state [`FutureSlotState::Polling`].
+    fn park(&mut self, value: T) {
+        match self.value {
+            FutureSlotState::Empty | FutureSlotState::Gone => {
+                panic!("Future slot is currently unoccupied, future can not be parked here!");
+            }
+            FutureSlotState::Pending(_) => {
+                panic!("Future slot is already occupied by a different future!")
+            }
+            FutureSlotState::Polling => {
+                self.value = FutureSlotState::Pending(value);
+            }
+        }
+    }
+}
+
+#[derive(Default)]
+/// The storage for the pending tasks of the async runtime.
+struct AsyncRuntime {
+    tasks: Vec<FutureSlot<Pin<Box<dyn Future<Output = ()>>>>>,
+    task_counter: u64,
+    #[cfg(feature = "trace")]
+    panicked_tasks: std::collections::HashSet<u64>,
+}
+
+impl AsyncRuntime {
+    fn new() -> Self {
+        Self {
+            tasks: Vec::with_capacity(10),
+            task_counter: 0,
+            #[cfg(feature = "trace")]
+            panicked_tasks: std::collections::HashSet::default(),
+        }
+    }
+
+    /// Get the next task ID.
+    fn next_id(&mut self) -> u64 {
+        let id = self.task_counter;
+        self.task_counter += 1;
+        id
+    }
+
+    /// Store a new async task in the runtime.
+    ///
+    /// First, a linear search is performed to locate an already existing but currently unoccupied slot in the task buffer. If there is no
+    /// free slot, a new slot is added which may grow the underlying [`Vec`].
+    ///
+    /// The future storage always starts out with a capacity of 10 tasks.
+    fn add_task<F: Future<Output = ()> + 'static>(&mut self, future: F) -> TaskHandle {
+        let id = self.next_id();
+        let index_slot = self
+            .tasks
+            .iter_mut()
+            .enumerate()
+            .find(|(_, slot)| slot.is_empty());
+
+        let boxed = Box::pin(future);
+
+        let index = match index_slot {
+            Some((index, slot)) => {
+                *slot = FutureSlot::pending(id, boxed);
+                index
+            }
+            None => {
+                self.tasks.push(FutureSlot::pending(id, boxed));
+                self.tasks.len() - 1
+            }
+        };
+
+        TaskHandle::new(index, id)
+    }
+
+    /// Extract a pending task from the storage.
+    ///
+    /// Attempts to extract a future with the given ID from the specified index and leaves the slot in state [`FutureSlotState::Polling`].
+    fn get_task(
+        &mut self,
+        index: usize,
+        id: u64,
+    ) -> FutureSlotState<Pin<Box<dyn Future<Output = ()> + 'static>>> {
+        let slot = self.tasks.get_mut(index);
+
+        slot.map(|inner| inner.take(id)).unwrap_or_default()
+    }
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: This is what I meant above with "extra indirections due to `Default`.

In my opinion, a
```rs
        slot.map(|inner| inner.take(id)).unwrap_or(FutureSlotState::Empty)
```
is much clearer, as it immediately clarifies two things:
- The retun type is `FutureSlotState` -- which may help given the monstrous return type `FutureSlotState<Pin<Box<dyn Future<Output = ()> + 'static>>>`
- The default state is `Empty` -- I don't need to manually look for `impl Default` or derives
created_at: 2025-03-02T20:42:13Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,409 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_try_future`](crate::builtin::Signal::to_try_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+/// Will be called during engine shudown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.take();
+}
+
+#[cfg(feature = "trace")]
+pub fn is_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_borrow(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+#[derive(Default)]
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    /// Attempts to extract the future with the given ID from the slot.
+    ///
+    /// Puts the slot into [`FutureSlotState::Polling`] state after taking the future out. It is expected that the future is either parked
+    /// again or the slot is cleared.
+    fn take(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
+
+    /// Parks the future in this slot again.
+    ///
+    /// # Panics
+    /// - If the slot is not in state [`FutureSlotState::Polling`].
+    fn park(&mut self, value: T) {
+        match self.value {
+            FutureSlotState::Empty | FutureSlotState::Gone => {
+                panic!("Future slot is currently unoccupied, future can not be parked here!");
+            }
+            FutureSlotState::Pending(_) => {
+                panic!("Future slot is already occupied by a different future!")
+            }
+            FutureSlotState::Polling => {
+                self.value = FutureSlotState::Pending(value);
+            }
+        }
+    }
+}
+
+#[derive(Default)]
+/// The storage for the pending tasks of the async runtime.
+struct AsyncRuntime {
+    tasks: Vec<FutureSlot<Pin<Box<dyn Future<Output = ()>>>>>,
+    task_counter: u64,
+    #[cfg(feature = "trace")]
+    panicked_tasks: std::collections::HashSet<u64>,
+}
+
+impl AsyncRuntime {
+    fn new() -> Self {
+        Self {
+            tasks: Vec::with_capacity(10),
+            task_counter: 0,
+            #[cfg(feature = "trace")]
+            panicked_tasks: std::collections::HashSet::default(),
+        }
+    }
+
+    /// Get the next task ID.
+    fn next_id(&mut self) -> u64 {
+        let id = self.task_counter;
+        self.task_counter += 1;
+        id
+    }
+
+    /// Store a new async task in the runtime.
+    ///
+    /// First, a linear search is performed to locate an already existing but currently unoccupied slot in the task buffer. If there is no
+    /// free slot, a new slot is added which may grow the underlying [`Vec`].
+    ///
+    /// The future storage always starts out with a capacity of 10 tasks.
+    fn add_task<F: Future<Output = ()> + 'static>(&mut self, future: F) -> TaskHandle {
+        let id = self.next_id();
+        let index_slot = self
+            .tasks
+            .iter_mut()
+            .enumerate()
+            .find(|(_, slot)| slot.is_empty());
+
+        let boxed = Box::pin(future);
+
+        let index = match index_slot {
+            Some((index, slot)) => {
+                *slot = FutureSlot::pending(id, boxed);
+                index
+            }
+            None => {
+                self.tasks.push(FutureSlot::pending(id, boxed));
+                self.tasks.len() - 1
+            }
+        };
+
+        TaskHandle::new(index, id)
+    }
+
+    /// Extract a pending task from the storage.
+    ///
+    /// Attempts to extract a future with the given ID from the specified index and leaves the slot in state [`FutureSlotState::Polling`].
+    fn get_task(
+        &mut self,
+        index: usize,
+        id: u64,
+    ) -> FutureSlotState<Pin<Box<dyn Future<Output = ()> + 'static>>> {
+        let slot = self.tasks.get_mut(index);
+
+        slot.map(|inner| inner.take(id)).unwrap_or_default()
+    }
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: Furthermore:
1. Docs should briefly mention the empty slot, too (again "Attempts to..." without explanation of "what if fails")
2. `get` is a very bad name for something that mutates the state machine and extracts the task. Not sure if `remove_task` or so is better :thinking: 
created_at: 2025-03-02T20:42:37Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,409 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_try_future`](crate::builtin::Signal::to_try_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+/// Will be called during engine shudown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.take();
+}
+
+#[cfg(feature = "trace")]
+pub fn is_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_borrow(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+#[derive(Default)]
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    /// Attempts to extract the future with the given ID from the slot.
+    ///
+    /// Puts the slot into [`FutureSlotState::Polling`] state after taking the future out. It is expected that the future is either parked
+    /// again or the slot is cleared.
+    fn take(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
+
+    /// Parks the future in this slot again.
+    ///
+    /// # Panics
+    /// - If the slot is not in state [`FutureSlotState::Polling`].
+    fn park(&mut self, value: T) {
+        match self.value {
+            FutureSlotState::Empty | FutureSlotState::Gone => {
+                panic!("Future slot is currently unoccupied, future can not be parked here!");
+            }
+            FutureSlotState::Pending(_) => {
+                panic!("Future slot is already occupied by a different future!")
+            }
+            FutureSlotState::Polling => {
+                self.value = FutureSlotState::Pending(value);
+            }
+        }
+    }
+}
+
+#[derive(Default)]
+/// The storage for the pending tasks of the async runtime.
+struct AsyncRuntime {
+    tasks: Vec<FutureSlot<Pin<Box<dyn Future<Output = ()>>>>>,
+    task_counter: u64,
+    #[cfg(feature = "trace")]
+    panicked_tasks: std::collections::HashSet<u64>,
+}
+
+impl AsyncRuntime {
+    fn new() -> Self {
+        Self {
+            tasks: Vec::with_capacity(10),
+            task_counter: 0,
+            #[cfg(feature = "trace")]
+            panicked_tasks: std::collections::HashSet::default(),
+        }
+    }
+
+    /// Get the next task ID.
+    fn next_id(&mut self) -> u64 {
+        let id = self.task_counter;
+        self.task_counter += 1;
+        id
+    }
+
+    /// Store a new async task in the runtime.
+    ///
+    /// First, a linear search is performed to locate an already existing but currently unoccupied slot in the task buffer. If there is no
+    /// free slot, a new slot is added which may grow the underlying [`Vec`].
+    ///
+    /// The future storage always starts out with a capacity of 10 tasks.
+    fn add_task<F: Future<Output = ()> + 'static>(&mut self, future: F) -> TaskHandle {
+        let id = self.next_id();
+        let index_slot = self
+            .tasks
+            .iter_mut()
+            .enumerate()
+            .find(|(_, slot)| slot.is_empty());
+
+        let boxed = Box::pin(future);
+
+        let index = match index_slot {
+            Some((index, slot)) => {
+                *slot = FutureSlot::pending(id, boxed);
+                index
+            }
+            None => {
+                self.tasks.push(FutureSlot::pending(id, boxed));
+                self.tasks.len() - 1
+            }
+        };
+
+        TaskHandle::new(index, id)
+    }
+
+    /// Extract a pending task from the storage.
+    ///
+    /// Attempts to extract a future with the given ID from the specified index and leaves the slot in state [`FutureSlotState::Polling`].
+    fn get_task(
+        &mut self,
+        index: usize,
+        id: u64,
+    ) -> FutureSlotState<Pin<Box<dyn Future<Output = ()> + 'static>>> {
+        let slot = self.tasks.get_mut(index);
+
+        slot.map(|inner| inner.take(id)).unwrap_or_default()
+    }
+
+    /// Remove a future from the storage and free up its slot.
+    ///
+    /// The slot is left in the [`FutureSlotState::Gone`] state.
+    fn clear_task(&mut self, index: usize) {
+        self.tasks[index].clear();
+    }
+
+    /// Move a future back into its slot.
+    ///
+    /// # Panic
+    /// - If the underlying slot is not in the [`FutureSlotState::Polling`] state.
+    fn park_task(&mut self, index: usize, future: Pin<Box<dyn Future<Output = ()>>>) {
+        self.tasks[index].park(future);
+    }
+
+    /// Track that a future caused a panic.
+    ///
+    /// This is only available for itest.
+    #[cfg(feature = "trace")]
+    fn track_panic(&mut self, task_id: u64) {
+        self.panicked_tasks.insert(task_id);
+    }
+}
+
+/// Use a godot waker to poll it's associated future.
+///
+/// # Panics
+/// - If called from a thread other than the main-thread.
+fn poll_future(godot_waker: Arc<GodotWaker>) {
+    let current_thread = thread::current().id();
+
+    assert_eq!(
+                godot_waker.thread_id,
+                current_thread,
+                "trying to poll future on a different thread!\n  Current thread: {:?}\n  Future thread: {:?}",
+                current_thread,
+                godot_waker.thread_id,
+            );
+
+    let waker = Waker::from(godot_waker.clone());
+    let mut ctx = Context::from_waker(&waker);
+
+    // Take future out of the runtime.
+    let future = ASYNC_RUNTIME.with_borrow_mut(|rt| {
+        match rt.get_task(godot_waker.runtime_index, godot_waker.task_id) {
+            FutureSlotState::Empty => {
+                panic!("Future slot is empty when waking it! This is a bug!");
+            }
+
+            FutureSlotState::Gone => None,
+
+            FutureSlotState::Polling => {
+                panic!("The same GodotWaker has been called recursively, this is not expected!");
+            }
+
+            FutureSlotState::Pending(future) => Some(future),
+        }
+    });
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: "Take future out" means it is moved out? Or removed?
Maybe use a slightly less ambiguous word here :thinking: 
created_at: 2025-03-02T20:46:09Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,409 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_try_future`](crate::builtin::Signal::to_try_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+/// Will be called during engine shudown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.take();
+}
+
+#[cfg(feature = "trace")]
+pub fn is_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_borrow(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+#[derive(Default)]
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    /// Attempts to extract the future with the given ID from the slot.
+    ///
+    /// Puts the slot into [`FutureSlotState::Polling`] state after taking the future out. It is expected that the future is either parked
+    /// again or the slot is cleared.
+    fn take(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
+
+    /// Parks the future in this slot again.
+    ///
+    /// # Panics
+    /// - If the slot is not in state [`FutureSlotState::Polling`].
+    fn park(&mut self, value: T) {
+        match self.value {
+            FutureSlotState::Empty | FutureSlotState::Gone => {
+                panic!("Future slot is currently unoccupied, future can not be parked here!");
+            }
+            FutureSlotState::Pending(_) => {
+                panic!("Future slot is already occupied by a different future!")
+            }
+            FutureSlotState::Polling => {
+                self.value = FutureSlotState::Pending(value);
+            }
+        }
+    }
+}
+
+#[derive(Default)]
+/// The storage for the pending tasks of the async runtime.
+struct AsyncRuntime {
+    tasks: Vec<FutureSlot<Pin<Box<dyn Future<Output = ()>>>>>,
+    task_counter: u64,
+    #[cfg(feature = "trace")]
+    panicked_tasks: std::collections::HashSet<u64>,
+}
+
+impl AsyncRuntime {
+    fn new() -> Self {
+        Self {
+            tasks: Vec::with_capacity(10),
+            task_counter: 0,
+            #[cfg(feature = "trace")]
+            panicked_tasks: std::collections::HashSet::default(),
+        }
+    }
+
+    /// Get the next task ID.
+    fn next_id(&mut self) -> u64 {
+        let id = self.task_counter;
+        self.task_counter += 1;
+        id
+    }
+
+    /// Store a new async task in the runtime.
+    ///
+    /// First, a linear search is performed to locate an already existing but currently unoccupied slot in the task buffer. If there is no
+    /// free slot, a new slot is added which may grow the underlying [`Vec`].
+    ///
+    /// The future storage always starts out with a capacity of 10 tasks.
+    fn add_task<F: Future<Output = ()> + 'static>(&mut self, future: F) -> TaskHandle {
+        let id = self.next_id();
+        let index_slot = self
+            .tasks
+            .iter_mut()
+            .enumerate()
+            .find(|(_, slot)| slot.is_empty());
+
+        let boxed = Box::pin(future);
+
+        let index = match index_slot {
+            Some((index, slot)) => {
+                *slot = FutureSlot::pending(id, boxed);
+                index
+            }
+            None => {
+                self.tasks.push(FutureSlot::pending(id, boxed));
+                self.tasks.len() - 1
+            }
+        };
+
+        TaskHandle::new(index, id)
+    }
+
+    /// Extract a pending task from the storage.
+    ///
+    /// Attempts to extract a future with the given ID from the specified index and leaves the slot in state [`FutureSlotState::Polling`].
+    fn get_task(
+        &mut self,
+        index: usize,
+        id: u64,
+    ) -> FutureSlotState<Pin<Box<dyn Future<Output = ()> + 'static>>> {
+        let slot = self.tasks.get_mut(index);
+
+        slot.map(|inner| inner.take(id)).unwrap_or_default()
+    }
+
+    /// Remove a future from the storage and free up its slot.
+    ///
+    /// The slot is left in the [`FutureSlotState::Gone`] state.
+    fn clear_task(&mut self, index: usize) {
+        self.tasks[index].clear();
+    }
+
+    /// Move a future back into its slot.
+    ///
+    /// # Panic
+    /// - If the underlying slot is not in the [`FutureSlotState::Polling`] state.
+    fn park_task(&mut self, index: usize, future: Pin<Box<dyn Future<Output = ()>>>) {
+        self.tasks[index].park(future);
+    }
+
+    /// Track that a future caused a panic.
+    ///
+    /// This is only available for itest.
+    #[cfg(feature = "trace")]
+    fn track_panic(&mut self, task_id: u64) {
+        self.panicked_tasks.insert(task_id);
+    }
+}
+
+/// Use a godot waker to poll it's associated future.
+///
+/// # Panics
+/// - If called from a thread other than the main-thread.
+fn poll_future(godot_waker: Arc<GodotWaker>) {
+    let current_thread = thread::current().id();
+
+    assert_eq!(
+                godot_waker.thread_id,
+                current_thread,
+                "trying to poll future on a different thread!\n  Current thread: {:?}\n  Future thread: {:?}",
+                current_thread,
+                godot_waker.thread_id,
+            );
+
+    let waker = Waker::from(godot_waker.clone());
+    let mut ctx = Context::from_waker(&waker);
+
+    // Take future out of the runtime.
+    let future = ASYNC_RUNTIME.with_borrow_mut(|rt| {
+        match rt.get_task(godot_waker.runtime_index, godot_waker.task_id) {
+            FutureSlotState::Empty => {
+                panic!("Future slot is empty when waking it! This is a bug!");
+            }
+
+            FutureSlotState::Gone => None,
+
+            FutureSlotState::Polling => {
+                panic!("The same GodotWaker has been called recursively, this is not expected!");
+            }
+
+            FutureSlotState::Pending(future) => Some(future),
+        }
+    });
+
+    let Some(future) = future else {
+        // Future has been canceled while the waker was already triggered.
+        return;
+    };
+
+    let error_context = || "Godot async task failed";
+    let mut future = AssertUnwindSafe(future);
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: Are there any specific considerations in regards to unwind safety (conditions under which `AssertUnwindSafe` holds)?

If not, OK, I treated this somewhat handwavy myself in the past...
created_at: 2025-03-02T20:47:48Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,409 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_try_future`](crate::builtin::Signal::to_try_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+/// Will be called during engine shudown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.take();
+}
+
+#[cfg(feature = "trace")]
+pub fn is_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_borrow(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+#[derive(Default)]
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    /// Attempts to extract the future with the given ID from the slot.
+    ///
+    /// Puts the slot into [`FutureSlotState::Polling`] state after taking the future out. It is expected that the future is either parked
+    /// again or the slot is cleared.
+    fn take(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
+
+    /// Parks the future in this slot again.
+    ///
+    /// # Panics
+    /// - If the slot is not in state [`FutureSlotState::Polling`].
+    fn park(&mut self, value: T) {
+        match self.value {
+            FutureSlotState::Empty | FutureSlotState::Gone => {
+                panic!("Future slot is currently unoccupied, future can not be parked here!");
+            }
+            FutureSlotState::Pending(_) => {
+                panic!("Future slot is already occupied by a different future!")
+            }
+            FutureSlotState::Polling => {
+                self.value = FutureSlotState::Pending(value);
+            }
+        }
+    }
+}
+
+#[derive(Default)]
+/// The storage for the pending tasks of the async runtime.
+struct AsyncRuntime {
+    tasks: Vec<FutureSlot<Pin<Box<dyn Future<Output = ()>>>>>,
+    task_counter: u64,
+    #[cfg(feature = "trace")]
+    panicked_tasks: std::collections::HashSet<u64>,
+}
+
+impl AsyncRuntime {
+    fn new() -> Self {
+        Self {
+            tasks: Vec::with_capacity(10),
+            task_counter: 0,
+            #[cfg(feature = "trace")]
+            panicked_tasks: std::collections::HashSet::default(),
+        }
+    }
+
+    /// Get the next task ID.
+    fn next_id(&mut self) -> u64 {
+        let id = self.task_counter;
+        self.task_counter += 1;
+        id
+    }
+
+    /// Store a new async task in the runtime.
+    ///
+    /// First, a linear search is performed to locate an already existing but currently unoccupied slot in the task buffer. If there is no
+    /// free slot, a new slot is added which may grow the underlying [`Vec`].
+    ///
+    /// The future storage always starts out with a capacity of 10 tasks.
+    fn add_task<F: Future<Output = ()> + 'static>(&mut self, future: F) -> TaskHandle {
+        let id = self.next_id();
+        let index_slot = self
+            .tasks
+            .iter_mut()
+            .enumerate()
+            .find(|(_, slot)| slot.is_empty());
+
+        let boxed = Box::pin(future);
+
+        let index = match index_slot {
+            Some((index, slot)) => {
+                *slot = FutureSlot::pending(id, boxed);
+                index
+            }
+            None => {
+                self.tasks.push(FutureSlot::pending(id, boxed));
+                self.tasks.len() - 1
+            }
+        };
+
+        TaskHandle::new(index, id)
+    }
+
+    /// Extract a pending task from the storage.
+    ///
+    /// Attempts to extract a future with the given ID from the specified index and leaves the slot in state [`FutureSlotState::Polling`].
+    fn get_task(
+        &mut self,
+        index: usize,
+        id: u64,
+    ) -> FutureSlotState<Pin<Box<dyn Future<Output = ()> + 'static>>> {
+        let slot = self.tasks.get_mut(index);
+
+        slot.map(|inner| inner.take(id)).unwrap_or_default()
+    }
+
+    /// Remove a future from the storage and free up its slot.
+    ///
+    /// The slot is left in the [`FutureSlotState::Gone`] state.
+    fn clear_task(&mut self, index: usize) {
+        self.tasks[index].clear();
+    }
+
+    /// Move a future back into its slot.
+    ///
+    /// # Panic
+    /// - If the underlying slot is not in the [`FutureSlotState::Polling`] state.
+    fn park_task(&mut self, index: usize, future: Pin<Box<dyn Future<Output = ()>>>) {
+        self.tasks[index].park(future);
+    }
+
+    /// Track that a future caused a panic.
+    ///
+    /// This is only available for itest.
+    #[cfg(feature = "trace")]
+    fn track_panic(&mut self, task_id: u64) {
+        self.panicked_tasks.insert(task_id);
+    }
+}
+
+/// Use a godot waker to poll it's associated future.
+///
+/// # Panics
+/// - If called from a thread other than the main-thread.
+fn poll_future(godot_waker: Arc<GodotWaker>) {
+    let current_thread = thread::current().id();
+
+    assert_eq!(
+                godot_waker.thread_id,
+                current_thread,
+                "trying to poll future on a different thread!\n  Current thread: {:?}\n  Future thread: {:?}",
+                current_thread,
+                godot_waker.thread_id,
+            );
+
+    let waker = Waker::from(godot_waker.clone());
+    let mut ctx = Context::from_waker(&waker);
+
+    // Take future out of the runtime.
+    let future = ASYNC_RUNTIME.with_borrow_mut(|rt| {
+        match rt.get_task(godot_waker.runtime_index, godot_waker.task_id) {
+            FutureSlotState::Empty => {
+                panic!("Future slot is empty when waking it! This is a bug!");
+            }
+
+            FutureSlotState::Gone => None,
+
+            FutureSlotState::Polling => {
+                panic!("The same GodotWaker has been called recursively, this is not expected!");
+            }
+
+            FutureSlotState::Pending(future) => Some(future),
+        }
+    });
+
+    let Some(future) = future else {
+        // Future has been canceled while the waker was already triggered.
+        return;
+    };
+
+    let error_context = || "Godot async task failed";
+    let mut future = AssertUnwindSafe(future);
+
+    let panic_result = handle_panic(error_context, move || {
+        (future.as_mut().poll(&mut ctx), future)
+    });
+
+    let Ok((poll_result, future)) = panic_result else {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            #[cfg(feature = "trace")]
+            rt.track_panic(godot_waker.task_id);
+            rt.clear_task(godot_waker.runtime_index);
+        });
+
+        return;
+    };
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: Also here, brief comment on the "roll back" or what exactly this does in the error case would be great!
created_at: 2025-03-02T20:48:46Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,409 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_try_future`](crate::builtin::Signal::to_try_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+/// Will be called during engine shudown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.take();
+}
+
+#[cfg(feature = "trace")]
+pub fn is_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_borrow(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+#[derive(Default)]
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    /// Attempts to extract the future with the given ID from the slot.
+    ///
+    /// Puts the slot into [`FutureSlotState::Polling`] state after taking the future out. It is expected that the future is either parked
+    /// again or the slot is cleared.
+    fn take(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
+
+    /// Parks the future in this slot again.
+    ///
+    /// # Panics
+    /// - If the slot is not in state [`FutureSlotState::Polling`].
+    fn park(&mut self, value: T) {
+        match self.value {
+            FutureSlotState::Empty | FutureSlotState::Gone => {
+                panic!("Future slot is currently unoccupied, future can not be parked here!");
+            }
+            FutureSlotState::Pending(_) => {
+                panic!("Future slot is already occupied by a different future!")
+            }
+            FutureSlotState::Polling => {
+                self.value = FutureSlotState::Pending(value);
+            }
+        }
+    }
+}
+
+#[derive(Default)]
+/// The storage for the pending tasks of the async runtime.
+struct AsyncRuntime {
+    tasks: Vec<FutureSlot<Pin<Box<dyn Future<Output = ()>>>>>,
+    task_counter: u64,
+    #[cfg(feature = "trace")]
+    panicked_tasks: std::collections::HashSet<u64>,
+}
+
+impl AsyncRuntime {
+    fn new() -> Self {
+        Self {
+            tasks: Vec::with_capacity(10),
+            task_counter: 0,
+            #[cfg(feature = "trace")]
+            panicked_tasks: std::collections::HashSet::default(),
+        }
+    }
+
+    /// Get the next task ID.
+    fn next_id(&mut self) -> u64 {
+        let id = self.task_counter;
+        self.task_counter += 1;
+        id
+    }
+
+    /// Store a new async task in the runtime.
+    ///
+    /// First, a linear search is performed to locate an already existing but currently unoccupied slot in the task buffer. If there is no
+    /// free slot, a new slot is added which may grow the underlying [`Vec`].
+    ///
+    /// The future storage always starts out with a capacity of 10 tasks.
+    fn add_task<F: Future<Output = ()> + 'static>(&mut self, future: F) -> TaskHandle {
+        let id = self.next_id();
+        let index_slot = self
+            .tasks
+            .iter_mut()
+            .enumerate()
+            .find(|(_, slot)| slot.is_empty());
+
+        let boxed = Box::pin(future);
+
+        let index = match index_slot {
+            Some((index, slot)) => {
+                *slot = FutureSlot::pending(id, boxed);
+                index
+            }
+            None => {
+                self.tasks.push(FutureSlot::pending(id, boxed));
+                self.tasks.len() - 1
+            }
+        };
+
+        TaskHandle::new(index, id)
+    }
+
+    /// Extract a pending task from the storage.
+    ///
+    /// Attempts to extract a future with the given ID from the specified index and leaves the slot in state [`FutureSlotState::Polling`].
+    fn get_task(
+        &mut self,
+        index: usize,
+        id: u64,
+    ) -> FutureSlotState<Pin<Box<dyn Future<Output = ()> + 'static>>> {
+        let slot = self.tasks.get_mut(index);
+
+        slot.map(|inner| inner.take(id)).unwrap_or_default()
+    }
+
+    /// Remove a future from the storage and free up its slot.
+    ///
+    /// The slot is left in the [`FutureSlotState::Gone`] state.
+    fn clear_task(&mut self, index: usize) {
+        self.tasks[index].clear();
+    }
+
+    /// Move a future back into its slot.
+    ///
+    /// # Panic
+    /// - If the underlying slot is not in the [`FutureSlotState::Polling`] state.
+    fn park_task(&mut self, index: usize, future: Pin<Box<dyn Future<Output = ()>>>) {
+        self.tasks[index].park(future);
+    }
+
+    /// Track that a future caused a panic.
+    ///
+    /// This is only available for itest.
+    #[cfg(feature = "trace")]
+    fn track_panic(&mut self, task_id: u64) {
+        self.panicked_tasks.insert(task_id);
+    }
+}
+
+/// Use a godot waker to poll it's associated future.
+///
+/// # Panics
+/// - If called from a thread other than the main-thread.
+fn poll_future(godot_waker: Arc<GodotWaker>) {
+    let current_thread = thread::current().id();
+
+    assert_eq!(
+                godot_waker.thread_id,
+                current_thread,
+                "trying to poll future on a different thread!\n  Current thread: {:?}\n  Future thread: {:?}",
+                current_thread,
+                godot_waker.thread_id,
+            );
+
+    let waker = Waker::from(godot_waker.clone());
+    let mut ctx = Context::from_waker(&waker);
+
+    // Take future out of the runtime.
+    let future = ASYNC_RUNTIME.with_borrow_mut(|rt| {
+        match rt.get_task(godot_waker.runtime_index, godot_waker.task_id) {
+            FutureSlotState::Empty => {
+                panic!("Future slot is empty when waking it! This is a bug!");
+            }
+
+            FutureSlotState::Gone => None,
+
+            FutureSlotState::Polling => {
+                panic!("The same GodotWaker has been called recursively, this is not expected!");
+            }
+
+            FutureSlotState::Pending(future) => Some(future),
+        }
+    });
+
+    let Some(future) = future else {
+        // Future has been canceled while the waker was already triggered.
+        return;
+    };
+
+    let error_context = || "Godot async task failed";
+    let mut future = AssertUnwindSafe(future);
+
+    let panic_result = handle_panic(error_context, move || {
+        (future.as_mut().poll(&mut ctx), future)
+    });
+
+    let Ok((poll_result, future)) = panic_result else {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            #[cfg(feature = "trace")]
+            rt.track_panic(godot_waker.task_id);
+            rt.clear_task(godot_waker.runtime_index);
+        });
+
+        return;
+    };
+
+    // Update the state of the Future in the runtime.
+    ASYNC_RUNTIME.with_borrow_mut(|rt| match poll_result {
+        // Future is still pending, so we park it again.
+        Poll::Pending => rt.park_task(godot_waker.runtime_index, future.0),
+
+        // Future has resolved, so we remove it from the runtime.
+        Poll::Ready(()) => rt.clear_task(godot_waker.runtime_index),
+    });
+}
+
+/// Implementation of a [`Waker`] to poll futures with the engine.
+struct GodotWaker {
+    runtime_index: usize,
+    task_id: u64,
+    thread_id: ThreadId,
+}
+
+impl GodotWaker {
+    fn new(index: usize, task_id: u64, thread_id: ThreadId) -> Self {
+        Self {
+            runtime_index: index,
+            thread_id,
+            task_id,
+        }
+    }
+}
+
+// uses a deferred callable to poll the associated future.
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: ```suggestion
// Uses a deferred callable to poll the associated future.
```
Maybe briefly repeat what "deferred" means in the context (see also Godot docs for [`Callable::call_deferred`](https://docs.godotengine.org/en/stable/classes/class_callable.html#class-callable-method-call-deferred)): processed during idle time, typically at the end of frames.
created_at: 2025-03-02T20:53:56Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,313 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use core::panic;
+use std::fmt::Display;
+use std::future::{Future, IntoFuture};
+use std::pin::Pin;
+use std::sync::{Arc, Mutex};
+use std::task::{Context, Poll, Waker};
+
+use crate::builtin::{Callable, RustCallable, Variant};
+use crate::classes::object::ConnectFlags;
+use crate::meta::ParamTuple;
+use crate::obj::{EngineBitfield, WithBaseField};
+use crate::registry::signal::TypedSignal;
+
+use super::Signal;
+
+/// The panicking counter part to the [`TrySignalFuture`].
+///
+/// This future works in the same ways as the TrySignalFuture, but panics when the signal object is freed instead of resolving to an
+/// [`Result::Err`]
+pub struct SignalFuture<R: ParamTuple + Sync + Send>(TrySignalFuture<R>);
+
+impl<R: ParamTuple + Sync + Send> SignalFuture<R> {
+    fn new(signal: Signal) -> Self {
+        Self(TrySignalFuture::new(signal))
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> Future for SignalFuture<R> {
+    type Output = R;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        let poll_result = self.get_mut().0.poll(cx);
+
+        match poll_result {
+            Poll::Pending => Poll::Pending,
+            Poll::Ready(Ok(value)) => Poll::Ready(value),
+            Poll::Ready(Err(TrySignalFutureError)) => panic!(
+                "The signal object of a SignalFuture was freed while the future was still waiting for the signal to be emitted!"
path: godot-core/src/builtin/signal/futures.rs
user: login: Bromeon
body: Please avoid exclamation marks in panics, also everywhere else.

When it's one sentence, we typically start with lower case (this follows the standard library):
```suggestion
                "the signal object of a SignalFuture was freed, while the future was still waiting for the signal to be emitted"
```
created_at: 2025-03-02T20:58:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,313 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use core::panic;
+use std::fmt::Display;
+use std::future::{Future, IntoFuture};
+use std::pin::Pin;
+use std::sync::{Arc, Mutex};
+use std::task::{Context, Poll, Waker};
+
+use crate::builtin::{Callable, RustCallable, Variant};
+use crate::classes::object::ConnectFlags;
+use crate::meta::ParamTuple;
+use crate::obj::{EngineBitfield, WithBaseField};
+use crate::registry::signal::TypedSignal;
+
+use super::Signal;
+
+/// The panicking counter part to the [`TrySignalFuture`].
+///
+/// This future works in the same ways as the TrySignalFuture, but panics when the signal object is freed instead of resolving to an
+/// [`Result::Err`]
+pub struct SignalFuture<R: ParamTuple + Sync + Send>(TrySignalFuture<R>);
+
+impl<R: ParamTuple + Sync + Send> SignalFuture<R> {
+    fn new(signal: Signal) -> Self {
+        Self(TrySignalFuture::new(signal))
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> Future for SignalFuture<R> {
+    type Output = R;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        let poll_result = self.get_mut().0.poll(cx);
+
+        match poll_result {
+            Poll::Pending => Poll::Pending,
+            Poll::Ready(Ok(value)) => Poll::Ready(value),
+            Poll::Ready(Err(TrySignalFutureError)) => panic!(
+                "The signal object of a SignalFuture was freed while the future was still waiting for the signal to be emitted!"
+            ),
+        }
+    }
+}
+
+// Not derived, otherwise an extra bound `Output: Default` is required.
+struct SignalFutureData<T> {
+    state: TrySignalFutureState<T>,
+    waker: Option<Waker>,
+}
+
+impl<T> Default for SignalFutureData<T> {
+    fn default() -> Self {
+        Self {
+            state: Default::default(),
+            waker: None,
+        }
+    }
+}
path: godot-core/src/builtin/signal/futures.rs
user: login: Bromeon
body: The purpose of `SignalFutureData` isn't quite clear to me. Could you add some RustDoc on it?

Also, might there be a better name for this than "data"?


In the `Default` impl, the field `state` having an explicit value would make it a bit easier to understand.


created_at: 2025-03-02T21:01:03Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,313 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use core::panic;
+use std::fmt::Display;
+use std::future::{Future, IntoFuture};
+use std::pin::Pin;
+use std::sync::{Arc, Mutex};
+use std::task::{Context, Poll, Waker};
+
+use crate::builtin::{Callable, RustCallable, Variant};
+use crate::classes::object::ConnectFlags;
+use crate::meta::ParamTuple;
+use crate::obj::{EngineBitfield, WithBaseField};
+use crate::registry::signal::TypedSignal;
+
+use super::Signal;
+
+/// The panicking counter part to the [`TrySignalFuture`].
+///
+/// This future works in the same ways as the TrySignalFuture, but panics when the signal object is freed instead of resolving to an
+/// [`Result::Err`]
+pub struct SignalFuture<R: ParamTuple + Sync + Send>(TrySignalFuture<R>);
+
+impl<R: ParamTuple + Sync + Send> SignalFuture<R> {
+    fn new(signal: Signal) -> Self {
+        Self(TrySignalFuture::new(signal))
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> Future for SignalFuture<R> {
+    type Output = R;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        let poll_result = self.get_mut().0.poll(cx);
+
+        match poll_result {
+            Poll::Pending => Poll::Pending,
+            Poll::Ready(Ok(value)) => Poll::Ready(value),
+            Poll::Ready(Err(TrySignalFutureError)) => panic!(
+                "The signal object of a SignalFuture was freed while the future was still waiting for the signal to be emitted!"
+            ),
+        }
+    }
+}
+
+// Not derived, otherwise an extra bound `Output: Default` is required.
+struct SignalFutureData<T> {
+    state: TrySignalFutureState<T>,
+    waker: Option<Waker>,
+}
+
+impl<T> Default for SignalFutureData<T> {
+    fn default() -> Self {
+        Self {
+            state: Default::default(),
+            waker: None,
+        }
+    }
+}
+
+// Only public for itest.
+#[cfg_attr(feature = "trace", derive(Default))]
+pub struct SignalFutureResolver<R> {
+    data: Arc<Mutex<SignalFutureData<R>>>,
+}
+
+impl<R> Clone for SignalFutureResolver<R> {
+    fn clone(&self) -> Self {
+        Self {
+            data: self.data.clone(),
+        }
+    }
+}
+
+impl<R> SignalFutureResolver<R> {
+    fn new(data: Arc<Mutex<SignalFutureData<R>>>) -> Self {
+        Self { data }
+    }
+}
+
+impl<R> std::hash::Hash for SignalFutureResolver<R> {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        state.write_usize(Arc::as_ptr(&self.data) as usize);
+    }
+}
+
+impl<R> PartialEq for SignalFutureResolver<R> {
+    fn eq(&self, other: &Self) -> bool {
+        Arc::ptr_eq(&self.data, &other.data)
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> RustCallable for SignalFutureResolver<R> {
+    fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+        let mut data = self.data.lock().unwrap();
+        // We no loger need the waker after we resolved. If the future get's polled again we also get a new waker.
path: godot-core/src/builtin/signal/futures.rs
user: login: Bromeon
body: ```suggestion

        // We no longer need the waker after we resolved. If the future is polled again, we'll also get a new waker.
```
created_at: 2025-03-02T21:01:27Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,313 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use core::panic;
+use std::fmt::Display;
+use std::future::{Future, IntoFuture};
+use std::pin::Pin;
+use std::sync::{Arc, Mutex};
+use std::task::{Context, Poll, Waker};
+
+use crate::builtin::{Callable, RustCallable, Variant};
+use crate::classes::object::ConnectFlags;
+use crate::meta::ParamTuple;
+use crate::obj::{EngineBitfield, WithBaseField};
+use crate::registry::signal::TypedSignal;
+
+use super::Signal;
+
+/// The panicking counter part to the [`TrySignalFuture`].
+///
+/// This future works in the same ways as the TrySignalFuture, but panics when the signal object is freed instead of resolving to an
+/// [`Result::Err`]
+pub struct SignalFuture<R: ParamTuple + Sync + Send>(TrySignalFuture<R>);
+
+impl<R: ParamTuple + Sync + Send> SignalFuture<R> {
+    fn new(signal: Signal) -> Self {
+        Self(TrySignalFuture::new(signal))
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> Future for SignalFuture<R> {
+    type Output = R;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        let poll_result = self.get_mut().0.poll(cx);
+
+        match poll_result {
+            Poll::Pending => Poll::Pending,
+            Poll::Ready(Ok(value)) => Poll::Ready(value),
+            Poll::Ready(Err(TrySignalFutureError)) => panic!(
+                "The signal object of a SignalFuture was freed while the future was still waiting for the signal to be emitted!"
+            ),
+        }
+    }
+}
+
+// Not derived, otherwise an extra bound `Output: Default` is required.
+struct SignalFutureData<T> {
+    state: TrySignalFutureState<T>,
+    waker: Option<Waker>,
+}
+
+impl<T> Default for SignalFutureData<T> {
+    fn default() -> Self {
+        Self {
+            state: Default::default(),
+            waker: None,
+        }
+    }
+}
+
+// Only public for itest.
+#[cfg_attr(feature = "trace", derive(Default))]
+pub struct SignalFutureResolver<R> {
+    data: Arc<Mutex<SignalFutureData<R>>>,
+}
+
+impl<R> Clone for SignalFutureResolver<R> {
+    fn clone(&self) -> Self {
+        Self {
+            data: self.data.clone(),
+        }
+    }
+}
+
+impl<R> SignalFutureResolver<R> {
+    fn new(data: Arc<Mutex<SignalFutureData<R>>>) -> Self {
+        Self { data }
+    }
+}
+
+impl<R> std::hash::Hash for SignalFutureResolver<R> {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        state.write_usize(Arc::as_ptr(&self.data) as usize);
+    }
+}
+
+impl<R> PartialEq for SignalFutureResolver<R> {
+    fn eq(&self, other: &Self) -> bool {
+        Arc::ptr_eq(&self.data, &other.data)
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> RustCallable for SignalFutureResolver<R> {
+    fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+        let mut data = self.data.lock().unwrap();
+        // We no loger need the waker after we resolved. If the future get's polled again we also get a new waker.
+        let waker = data.waker.take();
+
+        data.state = TrySignalFutureState::Ready(R::from_variant_array(args));
+        drop(data);
+
+        if let Some(waker) = waker {
+            waker.wake();
+        }
+
+        Ok(Variant::nil())
+    }
+}
+
+impl<R> Display for SignalFutureResolver<R> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "GuaranteedSignalFutureResolver::<{}>",
+            std::any::type_name::<R>()
+        )
+    }
+}
+
+// this resolver will resolve the future when it's being dropped (i.e. the engine removes all connected signal callables). This is very unusual.
+impl<R> Drop for SignalFutureResolver<R> {
+    fn drop(&mut self) {
+        let mut data = self.data.lock().unwrap();
+
+        if !matches!(data.state, TrySignalFutureState::Pending) {
+            return;
+        }
+
+        data.state = TrySignalFutureState::Dead;
+
+        if let Some(ref waker) = data.waker {
+            waker.wake_by_ref();
+        }
path: godot-core/src/builtin/signal/futures.rs
user: login: Bromeon
body: Some comments would really help here. There is a lot of implicit knowledge that an outsider (like me) doesn't have :wink: 
created_at: 2025-03-02T21:03:12Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,313 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use core::panic;
+use std::fmt::Display;
+use std::future::{Future, IntoFuture};
+use std::pin::Pin;
+use std::sync::{Arc, Mutex};
+use std::task::{Context, Poll, Waker};
+
+use crate::builtin::{Callable, RustCallable, Variant};
+use crate::classes::object::ConnectFlags;
+use crate::meta::ParamTuple;
+use crate::obj::{EngineBitfield, WithBaseField};
+use crate::registry::signal::TypedSignal;
+
+use super::Signal;
+
+/// The panicking counter part to the [`TrySignalFuture`].
+///
+/// This future works in the same ways as the TrySignalFuture, but panics when the signal object is freed instead of resolving to an
+/// [`Result::Err`]
+pub struct SignalFuture<R: ParamTuple + Sync + Send>(TrySignalFuture<R>);
+
+impl<R: ParamTuple + Sync + Send> SignalFuture<R> {
+    fn new(signal: Signal) -> Self {
+        Self(TrySignalFuture::new(signal))
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> Future for SignalFuture<R> {
+    type Output = R;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        let poll_result = self.get_mut().0.poll(cx);
+
+        match poll_result {
+            Poll::Pending => Poll::Pending,
+            Poll::Ready(Ok(value)) => Poll::Ready(value),
+            Poll::Ready(Err(TrySignalFutureError)) => panic!(
+                "The signal object of a SignalFuture was freed while the future was still waiting for the signal to be emitted!"
+            ),
+        }
+    }
+}
+
+// Not derived, otherwise an extra bound `Output: Default` is required.
+struct SignalFutureData<T> {
+    state: TrySignalFutureState<T>,
+    waker: Option<Waker>,
+}
+
+impl<T> Default for SignalFutureData<T> {
+    fn default() -> Self {
+        Self {
+            state: Default::default(),
+            waker: None,
+        }
+    }
+}
+
+// Only public for itest.
+#[cfg_attr(feature = "trace", derive(Default))]
+pub struct SignalFutureResolver<R> {
+    data: Arc<Mutex<SignalFutureData<R>>>,
+}
+
+impl<R> Clone for SignalFutureResolver<R> {
+    fn clone(&self) -> Self {
+        Self {
+            data: self.data.clone(),
+        }
+    }
+}
+
+impl<R> SignalFutureResolver<R> {
+    fn new(data: Arc<Mutex<SignalFutureData<R>>>) -> Self {
+        Self { data }
+    }
+}
+
+impl<R> std::hash::Hash for SignalFutureResolver<R> {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        state.write_usize(Arc::as_ptr(&self.data) as usize);
+    }
+}
+
+impl<R> PartialEq for SignalFutureResolver<R> {
+    fn eq(&self, other: &Self) -> bool {
+        Arc::ptr_eq(&self.data, &other.data)
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> RustCallable for SignalFutureResolver<R> {
+    fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+        let mut data = self.data.lock().unwrap();
+        // We no loger need the waker after we resolved. If the future get's polled again we also get a new waker.
+        let waker = data.waker.take();
+
+        data.state = TrySignalFutureState::Ready(R::from_variant_array(args));
+        drop(data);
+
+        if let Some(waker) = waker {
+            waker.wake();
+        }
+
+        Ok(Variant::nil())
+    }
+}
+
+impl<R> Display for SignalFutureResolver<R> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "GuaranteedSignalFutureResolver::<{}>",
+            std::any::type_name::<R>()
+        )
+    }
+}
+
+// this resolver will resolve the future when it's being dropped (i.e. the engine removes all connected signal callables). This is very unusual.
path: godot-core/src/builtin/signal/futures.rs
user: login: Bromeon
body: ```suggestion
// This resolver will resolve the future when it's being dropped (i.e. the engine removes all connected signal callables). This is very unusual.
```

"Unusual" as in "unexpected" (i.e. bad), or what does that mean?
created_at: 2025-03-02T21:03:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,313 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use core::panic;
+use std::fmt::Display;
+use std::future::{Future, IntoFuture};
+use std::pin::Pin;
+use std::sync::{Arc, Mutex};
+use std::task::{Context, Poll, Waker};
+
+use crate::builtin::{Callable, RustCallable, Variant};
+use crate::classes::object::ConnectFlags;
+use crate::meta::ParamTuple;
+use crate::obj::{EngineBitfield, WithBaseField};
+use crate::registry::signal::TypedSignal;
+
+use super::Signal;
+
+/// The panicking counter part to the [`TrySignalFuture`].
+///
+/// This future works in the same ways as the TrySignalFuture, but panics when the signal object is freed instead of resolving to an
+/// [`Result::Err`]
+pub struct SignalFuture<R: ParamTuple + Sync + Send>(TrySignalFuture<R>);
+
+impl<R: ParamTuple + Sync + Send> SignalFuture<R> {
+    fn new(signal: Signal) -> Self {
+        Self(TrySignalFuture::new(signal))
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> Future for SignalFuture<R> {
+    type Output = R;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        let poll_result = self.get_mut().0.poll(cx);
+
+        match poll_result {
+            Poll::Pending => Poll::Pending,
+            Poll::Ready(Ok(value)) => Poll::Ready(value),
+            Poll::Ready(Err(TrySignalFutureError)) => panic!(
+                "The signal object of a SignalFuture was freed while the future was still waiting for the signal to be emitted!"
+            ),
+        }
+    }
+}
+
+// Not derived, otherwise an extra bound `Output: Default` is required.
+struct SignalFutureData<T> {
+    state: TrySignalFutureState<T>,
+    waker: Option<Waker>,
+}
+
+impl<T> Default for SignalFutureData<T> {
+    fn default() -> Self {
+        Self {
+            state: Default::default(),
+            waker: None,
+        }
+    }
+}
+
+// Only public for itest.
+#[cfg_attr(feature = "trace", derive(Default))]
+pub struct SignalFutureResolver<R> {
+    data: Arc<Mutex<SignalFutureData<R>>>,
+}
+
+impl<R> Clone for SignalFutureResolver<R> {
+    fn clone(&self) -> Self {
+        Self {
+            data: self.data.clone(),
+        }
+    }
+}
+
+impl<R> SignalFutureResolver<R> {
+    fn new(data: Arc<Mutex<SignalFutureData<R>>>) -> Self {
+        Self { data }
+    }
+}
+
+impl<R> std::hash::Hash for SignalFutureResolver<R> {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        state.write_usize(Arc::as_ptr(&self.data) as usize);
+    }
+}
+
+impl<R> PartialEq for SignalFutureResolver<R> {
+    fn eq(&self, other: &Self) -> bool {
+        Arc::ptr_eq(&self.data, &other.data)
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> RustCallable for SignalFutureResolver<R> {
+    fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+        let mut data = self.data.lock().unwrap();
+        // We no loger need the waker after we resolved. If the future get's polled again we also get a new waker.
+        let waker = data.waker.take();
+
+        data.state = TrySignalFutureState::Ready(R::from_variant_array(args));
+        drop(data);
+
+        if let Some(waker) = waker {
+            waker.wake();
+        }
+
+        Ok(Variant::nil())
+    }
+}
+
+impl<R> Display for SignalFutureResolver<R> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "GuaranteedSignalFutureResolver::<{}>",
+            std::any::type_name::<R>()
+        )
+    }
+}
+
+// this resolver will resolve the future when it's being dropped (i.e. the engine removes all connected signal callables). This is very unusual.
+impl<R> Drop for SignalFutureResolver<R> {
+    fn drop(&mut self) {
+        let mut data = self.data.lock().unwrap();
+
+        if !matches!(data.state, TrySignalFutureState::Pending) {
+            return;
+        }
+
+        data.state = TrySignalFutureState::Dead;
+
+        if let Some(ref waker) = data.waker {
+            waker.wake_by_ref();
+        }
+    }
+}
+
+#[derive(Default)]
+enum TrySignalFutureState<T> {
+    #[default]
+    Pending,
+    Ready(T),
+    Dead,
+    Dropped,
+}
+
+impl<T> TrySignalFutureState<T> {
+    fn take(&mut self) -> Self {
+        let new_value = match self {
+            Self::Pending => Self::Pending,
+            Self::Ready(_) | Self::Dead => Self::Dead,
+            Self::Dropped => Self::Dropped,
+        };
+
+        std::mem::replace(self, new_value)
+    }
+}
+
+/// A future that tries to resolve as soon as the provided Godot signal was emitted.
+///
+/// The future might resolve to an error if the signal object is freed before the signal is emitted.
+pub struct TrySignalFuture<R: ParamTuple + Sync + Send> {
+    data: Arc<Mutex<SignalFutureData<R>>>,
+    callable: SignalFutureResolver<R>,
+    signal: Signal,
+}
+
+impl<R: ParamTuple + Sync + Send> TrySignalFuture<R> {
path: godot-core/src/builtin/signal/futures.rs
user: login: Bromeon
body: You're not always consistent with the generic argument, sometimes it's called `R` and sometimes `Ps`.

What does it represent exactly, which identifier is better fitted? It can also help to mention it in the RustDoc of the struct.
created_at: 2025-03-02T21:07:45Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,313 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use core::panic;
+use std::fmt::Display;
+use std::future::{Future, IntoFuture};
+use std::pin::Pin;
+use std::sync::{Arc, Mutex};
+use std::task::{Context, Poll, Waker};
+
+use crate::builtin::{Callable, RustCallable, Variant};
+use crate::classes::object::ConnectFlags;
+use crate::meta::ParamTuple;
+use crate::obj::{EngineBitfield, WithBaseField};
+use crate::registry::signal::TypedSignal;
+
+use super::Signal;
+
+/// The panicking counter part to the [`TrySignalFuture`].
+///
+/// This future works in the same ways as the TrySignalFuture, but panics when the signal object is freed instead of resolving to an
+/// [`Result::Err`]
+pub struct SignalFuture<R: ParamTuple + Sync + Send>(TrySignalFuture<R>);
+
+impl<R: ParamTuple + Sync + Send> SignalFuture<R> {
+    fn new(signal: Signal) -> Self {
+        Self(TrySignalFuture::new(signal))
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> Future for SignalFuture<R> {
+    type Output = R;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        let poll_result = self.get_mut().0.poll(cx);
+
+        match poll_result {
+            Poll::Pending => Poll::Pending,
+            Poll::Ready(Ok(value)) => Poll::Ready(value),
+            Poll::Ready(Err(TrySignalFutureError)) => panic!(
+                "The signal object of a SignalFuture was freed while the future was still waiting for the signal to be emitted!"
+            ),
+        }
+    }
+}
+
+// Not derived, otherwise an extra bound `Output: Default` is required.
+struct SignalFutureData<T> {
+    state: TrySignalFutureState<T>,
+    waker: Option<Waker>,
+}
+
+impl<T> Default for SignalFutureData<T> {
+    fn default() -> Self {
+        Self {
+            state: Default::default(),
+            waker: None,
+        }
+    }
+}
+
+// Only public for itest.
+#[cfg_attr(feature = "trace", derive(Default))]
+pub struct SignalFutureResolver<R> {
+    data: Arc<Mutex<SignalFutureData<R>>>,
+}
+
+impl<R> Clone for SignalFutureResolver<R> {
+    fn clone(&self) -> Self {
+        Self {
+            data: self.data.clone(),
+        }
+    }
+}
+
+impl<R> SignalFutureResolver<R> {
+    fn new(data: Arc<Mutex<SignalFutureData<R>>>) -> Self {
+        Self { data }
+    }
+}
+
+impl<R> std::hash::Hash for SignalFutureResolver<R> {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        state.write_usize(Arc::as_ptr(&self.data) as usize);
+    }
+}
+
+impl<R> PartialEq for SignalFutureResolver<R> {
+    fn eq(&self, other: &Self) -> bool {
+        Arc::ptr_eq(&self.data, &other.data)
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> RustCallable for SignalFutureResolver<R> {
+    fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+        let mut data = self.data.lock().unwrap();
+        // We no loger need the waker after we resolved. If the future get's polled again we also get a new waker.
+        let waker = data.waker.take();
+
+        data.state = TrySignalFutureState::Ready(R::from_variant_array(args));
+        drop(data);
+
+        if let Some(waker) = waker {
+            waker.wake();
+        }
+
+        Ok(Variant::nil())
+    }
+}
+
+impl<R> Display for SignalFutureResolver<R> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "GuaranteedSignalFutureResolver::<{}>",
+            std::any::type_name::<R>()
+        )
+    }
+}
+
+// this resolver will resolve the future when it's being dropped (i.e. the engine removes all connected signal callables). This is very unusual.
+impl<R> Drop for SignalFutureResolver<R> {
+    fn drop(&mut self) {
+        let mut data = self.data.lock().unwrap();
+
+        if !matches!(data.state, TrySignalFutureState::Pending) {
+            return;
+        }
+
+        data.state = TrySignalFutureState::Dead;
+
+        if let Some(ref waker) = data.waker {
+            waker.wake_by_ref();
+        }
+    }
+}
+
+#[derive(Default)]
+enum TrySignalFutureState<T> {
+    #[default]
+    Pending,
+    Ready(T),
+    Dead,
+    Dropped,
+}
+
+impl<T> TrySignalFutureState<T> {
+    fn take(&mut self) -> Self {
+        let new_value = match self {
+            Self::Pending => Self::Pending,
+            Self::Ready(_) | Self::Dead => Self::Dead,
+            Self::Dropped => Self::Dropped,
+        };
+
+        std::mem::replace(self, new_value)
+    }
+}
+
+/// A future that tries to resolve as soon as the provided Godot signal was emitted.
+///
+/// The future might resolve to an error if the signal object is freed before the signal is emitted.
+pub struct TrySignalFuture<R: ParamTuple + Sync + Send> {
+    data: Arc<Mutex<SignalFutureData<R>>>,
+    callable: SignalFutureResolver<R>,
+    signal: Signal,
+}
+
+impl<R: ParamTuple + Sync + Send> TrySignalFuture<R> {
+    fn new(signal: Signal) -> Self {
+        debug_assert!(
+            !signal.is_null(),
+            "Failed to create a future for an invalid Signal!\nEither the signal object was already freed or the signal was not registered in the object before using it.",
+        );
+
+        let data = Arc::new(Mutex::new(SignalFutureData::default()));
+
+        // The callable currently requires that the return value is Sync + Send.
+        let callable = SignalFutureResolver::new(data.clone());
+
+        signal.connect(
+            &Callable::from_custom(callable.clone()),
+            ConnectFlags::ONE_SHOT.ord() as i64,
+        );
+
+        Self {
+            data,
+            callable,
+            signal,
+        }
+    }
+    fn poll(&mut self, cx: &mut Context<'_>) -> Poll<Result<R, TrySignalFutureError>> {
+        let mut data = self.data.lock().unwrap();
+
+        data.waker.replace(cx.waker().clone());
+
+        let value = data.state.take();
+
+        match value {
+            TrySignalFutureState::Pending => Poll::Pending,
+            TrySignalFutureState::Dropped => unreachable!(),
+            TrySignalFutureState::Dead => Poll::Ready(Err(TrySignalFutureError)),
+            TrySignalFutureState::Ready(value) => Poll::Ready(Ok(value)),
+        }
+    }
+}
+
+pub struct TrySignalFutureError;
+
+impl<R: ParamTuple + Sync + Send> Future for TrySignalFuture<R> {
+    type Output = Result<R, TrySignalFutureError>;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        self.get_mut().poll(cx)
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> Drop for TrySignalFuture<R> {
+    fn drop(&mut self) {
+        // The callable might alredy be destroyed, this occurs during engine shutdown.
+        if self.signal.object().is_none() {
+            return;
+        }
+
+        let mut data = self.data.lock().unwrap();
+
+        data.state = TrySignalFutureState::Dropped;
+
+        drop(data);
+
+        let gd_callable = Callable::from_custom(self.callable.clone());
+
+        if self.signal.is_connected(&gd_callable) {
+            self.signal.disconnect(&gd_callable);
+        }
+    }
+}
+
+impl Signal {
+    /// Creates a fallible future for this signal.
+    ///
+    /// The future will resolve the next time the signal is emitted.
+    /// See [`TrySignalFuture`] for details.
+    ///
+    /// Since the `Signal` type does not contain information on the signal argument types, the future output type has to be inferred from
+    /// the call to this function.
+    pub fn to_try_future<R: ParamTuple + Sync + Send>(&self) -> TrySignalFuture<R> {
+        TrySignalFuture::new(self.clone())
+    }
+
+    /// Creates a future for this `Signal`.
+    ///
+    /// The future will resolve the next time the `Signal` is emitted, but might panic if the signal object is freed.
+    /// See [`SignalFuture`] for details.
+    ///
+    /// Since the `Signal` type does not contain information on the signal argument types, the future output type has to be inferred from
+    /// the call to this function.
+    pub fn to_future<R: ParamTuple + Sync + Send>(&self) -> SignalFuture<R> {
+        SignalFuture::new(self.clone())
+    }
+}
+
+impl<C: WithBaseField, Ps: ParamTuple + Sync + Send> TypedSignal<'_, C, Ps> {
+    /// Creates a fallible future for this signal.
+    ///
+    /// The future will resolve the next time the signal is emitted.
+    /// See [`TrySignalFuture`] for details.
+    pub fn to_try_future(&self) -> TrySignalFuture<Ps> {
+        TrySignalFuture::new(self.to_untyped())
+    }
+
+    /// Creates a future for this `Signal`.
path: godot-core/src/builtin/signal/futures.rs
user: login: Bromeon
body: ```suggestion
    /// Creates a future for this signal.
```
created_at: 2025-03-02T21:08:20Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,111 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::ops::Deref;
+
+use godot::builtin::{godot_task, Callable, Signal, SignalFuture, SignalFutureResolver, Variant};
+use godot::classes::{Object, RefCounted};
+use godot::meta::ToGodot;
+use godot::obj::{Base, Gd, NewAlloc, NewGd};
+use godot::prelude::{godot_api, GodotClass};
+
+use crate::framework::{itest, TestContext};
+
+#[derive(GodotClass)]
+#[class(init)]
+struct AsyncRefCounted {
+    base: Base<RefCounted>,
+}
+
+#[godot_api]
+impl AsyncRefCounted {
+    #[signal]
+    fn custom_signal(value: u32);
+}
+
+#[itest(async)]
+fn start_async_task() -> TaskHandle {
+    let mut object = RefCounted::new_gd();
+    let object_ref = object.clone();
+    let signal = Signal::from_object_signal(&object, "custom_signal");
+
+    object.add_user_signal("custom_signal");
+
+    let task_handle = godot_task(async move {
+        let signal_future: SignalFuture<(u8,)> = signal.to_future();
+        let (result,) = signal_future.await;
+
+        assert_eq!(result, 10);
+        drop(object_ref);
+    });
+
+    object.emit_signal("custom_signal", &[10.to_variant()]);
+
+    task_handle
+}
+
+#[itest]
+fn cancel_async_task(ctx: &TestContext) {
+    let tree = ctx.scene_tree.get_tree().unwrap();
+    let signal = Signal::from_object_signal(&tree, "process_frame");
+
+    let handle = godot_task(async move {
+        let _: () = signal.to_future().await;
+
+        unreachable!();
+    });
+
+    handle.cancel();
+}
+
+#[itest(async)]
+fn async_task_guaranteed_signal_future() -> TaskHandle {
+    let mut obj = Object::new_alloc();
+
+    let signal = Signal::from_object_signal(&obj, "script_changed");
+
+    let handle = godot_task(async move {
+        let result = signal.to_try_future::<()>().await;
+
+        assert!(result.is_err());
+    });
+
+    obj.call_deferred("free", &[]);
+
+    handle
+}
+
+// Test that two callables created from the same future resolver (but cloned) are equal, while they are not equal to an unrelated
+// callable.
+#[itest]
+fn resolver_callabable_equality() {
+    let resolver = SignalFutureResolver::<(u8,)>::default();
+
+    let callable = Callable::from_custom(resolver.clone());
+    let cloned_callable = Callable::from_custom(resolver.clone());
+    let unrelated_callable = Callable::from_local_fn("fn", |_| Ok(Variant::nil()));
path: itest/rust/src/engine_tests/async_test.rs
user: login: Bromeon
body: ```suggestion
    let unrelated_callable = Callable::from_local_fn("unrelated", |_| Ok(Variant::nil()));
```
created_at: 2025-03-02T21:09:53Z
author_association: MEMBER
diff_hunk: @@ -191,36 +318,36 @@ impl IntegrationTests {
                 }
             };
 
-            self.update_stats(&outcome, &test_file, &test_case);
+            Self::update_stats(&mut self.stats, &outcome, &test_file, &test_case);
path: itest/rust/src/framework/runner.rs
user: login: Bromeon
body: Why this change, is this due to some partial borrowing shenanigans?
created_at: 2025-03-02T21:11:50Z
author_association: MEMBER
diff_hunk: @@ -191,36 +318,36 @@ impl IntegrationTests {
                 }
             };
 
-            self.update_stats(&outcome, &test_file, &test_case);
+            Self::update_stats(&mut self.stats, &outcome, &test_file, &test_case);
path: itest/rust/src/framework/runner.rs
user: login: TitanNano
body: Yes `update_stats` can't access `&mut self` anymore in the async setup, but in both cases we can just pass in the stats' struct.
created_at: 2025-03-02T21:47:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,409 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_try_future`](crate::builtin::Signal::to_try_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+/// Will be called during engine shudown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.take();
+}
+
+#[cfg(feature = "trace")]
+pub fn is_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_borrow(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+#[derive(Default)]
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
path: godot-core/src/builtin/async_runtime.rs
user: login: TitanNano
body: It's currently used in `AsyncRuntime::get_task` which does a `unwrap_or_default` when trying to access an index that is out of bounds.
created_at: 2025-03-02T21:56:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,409 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_try_future`](crate::builtin::Signal::to_try_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+/// Will be called during engine shudown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.take();
+}
+
+#[cfg(feature = "trace")]
+pub fn is_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_borrow(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+#[derive(Default)]
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
path: godot-core/src/builtin/async_runtime.rs
user: login: TitanNano
body: > Does this require certain state(s) in the FSM to be active now, i.e. a check like you did in park?

Not really because the task could either be being polled, and then cleared because it resolved or panicked, or it is just being canceled. There is one edge-case for when the task gets canceled while it's being polled, but this currently not possible as there is no way to get access to the `TaskHandle` inside the related task. If it occurred, this would cause a panic in the `FutureSlot::park` function when trying to park after polling is done.

> "Clear" and "park" are more or less standard terminology in this context of async runtimes?

I took these terms more from general task scheduling terminology. If you have other preferences, let me know. Terminology around futures is often sleeping, waking, pending and polling.
created_at: 2025-03-02T22:17:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,409 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_try_future`](crate::builtin::Signal::to_try_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+/// Will be called during engine shudown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.take();
+}
+
+#[cfg(feature = "trace")]
+pub fn is_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_borrow(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+#[derive(Default)]
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    /// Attempts to extract the future with the given ID from the slot.
+    ///
+    /// Puts the slot into [`FutureSlotState::Polling`] state after taking the future out. It is expected that the future is either parked
+    /// again or the slot is cleared.
+    fn take(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
path: godot-core/src/builtin/async_runtime.rs
user: login: TitanNano
body: I felt like `take` was close enough without being too long. Something like `take_to_poll()` would be more descriptive but quite wordy. `poll_future` implies that it will call `Future::poll` which is not the case...
created_at: 2025-03-02T22:24:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,409 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_try_future`](crate::builtin::Signal::to_try_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+/// Will be called during engine shudown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.take();
+}
+
+#[cfg(feature = "trace")]
+pub fn is_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_borrow(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+#[derive(Default)]
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    /// Attempts to extract the future with the given ID from the slot.
+    ///
+    /// Puts the slot into [`FutureSlotState::Polling`] state after taking the future out. It is expected that the future is either parked
+    /// again or the slot is cleared.
+    fn take(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
+
+    /// Parks the future in this slot again.
+    ///
+    /// # Panics
+    /// - If the slot is not in state [`FutureSlotState::Polling`].
+    fn park(&mut self, value: T) {
+        match self.value {
+            FutureSlotState::Empty | FutureSlotState::Gone => {
+                panic!("Future slot is currently unoccupied, future can not be parked here!");
+            }
+            FutureSlotState::Pending(_) => {
+                panic!("Future slot is already occupied by a different future!")
+            }
+            FutureSlotState::Polling => {
+                self.value = FutureSlotState::Pending(value);
+            }
+        }
+    }
+}
+
+#[derive(Default)]
+/// The storage for the pending tasks of the async runtime.
+struct AsyncRuntime {
+    tasks: Vec<FutureSlot<Pin<Box<dyn Future<Output = ()>>>>>,
+    task_counter: u64,
+    #[cfg(feature = "trace")]
+    panicked_tasks: std::collections::HashSet<u64>,
+}
+
+impl AsyncRuntime {
+    fn new() -> Self {
+        Self {
+            tasks: Vec::with_capacity(10),
+            task_counter: 0,
+            #[cfg(feature = "trace")]
+            panicked_tasks: std::collections::HashSet::default(),
+        }
+    }
+
+    /// Get the next task ID.
+    fn next_id(&mut self) -> u64 {
+        let id = self.task_counter;
+        self.task_counter += 1;
+        id
+    }
+
+    /// Store a new async task in the runtime.
+    ///
+    /// First, a linear search is performed to locate an already existing but currently unoccupied slot in the task buffer. If there is no
+    /// free slot, a new slot is added which may grow the underlying [`Vec`].
+    ///
+    /// The future storage always starts out with a capacity of 10 tasks.
+    fn add_task<F: Future<Output = ()> + 'static>(&mut self, future: F) -> TaskHandle {
+        let id = self.next_id();
+        let index_slot = self
+            .tasks
+            .iter_mut()
+            .enumerate()
+            .find(|(_, slot)| slot.is_empty());
+
+        let boxed = Box::pin(future);
+
+        let index = match index_slot {
+            Some((index, slot)) => {
+                *slot = FutureSlot::pending(id, boxed);
+                index
+            }
+            None => {
+                self.tasks.push(FutureSlot::pending(id, boxed));
+                self.tasks.len() - 1
+            }
+        };
+
+        TaskHandle::new(index, id)
+    }
+
+    /// Extract a pending task from the storage.
+    ///
+    /// Attempts to extract a future with the given ID from the specified index and leaves the slot in state [`FutureSlotState::Polling`].
+    fn get_task(
+        &mut self,
+        index: usize,
+        id: u64,
+    ) -> FutureSlotState<Pin<Box<dyn Future<Output = ()> + 'static>>> {
+        let slot = self.tasks.get_mut(index);
+
+        slot.map(|inner| inner.take(id)).unwrap_or_default()
+    }
+
+    /// Remove a future from the storage and free up its slot.
+    ///
+    /// The slot is left in the [`FutureSlotState::Gone`] state.
+    fn clear_task(&mut self, index: usize) {
+        self.tasks[index].clear();
+    }
+
+    /// Move a future back into its slot.
+    ///
+    /// # Panic
+    /// - If the underlying slot is not in the [`FutureSlotState::Polling`] state.
+    fn park_task(&mut self, index: usize, future: Pin<Box<dyn Future<Output = ()>>>) {
+        self.tasks[index].park(future);
+    }
+
+    /// Track that a future caused a panic.
+    ///
+    /// This is only available for itest.
+    #[cfg(feature = "trace")]
+    fn track_panic(&mut self, task_id: u64) {
+        self.panicked_tasks.insert(task_id);
+    }
+}
+
+/// Use a godot waker to poll it's associated future.
+///
+/// # Panics
+/// - If called from a thread other than the main-thread.
+fn poll_future(godot_waker: Arc<GodotWaker>) {
+    let current_thread = thread::current().id();
+
+    assert_eq!(
+                godot_waker.thread_id,
+                current_thread,
+                "trying to poll future on a different thread!\n  Current thread: {:?}\n  Future thread: {:?}",
+                current_thread,
+                godot_waker.thread_id,
+            );
+
+    let waker = Waker::from(godot_waker.clone());
+    let mut ctx = Context::from_waker(&waker);
+
+    // Take future out of the runtime.
+    let future = ASYNC_RUNTIME.with_borrow_mut(|rt| {
+        match rt.get_task(godot_waker.runtime_index, godot_waker.task_id) {
+            FutureSlotState::Empty => {
+                panic!("Future slot is empty when waking it! This is a bug!");
+            }
+
+            FutureSlotState::Gone => None,
+
+            FutureSlotState::Polling => {
+                panic!("The same GodotWaker has been called recursively, this is not expected!");
+            }
+
+            FutureSlotState::Pending(future) => Some(future),
+        }
+    });
+
+    let Some(future) = future else {
+        // Future has been canceled while the waker was already triggered.
+        return;
+    };
+
+    let error_context = || "Godot async task failed";
+    let mut future = AssertUnwindSafe(future);
path: godot-core/src/builtin/async_runtime.rs
user: login: TitanNano
body: I think the future should always be unwind safe as it is dropped and never used again if `Future::poll` panics. Same goes for the `Context`.
created_at: 2025-03-02T23:34:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,313 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use core::panic;
+use std::fmt::Display;
+use std::future::{Future, IntoFuture};
+use std::pin::Pin;
+use std::sync::{Arc, Mutex};
+use std::task::{Context, Poll, Waker};
+
+use crate::builtin::{Callable, RustCallable, Variant};
+use crate::classes::object::ConnectFlags;
+use crate::meta::ParamTuple;
+use crate::obj::{EngineBitfield, WithBaseField};
+use crate::registry::signal::TypedSignal;
+
+use super::Signal;
+
+/// The panicking counter part to the [`TrySignalFuture`].
+///
+/// This future works in the same ways as the TrySignalFuture, but panics when the signal object is freed instead of resolving to an
+/// [`Result::Err`]
+pub struct SignalFuture<R: ParamTuple + Sync + Send>(TrySignalFuture<R>);
+
+impl<R: ParamTuple + Sync + Send> SignalFuture<R> {
+    fn new(signal: Signal) -> Self {
+        Self(TrySignalFuture::new(signal))
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> Future for SignalFuture<R> {
+    type Output = R;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        let poll_result = self.get_mut().0.poll(cx);
+
+        match poll_result {
+            Poll::Pending => Poll::Pending,
+            Poll::Ready(Ok(value)) => Poll::Ready(value),
+            Poll::Ready(Err(TrySignalFutureError)) => panic!(
+                "The signal object of a SignalFuture was freed while the future was still waiting for the signal to be emitted!"
+            ),
+        }
+    }
+}
+
+// Not derived, otherwise an extra bound `Output: Default` is required.
+struct SignalFutureData<T> {
+    state: TrySignalFutureState<T>,
+    waker: Option<Waker>,
+}
+
+impl<T> Default for SignalFutureData<T> {
+    fn default() -> Self {
+        Self {
+            state: Default::default(),
+            waker: None,
+        }
+    }
+}
+
+// Only public for itest.
+#[cfg_attr(feature = "trace", derive(Default))]
+pub struct SignalFutureResolver<R> {
+    data: Arc<Mutex<SignalFutureData<R>>>,
+}
+
+impl<R> Clone for SignalFutureResolver<R> {
+    fn clone(&self) -> Self {
+        Self {
+            data: self.data.clone(),
+        }
+    }
+}
+
+impl<R> SignalFutureResolver<R> {
+    fn new(data: Arc<Mutex<SignalFutureData<R>>>) -> Self {
+        Self { data }
+    }
+}
+
+impl<R> std::hash::Hash for SignalFutureResolver<R> {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        state.write_usize(Arc::as_ptr(&self.data) as usize);
+    }
+}
+
+impl<R> PartialEq for SignalFutureResolver<R> {
+    fn eq(&self, other: &Self) -> bool {
+        Arc::ptr_eq(&self.data, &other.data)
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> RustCallable for SignalFutureResolver<R> {
+    fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+        let mut data = self.data.lock().unwrap();
+        // We no loger need the waker after we resolved. If the future get's polled again we also get a new waker.
+        let waker = data.waker.take();
+
+        data.state = TrySignalFutureState::Ready(R::from_variant_array(args));
+        drop(data);
+
+        if let Some(waker) = waker {
+            waker.wake();
+        }
+
+        Ok(Variant::nil())
+    }
+}
+
+impl<R> Display for SignalFutureResolver<R> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "GuaranteedSignalFutureResolver::<{}>",
+            std::any::type_name::<R>()
+        )
+    }
+}
+
+// this resolver will resolve the future when it's being dropped (i.e. the engine removes all connected signal callables). This is very unusual.
path: godot-core/src/builtin/signal/futures.rs
user: login: TitanNano
body: This was related to the unusual behavior of the `GuaranteedFuture` and shouldn't be here anymore. I'm removing it.
created_at: 2025-03-02T23:49:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,313 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use core::panic;
+use std::fmt::Display;
+use std::future::{Future, IntoFuture};
+use std::pin::Pin;
+use std::sync::{Arc, Mutex};
+use std::task::{Context, Poll, Waker};
+
+use crate::builtin::{Callable, RustCallable, Variant};
+use crate::classes::object::ConnectFlags;
+use crate::meta::ParamTuple;
+use crate::obj::{EngineBitfield, WithBaseField};
+use crate::registry::signal::TypedSignal;
+
+use super::Signal;
+
+/// The panicking counter part to the [`TrySignalFuture`].
+///
+/// This future works in the same ways as the TrySignalFuture, but panics when the signal object is freed instead of resolving to an
+/// [`Result::Err`]
+pub struct SignalFuture<R: ParamTuple + Sync + Send>(TrySignalFuture<R>);
+
+impl<R: ParamTuple + Sync + Send> SignalFuture<R> {
+    fn new(signal: Signal) -> Self {
+        Self(TrySignalFuture::new(signal))
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> Future for SignalFuture<R> {
+    type Output = R;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        let poll_result = self.get_mut().0.poll(cx);
+
+        match poll_result {
+            Poll::Pending => Poll::Pending,
+            Poll::Ready(Ok(value)) => Poll::Ready(value),
+            Poll::Ready(Err(TrySignalFutureError)) => panic!(
+                "The signal object of a SignalFuture was freed while the future was still waiting for the signal to be emitted!"
+            ),
+        }
+    }
+}
+
+// Not derived, otherwise an extra bound `Output: Default` is required.
+struct SignalFutureData<T> {
+    state: TrySignalFutureState<T>,
+    waker: Option<Waker>,
+}
+
+impl<T> Default for SignalFutureData<T> {
+    fn default() -> Self {
+        Self {
+            state: Default::default(),
+            waker: None,
+        }
+    }
+}
+
+// Only public for itest.
+#[cfg_attr(feature = "trace", derive(Default))]
+pub struct SignalFutureResolver<R> {
+    data: Arc<Mutex<SignalFutureData<R>>>,
+}
+
+impl<R> Clone for SignalFutureResolver<R> {
+    fn clone(&self) -> Self {
+        Self {
+            data: self.data.clone(),
+        }
+    }
+}
+
+impl<R> SignalFutureResolver<R> {
+    fn new(data: Arc<Mutex<SignalFutureData<R>>>) -> Self {
+        Self { data }
+    }
+}
+
+impl<R> std::hash::Hash for SignalFutureResolver<R> {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        state.write_usize(Arc::as_ptr(&self.data) as usize);
+    }
+}
+
+impl<R> PartialEq for SignalFutureResolver<R> {
+    fn eq(&self, other: &Self) -> bool {
+        Arc::ptr_eq(&self.data, &other.data)
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> RustCallable for SignalFutureResolver<R> {
+    fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+        let mut data = self.data.lock().unwrap();
+        // We no loger need the waker after we resolved. If the future get's polled again we also get a new waker.
+        let waker = data.waker.take();
+
+        data.state = TrySignalFutureState::Ready(R::from_variant_array(args));
+        drop(data);
+
+        if let Some(waker) = waker {
+            waker.wake();
+        }
+
+        Ok(Variant::nil())
+    }
+}
+
+impl<R> Display for SignalFutureResolver<R> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "GuaranteedSignalFutureResolver::<{}>",
+            std::any::type_name::<R>()
+        )
+    }
+}
+
+// this resolver will resolve the future when it's being dropped (i.e. the engine removes all connected signal callables). This is very unusual.
+impl<R> Drop for SignalFutureResolver<R> {
+    fn drop(&mut self) {
+        let mut data = self.data.lock().unwrap();
+
+        if !matches!(data.state, TrySignalFutureState::Pending) {
+            return;
+        }
+
+        data.state = TrySignalFutureState::Dead;
+
+        if let Some(ref waker) = data.waker {
+            waker.wake_by_ref();
+        }
+    }
+}
+
+#[derive(Default)]
+enum TrySignalFutureState<T> {
+    #[default]
+    Pending,
+    Ready(T),
+    Dead,
+    Dropped,
+}
+
+impl<T> TrySignalFutureState<T> {
+    fn take(&mut self) -> Self {
+        let new_value = match self {
+            Self::Pending => Self::Pending,
+            Self::Ready(_) | Self::Dead => Self::Dead,
+            Self::Dropped => Self::Dropped,
+        };
+
+        std::mem::replace(self, new_value)
+    }
+}
+
+/// A future that tries to resolve as soon as the provided Godot signal was emitted.
+///
+/// The future might resolve to an error if the signal object is freed before the signal is emitted.
+pub struct TrySignalFuture<R: ParamTuple + Sync + Send> {
+    data: Arc<Mutex<SignalFutureData<R>>>,
+    callable: SignalFutureResolver<R>,
+    signal: Signal,
+}
+
+impl<R: ParamTuple + Sync + Send> TrySignalFuture<R> {
path: godot-core/src/builtin/signal/futures.rs
user: login: TitanNano
body: Right, I think it should always be `R` that is what I initially used and stands for `Return`. `Ps` accidentally crept in when I adopted the `ParamTuple` from the `TypedSignal`.
created_at: 2025-03-02T23:51:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,409 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_try_future`](crate::builtin::Signal::to_try_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+/// Will be called during engine shudown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.take();
+}
+
+#[cfg(feature = "trace")]
+pub fn is_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_borrow(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+#[derive(Default)]
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
path: godot-core/src/builtin/async_runtime.rs
user: login: TitanNano
body: But I eliminated the need for the default implementation based on one of the later comments.
created_at: 2025-03-02T23:55:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,409 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_try_future`](crate::builtin::Signal::to_try_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+/// Will be called during engine shudown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.take();
+}
+
+#[cfg(feature = "trace")]
+pub fn is_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_borrow(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+#[derive(Default)]
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    /// Attempts to extract the future with the given ID from the slot.
+    ///
+    /// Puts the slot into [`FutureSlotState::Polling`] state after taking the future out. It is expected that the future is either parked
+    /// again or the slot is cleared.
+    fn take(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
path: godot-core/src/builtin/async_runtime.rs
user: login: TitanNano
body: Maybe `take_future`?
created_at: 2025-03-02T23:56:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,313 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use core::panic;
+use std::fmt::Display;
+use std::future::{Future, IntoFuture};
+use std::pin::Pin;
+use std::sync::{Arc, Mutex};
+use std::task::{Context, Poll, Waker};
+
+use crate::builtin::{Callable, RustCallable, Variant};
+use crate::classes::object::ConnectFlags;
+use crate::meta::ParamTuple;
+use crate::obj::{EngineBitfield, WithBaseField};
+use crate::registry::signal::TypedSignal;
+
+use super::Signal;
+
+/// The panicking counter part to the [`TrySignalFuture`].
+///
+/// This future works in the same ways as the TrySignalFuture, but panics when the signal object is freed instead of resolving to an
+/// [`Result::Err`]
+pub struct SignalFuture<R: ParamTuple + Sync + Send>(TrySignalFuture<R>);
+
+impl<R: ParamTuple + Sync + Send> SignalFuture<R> {
+    fn new(signal: Signal) -> Self {
+        Self(TrySignalFuture::new(signal))
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> Future for SignalFuture<R> {
+    type Output = R;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        let poll_result = self.get_mut().0.poll(cx);
+
+        match poll_result {
+            Poll::Pending => Poll::Pending,
+            Poll::Ready(Ok(value)) => Poll::Ready(value),
+            Poll::Ready(Err(TrySignalFutureError)) => panic!(
+                "The signal object of a SignalFuture was freed while the future was still waiting for the signal to be emitted!"
+            ),
+        }
+    }
+}
+
+// Not derived, otherwise an extra bound `Output: Default` is required.
+struct SignalFutureData<T> {
+    state: TrySignalFutureState<T>,
+    waker: Option<Waker>,
+}
+
+impl<T> Default for SignalFutureData<T> {
+    fn default() -> Self {
+        Self {
+            state: Default::default(),
+            waker: None,
+        }
+    }
+}
path: godot-core/src/builtin/signal/futures.rs
user: login: TitanNano
body: I very much dislike the name. The data is the shared data between the future and the resolver. This encompasses both the state enum `SignalFutureState` and the optional `Waker`.
created_at: 2025-03-03T00:00:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,409 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_try_future`](crate::builtin::Signal::to_try_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+/// Will be called during engine shudown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.take();
+}
+
+#[cfg(feature = "trace")]
+pub fn is_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_borrow(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+#[derive(Default)]
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: Thanks! If the terms are common enough, that's fine for me :+1: 
created_at: 2025-03-03T09:29:39Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,409 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_try_future`](crate::builtin::Signal::to_try_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+/// Will be called during engine shudown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.take();
+}
+
+#[cfg(feature = "trace")]
+pub fn is_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_borrow(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+#[derive(Default)]
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    /// Attempts to extract the future with the given ID from the slot.
+    ///
+    /// Puts the slot into [`FutureSlotState::Polling`] state after taking the future out. It is expected that the future is either parked
+    /// again or the slot is cleared.
+    fn take(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
+
+    /// Parks the future in this slot again.
+    ///
+    /// # Panics
+    /// - If the slot is not in state [`FutureSlotState::Polling`].
+    fn park(&mut self, value: T) {
+        match self.value {
+            FutureSlotState::Empty | FutureSlotState::Gone => {
+                panic!("Future slot is currently unoccupied, future can not be parked here!");
+            }
+            FutureSlotState::Pending(_) => {
+                panic!("Future slot is already occupied by a different future!")
+            }
+            FutureSlotState::Polling => {
+                self.value = FutureSlotState::Pending(value);
+            }
+        }
+    }
+}
+
+#[derive(Default)]
+/// The storage for the pending tasks of the async runtime.
+struct AsyncRuntime {
+    tasks: Vec<FutureSlot<Pin<Box<dyn Future<Output = ()>>>>>,
+    task_counter: u64,
+    #[cfg(feature = "trace")]
+    panicked_tasks: std::collections::HashSet<u64>,
+}
+
+impl AsyncRuntime {
+    fn new() -> Self {
+        Self {
+            tasks: Vec::with_capacity(10),
+            task_counter: 0,
+            #[cfg(feature = "trace")]
+            panicked_tasks: std::collections::HashSet::default(),
+        }
+    }
+
+    /// Get the next task ID.
+    fn next_id(&mut self) -> u64 {
+        let id = self.task_counter;
+        self.task_counter += 1;
+        id
+    }
+
+    /// Store a new async task in the runtime.
+    ///
+    /// First, a linear search is performed to locate an already existing but currently unoccupied slot in the task buffer. If there is no
+    /// free slot, a new slot is added which may grow the underlying [`Vec`].
+    ///
+    /// The future storage always starts out with a capacity of 10 tasks.
+    fn add_task<F: Future<Output = ()> + 'static>(&mut self, future: F) -> TaskHandle {
+        let id = self.next_id();
+        let index_slot = self
+            .tasks
+            .iter_mut()
+            .enumerate()
+            .find(|(_, slot)| slot.is_empty());
+
+        let boxed = Box::pin(future);
+
+        let index = match index_slot {
+            Some((index, slot)) => {
+                *slot = FutureSlot::pending(id, boxed);
+                index
+            }
+            None => {
+                self.tasks.push(FutureSlot::pending(id, boxed));
+                self.tasks.len() - 1
+            }
+        };
+
+        TaskHandle::new(index, id)
+    }
+
+    /// Extract a pending task from the storage.
+    ///
+    /// Attempts to extract a future with the given ID from the specified index and leaves the slot in state [`FutureSlotState::Polling`].
+    fn get_task(
+        &mut self,
+        index: usize,
+        id: u64,
+    ) -> FutureSlotState<Pin<Box<dyn Future<Output = ()> + 'static>>> {
+        let slot = self.tasks.get_mut(index);
+
+        slot.map(|inner| inner.take(id)).unwrap_or_default()
+    }
+
+    /// Remove a future from the storage and free up its slot.
+    ///
+    /// The slot is left in the [`FutureSlotState::Gone`] state.
+    fn clear_task(&mut self, index: usize) {
+        self.tasks[index].clear();
+    }
+
+    /// Move a future back into its slot.
+    ///
+    /// # Panic
+    /// - If the underlying slot is not in the [`FutureSlotState::Polling`] state.
+    fn park_task(&mut self, index: usize, future: Pin<Box<dyn Future<Output = ()>>>) {
+        self.tasks[index].park(future);
+    }
+
+    /// Track that a future caused a panic.
+    ///
+    /// This is only available for itest.
+    #[cfg(feature = "trace")]
+    fn track_panic(&mut self, task_id: u64) {
+        self.panicked_tasks.insert(task_id);
+    }
+}
+
+/// Use a godot waker to poll it's associated future.
+///
+/// # Panics
+/// - If called from a thread other than the main-thread.
+fn poll_future(godot_waker: Arc<GodotWaker>) {
+    let current_thread = thread::current().id();
+
+    assert_eq!(
+                godot_waker.thread_id,
+                current_thread,
+                "trying to poll future on a different thread!\n  Current thread: {:?}\n  Future thread: {:?}",
+                current_thread,
+                godot_waker.thread_id,
+            );
+
+    let waker = Waker::from(godot_waker.clone());
+    let mut ctx = Context::from_waker(&waker);
+
+    // Take future out of the runtime.
+    let future = ASYNC_RUNTIME.with_borrow_mut(|rt| {
+        match rt.get_task(godot_waker.runtime_index, godot_waker.task_id) {
+            FutureSlotState::Empty => {
+                panic!("Future slot is empty when waking it! This is a bug!");
+            }
+
+            FutureSlotState::Gone => None,
+
+            FutureSlotState::Polling => {
+                panic!("The same GodotWaker has been called recursively, this is not expected!");
+            }
+
+            FutureSlotState::Pending(future) => Some(future),
+        }
+    });
+
+    let Some(future) = future else {
+        // Future has been canceled while the waker was already triggered.
+        return;
+    };
+
+    let error_context = || "Godot async task failed";
+    let mut future = AssertUnwindSafe(future);
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: Thanks! Could you add this as a comment?
created_at: 2025-03-03T09:36:25Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,409 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_try_future`](crate::builtin::Signal::to_try_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+/// Will be called during engine shudown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.take();
+}
+
+#[cfg(feature = "trace")]
+pub fn is_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_borrow(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+#[derive(Default)]
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    /// Attempts to extract the future with the given ID from the slot.
+    ///
+    /// Puts the slot into [`FutureSlotState::Polling`] state after taking the future out. It is expected that the future is either parked
+    /// again or the slot is cleared.
+    fn take(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: "Wordy" shouldn't be a big issue, as this is used internally and isn't called in many places.

But now I read this comment:
> Puts the slot into [`FutureSlotState::Polling`] state after taking the future out. It is expected that the future is either parked again or the slot is cleared.

My first idea here was to have an API which "lends out" the future for a limited scope, and at the end decides whether it's parked or cleared. But I think that might be a bit too much over multiple call levels :thinking: 
```rs
rt.with_future(|fut| {
   fut.poll(); // do stuff

   true // if parked again; false if cleared
})
```


Otherwise, something like `take_to_poll` or `take_for_polling`?
created_at: 2025-03-03T10:27:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,318 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use core::panic;
+use std::fmt::Display;
+use std::future::{Future, IntoFuture};
+use std::pin::Pin;
+use std::sync::{Arc, Mutex};
+use std::task::{Context, Poll, Waker};
+
+use crate::builtin::{Callable, RustCallable, Variant};
+use crate::classes::object::ConnectFlags;
+use crate::meta::ParamTuple;
+use crate::obj::{EngineBitfield, WithBaseField};
+use crate::registry::signal::TypedSignal;
+
+use super::Signal;
+
+/// The panicking counter part to the [`TrySignalFuture`].
+///
+/// This future works in the same ways as the TrySignalFuture, but panics when the signal object is freed instead of resolving to an
+/// [`Result::Err`]
+pub struct SignalFuture<R: ParamTuple + Sync + Send>(TrySignalFuture<R>);
+
+impl<R: ParamTuple + Sync + Send> SignalFuture<R> {
+    fn new(signal: Signal) -> Self {
+        Self(TrySignalFuture::new(signal))
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> Future for SignalFuture<R> {
+    type Output = R;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        let poll_result = self.get_mut().0.poll(cx);
+
+        match poll_result {
+            Poll::Pending => Poll::Pending,
+            Poll::Ready(Ok(value)) => Poll::Ready(value),
+            Poll::Ready(Err(TrySignalFutureError)) => panic!(
+                "the signal object of a SignalFuture was freed, while the future was still waiting for the signal to be emitted"
+            ),
+        }
+    }
+}
+
+// Not derived, otherwise an extra bound `Output: Default` is required.
+struct SignalFutureData<T> {
+    state: TrySignalFutureState<T>,
+    waker: Option<Waker>,
+}
+
+impl<T> Default for SignalFutureData<T> {
+    fn default() -> Self {
+        Self {
+            state: Default::default(),
+            waker: None,
+        }
+    }
+}
+
+// Only public for itest.
+#[cfg_attr(feature = "trace", derive(Default))]
+pub struct SignalFutureResolver<R> {
+    data: Arc<Mutex<SignalFutureData<R>>>,
+}
+
+impl<R> Clone for SignalFutureResolver<R> {
+    fn clone(&self) -> Self {
+        Self {
+            data: self.data.clone(),
+        }
+    }
+}
+
+impl<R> SignalFutureResolver<R> {
+    fn new(data: Arc<Mutex<SignalFutureData<R>>>) -> Self {
+        Self { data }
+    }
+}
+
+impl<R> std::hash::Hash for SignalFutureResolver<R> {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        state.write_usize(Arc::as_ptr(&self.data) as usize);
+    }
+}
+
+impl<R> PartialEq for SignalFutureResolver<R> {
+    fn eq(&self, other: &Self) -> bool {
+        Arc::ptr_eq(&self.data, &other.data)
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> RustCallable for SignalFutureResolver<R> {
+    fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+        let mut data = self.data.lock().unwrap();
+        // We no longer need the waker after we resolved. If the future is polled again, we'll also get a new waker.
+        let waker = data.waker.take();
+
+        data.state = TrySignalFutureState::Ready(R::from_variant_array(args));
+        drop(data);
path: godot-core/src/builtin/signal/futures.rs
user: login: Bromeon
body: It took me a while to see that `drop(data)` doesn't drop the data, but the lock.
Maybe a local scope helps here:
```suggestion
        let mut waker = {
            let data = self.data.lock().unwrap();
            data.state = TrySignalFutureState::Ready(R::from_variant_array(args));
            
            // We no longer need the waker after we resolved. If the future is polled again, we'll also get a new waker.
            data.waker.take();
        }
```
created_at: 2025-03-03T12:28:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,318 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use core::panic;
+use std::fmt::Display;
+use std::future::{Future, IntoFuture};
+use std::pin::Pin;
+use std::sync::{Arc, Mutex};
+use std::task::{Context, Poll, Waker};
+
+use crate::builtin::{Callable, RustCallable, Variant};
+use crate::classes::object::ConnectFlags;
+use crate::meta::ParamTuple;
+use crate::obj::{EngineBitfield, WithBaseField};
+use crate::registry::signal::TypedSignal;
+
+use super::Signal;
+
+/// The panicking counter part to the [`TrySignalFuture`].
+///
+/// This future works in the same ways as the TrySignalFuture, but panics when the signal object is freed instead of resolving to an
+/// [`Result::Err`]
+pub struct SignalFuture<R: ParamTuple + Sync + Send>(TrySignalFuture<R>);
+
+impl<R: ParamTuple + Sync + Send> SignalFuture<R> {
+    fn new(signal: Signal) -> Self {
+        Self(TrySignalFuture::new(signal))
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> Future for SignalFuture<R> {
+    type Output = R;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        let poll_result = self.get_mut().0.poll(cx);
+
+        match poll_result {
+            Poll::Pending => Poll::Pending,
+            Poll::Ready(Ok(value)) => Poll::Ready(value),
+            Poll::Ready(Err(TrySignalFutureError)) => panic!(
+                "the signal object of a SignalFuture was freed, while the future was still waiting for the signal to be emitted"
+            ),
+        }
+    }
+}
+
+// Not derived, otherwise an extra bound `Output: Default` is required.
+struct SignalFutureData<T> {
+    state: TrySignalFutureState<T>,
+    waker: Option<Waker>,
+}
+
+impl<T> Default for SignalFutureData<T> {
+    fn default() -> Self {
+        Self {
+            state: Default::default(),
+            waker: None,
+        }
+    }
+}
+
+// Only public for itest.
+#[cfg_attr(feature = "trace", derive(Default))]
+pub struct SignalFutureResolver<R> {
+    data: Arc<Mutex<SignalFutureData<R>>>,
+}
+
+impl<R> Clone for SignalFutureResolver<R> {
+    fn clone(&self) -> Self {
+        Self {
+            data: self.data.clone(),
+        }
+    }
+}
+
+impl<R> SignalFutureResolver<R> {
+    fn new(data: Arc<Mutex<SignalFutureData<R>>>) -> Self {
+        Self { data }
+    }
+}
+
+impl<R> std::hash::Hash for SignalFutureResolver<R> {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        state.write_usize(Arc::as_ptr(&self.data) as usize);
+    }
+}
+
+impl<R> PartialEq for SignalFutureResolver<R> {
+    fn eq(&self, other: &Self) -> bool {
+        Arc::ptr_eq(&self.data, &other.data)
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> RustCallable for SignalFutureResolver<R> {
+    fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+        let mut data = self.data.lock().unwrap();
+        // We no longer need the waker after we resolved. If the future is polled again, we'll also get a new waker.
+        let waker = data.waker.take();
+
+        data.state = TrySignalFutureState::Ready(R::from_variant_array(args));
+        drop(data);
+
+        if let Some(waker) = waker {
+            waker.wake();
+        }
+
+        Ok(Variant::nil())
+    }
+}
+
+impl<R> Display for SignalFutureResolver<R> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "GuaranteedSignalFutureResolver::<{}>",
+            std::any::type_name::<R>()
+        )
+    }
+}
+
+// This resolver will change the futures state when it's being dropped (i.e. the engine removes all connected signal callables). By marking
+// the future as dead we can resolve it to an error value the next time it gets polled.
+impl<R> Drop for SignalFutureResolver<R> {
+    fn drop(&mut self) {
+        let mut data = self.data.lock().unwrap();
+
+        if !matches!(data.state, TrySignalFutureState::Pending) {
+            // The future is no longer pending, so no clean up is required.
+            return;
+        }
+
+        // We mark the future as dead, so the next time it gets polled we can react to it's inability to resolve.
+        data.state = TrySignalFutureState::Dead;
+
+        // If we got a waker we trigger it to get the future polled. If there is no waker, then the future has not been polled yet and we
+        // simply wait for the runtime to perform the first poll.
+        if let Some(ref waker) = data.waker {
+            waker.wake_by_ref();
+        }
+    }
+}
+
+#[derive(Default)]
+enum TrySignalFutureState<T> {
+    #[default]
+    Pending,
+    Ready(T),
+    Dead,
+    Dropped,
+}
+
+impl<T> TrySignalFutureState<T> {
+    fn take(&mut self) -> Self {
+        let new_value = match self {
+            Self::Pending => Self::Pending,
+            Self::Ready(_) | Self::Dead => Self::Dead,
+            Self::Dropped => Self::Dropped,
+        };
+
+        std::mem::replace(self, new_value)
+    }
+}
+
+/// A future that tries to resolve as soon as the provided Godot signal was emitted.
+///
+/// The future might resolve to an error if the signal object is freed before the signal is emitted.
+pub struct TrySignalFuture<R: ParamTuple + Sync + Send> {
+    data: Arc<Mutex<SignalFutureData<R>>>,
+    callable: SignalFutureResolver<R>,
+    signal: Signal,
+}
+
+impl<R: ParamTuple + Sync + Send> TrySignalFuture<R> {
+    fn new(signal: Signal) -> Self {
+        debug_assert!(
+            !signal.is_null(),
+            "Failed to create a future for an invalid Signal!\nEither the signal object was already freed or the signal was not registered in the object before using it.",
+        );
+
+        let data = Arc::new(Mutex::new(SignalFutureData::default()));
+
+        // The callable currently requires that the return value is Sync + Send.
+        let callable = SignalFutureResolver::new(data.clone());
+
+        signal.connect(
+            &Callable::from_custom(callable.clone()),
+            ConnectFlags::ONE_SHOT.ord() as i64,
+        );
+
+        Self {
+            data,
+            callable,
+            signal,
+        }
+    }
+    fn poll(&mut self, cx: &mut Context<'_>) -> Poll<Result<R, TrySignalFutureError>> {
+        let mut data = self.data.lock().unwrap();
+
+        data.waker.replace(cx.waker().clone());
+
+        let value = data.state.take();
+
+        match value {
+            TrySignalFutureState::Pending => Poll::Pending,
+            TrySignalFutureState::Dropped => unreachable!(),
+            TrySignalFutureState::Dead => Poll::Ready(Err(TrySignalFutureError)),
+            TrySignalFutureState::Ready(value) => Poll::Ready(Ok(value)),
+        }
+    }
+}
+
+pub struct TrySignalFutureError;
+
+impl<R: ParamTuple + Sync + Send> Future for TrySignalFuture<R> {
+    type Output = Result<R, TrySignalFutureError>;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        self.get_mut().poll(cx)
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> Drop for TrySignalFuture<R> {
+    fn drop(&mut self) {
+        // The callable might alredy be destroyed, this occurs during engine shutdown.
+        if self.signal.object().is_none() {
+            return;
+        }
+
+        let mut data = self.data.lock().unwrap();
+
+        data.state = TrySignalFutureState::Dropped;
+
+        drop(data);
+
+        let gd_callable = Callable::from_custom(self.callable.clone());
+
+        if self.signal.is_connected(&gd_callable) {
+            self.signal.disconnect(&gd_callable);
+        }
path: godot-core/src/builtin/signal/futures.rs
user: login: Bromeon
body: I think I asked a similar question once -- but since you create a new custom callable here, will `is_connected()` ever be true (are those equal in Godot)?

It's why I can be a bit of a pain with the "please add a comment" reviews :sweat_smile: but I think it really helps to document the reasoning / invariants that allow such things to work :+1: 
created_at: 2025-03-03T12:34:51Z
author_association: MEMBER
diff_hunk: @@ -320,7 +454,71 @@ fn run_rust_test(test: &RustTestCase, ctx: &TestContext) -> TestOutcome {
     TestOutcome::from_bool(success.is_ok())
 }
 
-fn print_test_pre(test_case: &str, test_file: String, last_file: &mut Option<String>, flush: bool) {
+#[cfg(since_api = "4.2")]
+fn run_async_rust_test(
+    test: &AsyncRustTestCase,
+    ctx: &TestContext,
+    on_test_finished: impl FnOnce(TestOutcome) + 'static,
+) {
+    if test.skipped {
+        return on_test_finished(TestOutcome::Skipped);
+    }
+
+    // Explicit type to prevent tests from returning a value
+    let err_context = || format!("itest `{}` failed", test.name);
+    let success: Result<godot::builtin::TaskHandle, _> =
+        godot::private::handle_panic(err_context, || (test.function)(ctx));
+
+    let Ok(task_handle) = success else {
+        return on_test_finished(TestOutcome::Failed);
+    };
+
+    check_async_test_task(task_handle, on_test_finished, ctx);
+}
+
+#[cfg(since_api = "4.2")]
+fn check_async_test_task(
+    task_handle: godot::builtin::TaskHandle,
+    on_test_finished: impl FnOnce(TestOutcome) + 'static,
+    ctx: &TestContext,
+) {
+    use godot::builtin::has_godot_task_panicked;
+    use godot::classes::object::ConnectFlags;
+    use godot::obj::EngineBitfield;
+
+    if task_handle.is_pending() {
+        let next_ctx = ctx.clone();
+        let mut callback = Some(on_test_finished);
+        let mut probably_task_handle = Some(task_handle);
+
+        let deferred = Callable::from_local_fn("run_async_rust_test", move |_| {
+            check_async_test_task(
+                probably_task_handle
+                    .take()
+                    .expect("Callable will only be called once!"),
+                callback
+                    .take()
+                    .expect("Callable should not be called multiple times!"),
+                &next_ctx,
+            );
+            Ok(Variant::nil())
+        });
+
+        ctx.scene_tree
+            .get_tree()
+            .expect("The itest scene tree node is part of a Godot SceneTree")
+            .connect_ex("process_frame", &deferred)
+            .flags(ConnectFlags::ONE_SHOT.ord() as u32)
+            .done();
+        return;
+    }
+
+    on_test_finished(TestOutcome::from_bool(!has_godot_task_panicked(
+        task_handle,
+    )));
path: itest/rust/src/framework/runner.rs
user: login: Bromeon
body: Maybe a bit easier if the "happy" (short) condition comes first with early exit:
```rs
if !task_handle.is_pending() {
    on_test_finished(...);
    return;
}

let next_ctx = ctx.clone();
...
```

created_at: 2025-03-03T12:38:35Z
author_association: MEMBER
diff_hunk: @@ -364,7 +559,7 @@ fn print_test_post(test_case: &str, outcome: TestOutcome) {
     }
 }
 
-fn print_bench_pre(benchmark: &str, bench_file: String, last_file: &mut Option<String>) {
+fn print_bench_pre(benchmark: &str, bench_file: &str, last_file: Option<&str>) {
     print_file_header(bench_file, last_file);
 
path: itest/rust/src/framework/runner.rs
user: login: Bromeon
body: Handling the `last_file` outside is quite a bit nicer, thanks!
created_at: 2025-03-03T12:55:16Z
author_association: MEMBER
diff_hunk: @@ -402,6 +597,47 @@ fn get_errors(test: &Variant) -> Array<GString> {
         .unwrap_or_default()
 }
 
+#[cfg(before_api = "4.2")]
+fn collect_rust_tests(filters: &[String]) -> (Vec<RustTestCase>, usize, usize, bool) {
+    let (rust_tests, rust_files, focus_run) = super::collect_rust_tests(filters);
+
+    let rust_test_count = rust_tests.len();
+
+    (rust_tests, rust_test_count, rust_files.len(), focus_run)
+}
+
+#[cfg(since_api = "4.2")]
+fn collect_rust_tests(
+    filters: &[String],
+) -> (
+    Vec<RustTestCase>,
+    Vec<AsyncRustTestCase>,
+    usize,
+    usize,
+    bool,
+) {
path: itest/rust/src/framework/runner.rs
user: login: Bromeon
body: I think this deserves a struct at this point :wink:

The 2nd field could then simply be configured out with `#[cfg(since_api = "4.2")]`.
created_at: 2025-03-03T12:55:45Z
author_association: MEMBER
diff_hunk: @@ -18,19 +18,30 @@ use crate::framework::{
     bencher, passes_filter, BenchResult, RustBenchmark, RustTestCase, TestContext,
 };
 
+#[cfg(since_api = "4.2")]
+use super::AsyncRustTestCase;
+
+#[derive(Debug, Clone, Default)]
+struct TestStats {
+    total: u64,
+    passed: u64,
+    skipped: u64,
path: itest/rust/src/framework/runner.rs
user: login: Bromeon
body: If you change from `i64` to `u64`, might as well use `usize`, or not? :thinking: 
created_at: 2025-03-03T12:59:34Z
author_association: MEMBER
diff_hunk: @@ -72,18 +89,71 @@ impl IntegrationTests {
         }
 
         let clock = Instant::now();
-        self.run_rust_tests(rust_tests, scene_tree, property_tests.clone());
+        self.run_rust_tests(rust_tests, scene_tree.clone(), property_tests.clone());
         let rust_time = clock.elapsed();
-        property_tests.free();
 
         let gdscript_time = if !focus_run {
             let extra_duration = self.run_gdscript_tests(gdscript_tests);
-            Some((clock.elapsed() - rust_time) + extra_duration)
+            Some((clock.elapsed() - rust_time, extra_duration))
         } else {
             None
         };
 
-        self.conclude_tests(rust_time, gdscript_time, allow_focus)
+        #[cfg(before_api = "4.2")]
+        {
+            use godot::obj::WithBaseField;
+
+            property_tests.free();
+
+            let result = Self::conclude_tests(
+                &self.stats,
+                rust_time,
+                gdscript_time.map(|(elapsed, extra)| elapsed + extra),
+                allow_focus,
+            );
+
+            // on_finished will call back into self, so we have to make self re-entrant. We also can't call on_finished in deferred mode,
+            // since it's not available under the 4.1 API.
+            let base = self.base_mut();
+            on_finished.callv(&[result.to_variant()].to_godot());
path: itest/rust/src/framework/runner.rs
user: login: Bromeon
body: ```suggestion
            on_finished.callv(varray![&result]);
```
(not 100% sure if `&` is needed)
created_at: 2025-03-03T13:15:03Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,318 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use core::panic;
+use std::fmt::Display;
+use std::future::{Future, IntoFuture};
+use std::pin::Pin;
+use std::sync::{Arc, Mutex};
+use std::task::{Context, Poll, Waker};
+
+use crate::builtin::{Callable, RustCallable, Variant};
+use crate::classes::object::ConnectFlags;
+use crate::meta::ParamTuple;
+use crate::obj::{EngineBitfield, WithBaseField};
+use crate::registry::signal::TypedSignal;
+
+use super::Signal;
+
+/// The panicking counter part to the [`TrySignalFuture`].
+///
+/// This future works in the same ways as the TrySignalFuture, but panics when the signal object is freed instead of resolving to an
+/// [`Result::Err`]
+pub struct SignalFuture<R: ParamTuple + Sync + Send>(TrySignalFuture<R>);
+
+impl<R: ParamTuple + Sync + Send> SignalFuture<R> {
+    fn new(signal: Signal) -> Self {
+        Self(TrySignalFuture::new(signal))
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> Future for SignalFuture<R> {
+    type Output = R;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        let poll_result = self.get_mut().0.poll(cx);
+
+        match poll_result {
+            Poll::Pending => Poll::Pending,
+            Poll::Ready(Ok(value)) => Poll::Ready(value),
+            Poll::Ready(Err(TrySignalFutureError)) => panic!(
+                "the signal object of a SignalFuture was freed, while the future was still waiting for the signal to be emitted"
+            ),
+        }
+    }
+}
+
+// Not derived, otherwise an extra bound `Output: Default` is required.
+struct SignalFutureData<T> {
+    state: TrySignalFutureState<T>,
+    waker: Option<Waker>,
+}
+
+impl<T> Default for SignalFutureData<T> {
+    fn default() -> Self {
+        Self {
+            state: Default::default(),
+            waker: None,
+        }
+    }
+}
+
+// Only public for itest.
+#[cfg_attr(feature = "trace", derive(Default))]
+pub struct SignalFutureResolver<R> {
+    data: Arc<Mutex<SignalFutureData<R>>>,
+}
+
+impl<R> Clone for SignalFutureResolver<R> {
+    fn clone(&self) -> Self {
+        Self {
+            data: self.data.clone(),
+        }
+    }
+}
+
+impl<R> SignalFutureResolver<R> {
+    fn new(data: Arc<Mutex<SignalFutureData<R>>>) -> Self {
+        Self { data }
+    }
+}
+
+impl<R> std::hash::Hash for SignalFutureResolver<R> {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        state.write_usize(Arc::as_ptr(&self.data) as usize);
+    }
+}
+
+impl<R> PartialEq for SignalFutureResolver<R> {
+    fn eq(&self, other: &Self) -> bool {
+        Arc::ptr_eq(&self.data, &other.data)
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> RustCallable for SignalFutureResolver<R> {
+    fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+        let mut data = self.data.lock().unwrap();
+        // We no longer need the waker after we resolved. If the future is polled again, we'll also get a new waker.
+        let waker = data.waker.take();
+
+        data.state = TrySignalFutureState::Ready(R::from_variant_array(args));
+        drop(data);
+
+        if let Some(waker) = waker {
+            waker.wake();
+        }
+
+        Ok(Variant::nil())
+    }
+}
+
+impl<R> Display for SignalFutureResolver<R> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "GuaranteedSignalFutureResolver::<{}>",
+            std::any::type_name::<R>()
+        )
+    }
+}
+
+// This resolver will change the futures state when it's being dropped (i.e. the engine removes all connected signal callables). By marking
+// the future as dead we can resolve it to an error value the next time it gets polled.
+impl<R> Drop for SignalFutureResolver<R> {
+    fn drop(&mut self) {
+        let mut data = self.data.lock().unwrap();
+
+        if !matches!(data.state, TrySignalFutureState::Pending) {
+            // The future is no longer pending, so no clean up is required.
+            return;
+        }
+
+        // We mark the future as dead, so the next time it gets polled we can react to it's inability to resolve.
+        data.state = TrySignalFutureState::Dead;
+
+        // If we got a waker we trigger it to get the future polled. If there is no waker, then the future has not been polled yet and we
+        // simply wait for the runtime to perform the first poll.
+        if let Some(ref waker) = data.waker {
+            waker.wake_by_ref();
+        }
+    }
+}
+
+#[derive(Default)]
+enum TrySignalFutureState<T> {
+    #[default]
+    Pending,
+    Ready(T),
+    Dead,
+    Dropped,
+}
+
+impl<T> TrySignalFutureState<T> {
+    fn take(&mut self) -> Self {
+        let new_value = match self {
+            Self::Pending => Self::Pending,
+            Self::Ready(_) | Self::Dead => Self::Dead,
+            Self::Dropped => Self::Dropped,
+        };
+
+        std::mem::replace(self, new_value)
+    }
+}
+
+/// A future that tries to resolve as soon as the provided Godot signal was emitted.
+///
+/// The future might resolve to an error if the signal object is freed before the signal is emitted.
+pub struct TrySignalFuture<R: ParamTuple + Sync + Send> {
+    data: Arc<Mutex<SignalFutureData<R>>>,
+    callable: SignalFutureResolver<R>,
+    signal: Signal,
+}
+
+impl<R: ParamTuple + Sync + Send> TrySignalFuture<R> {
+    fn new(signal: Signal) -> Self {
+        debug_assert!(
+            !signal.is_null(),
+            "Failed to create a future for an invalid Signal!\nEither the signal object was already freed or the signal was not registered in the object before using it.",
+        );
+
+        let data = Arc::new(Mutex::new(SignalFutureData::default()));
+
+        // The callable currently requires that the return value is Sync + Send.
+        let callable = SignalFutureResolver::new(data.clone());
+
+        signal.connect(
+            &Callable::from_custom(callable.clone()),
+            ConnectFlags::ONE_SHOT.ord() as i64,
+        );
+
+        Self {
+            data,
+            callable,
+            signal,
+        }
+    }
+    fn poll(&mut self, cx: &mut Context<'_>) -> Poll<Result<R, TrySignalFutureError>> {
+        let mut data = self.data.lock().unwrap();
+
+        data.waker.replace(cx.waker().clone());
+
+        let value = data.state.take();
+
+        match value {
+            TrySignalFutureState::Pending => Poll::Pending,
+            TrySignalFutureState::Dropped => unreachable!(),
+            TrySignalFutureState::Dead => Poll::Ready(Err(TrySignalFutureError)),
+            TrySignalFutureState::Ready(value) => Poll::Ready(Ok(value)),
+        }
+    }
+}
+
+pub struct TrySignalFutureError;
+
+impl<R: ParamTuple + Sync + Send> Future for TrySignalFuture<R> {
+    type Output = Result<R, TrySignalFutureError>;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        self.get_mut().poll(cx)
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> Drop for TrySignalFuture<R> {
+    fn drop(&mut self) {
+        // The callable might alredy be destroyed, this occurs during engine shutdown.
+        if self.signal.object().is_none() {
+            return;
+        }
+
+        let mut data = self.data.lock().unwrap();
+
+        data.state = TrySignalFutureState::Dropped;
+
+        drop(data);
+
+        let gd_callable = Callable::from_custom(self.callable.clone());
+
+        if self.signal.is_connected(&gd_callable) {
+            self.signal.disconnect(&gd_callable);
+        }
path: godot-core/src/builtin/signal/futures.rs
user: login: Yarwin
body: tl;dr – yeah, it compares hashes of given callables.

<details>
<summary> I get lost checking something else </summary>

Godot uses hashes of callables as keys in hashmap to store information about connections; given callable is connected iff its hash is key in said hashmap. Hash function for Callable can be specified while creating (custom) callable, otherwise it fallbacks to godot's default implementation (it hashes method, duh). 

`Callable::from_custom` always specify its own hash method, unlike ones created from fn. In our case it is hash implementation of SignalFutureResolver.

Hash of `Arc::as_ptr(&self.data) as usize` is the very same both for original and the clone so yes, it is valid way to check it.

Signal (delegates to its object)
https://github.com/godotengine/godot/blob/9f68a816599412c56a4626dd6c47244ebc65ed1f/core/variant/callable.cpp#L555-L559

Object::is_connected: https://github.com/godotengine/godot/blob/9f68a816599412c56a4626dd6c47244ebc65ed1f/core/object/object.cpp#L1493

Callable::get_base_comparator & hash: https://github.com/godotengine/godot/blob/9f68a816599412c56a4626dd6c47244ebc65ed1f/core/variant/callable.cpp#L242

</details>

RE: Commenting the code, maybe we can extend description in `godot-core/src/builtin/signal.rs:150` (something along the lines of "given non-custom callable is connected to the signal if method it points to is already connected, while custom callable is connected if callable with its hash has been connected to this signal"?) and add some tests against regression? It is kinda low-capacity and can be done in-between other stuff so I could do it this week :thinking: 
created_at: 2025-03-03T18:48:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,409 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_try_future`](crate::builtin::Signal::to_try_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+/// Will be called during engine shudown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.take();
+}
+
+#[cfg(feature = "trace")]
+pub fn is_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_borrow(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+#[derive(Default)]
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    /// Attempts to extract the future with the given ID from the slot.
+    ///
+    /// Puts the slot into [`FutureSlotState::Polling`] state after taking the future out. It is expected that the future is either parked
+    /// again or the slot is cleared.
+    fn take(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
path: godot-core/src/builtin/async_runtime.rs
user: login: TitanNano
body: > an API which "lends out" the future for a limited scope

The reason why we want to take the future out of the runtime and not have a mutable reference into the runtime is so we can poll multiple tasks at the same time. At the moment, we don't allow multithreaded execution of futures as it makes working with `Gd<T>`s impossible, but we can spawn tasks from inside other tasks. This would panic if we were holding a mutable reference while polling the future.

--- 
> Otherwise, something like take_to_poll or take_for_polling?

I would go with `take_for_polling` then.
created_at: 2025-03-03T18:53:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,318 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use core::panic;
+use std::fmt::Display;
+use std::future::{Future, IntoFuture};
+use std::pin::Pin;
+use std::sync::{Arc, Mutex};
+use std::task::{Context, Poll, Waker};
+
+use crate::builtin::{Callable, RustCallable, Variant};
+use crate::classes::object::ConnectFlags;
+use crate::meta::ParamTuple;
+use crate::obj::{EngineBitfield, WithBaseField};
+use crate::registry::signal::TypedSignal;
+
+use super::Signal;
+
+/// The panicking counter part to the [`TrySignalFuture`].
+///
+/// This future works in the same ways as the TrySignalFuture, but panics when the signal object is freed instead of resolving to an
+/// [`Result::Err`]
+pub struct SignalFuture<R: ParamTuple + Sync + Send>(TrySignalFuture<R>);
+
+impl<R: ParamTuple + Sync + Send> SignalFuture<R> {
+    fn new(signal: Signal) -> Self {
+        Self(TrySignalFuture::new(signal))
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> Future for SignalFuture<R> {
+    type Output = R;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        let poll_result = self.get_mut().0.poll(cx);
+
+        match poll_result {
+            Poll::Pending => Poll::Pending,
+            Poll::Ready(Ok(value)) => Poll::Ready(value),
+            Poll::Ready(Err(TrySignalFutureError)) => panic!(
+                "the signal object of a SignalFuture was freed, while the future was still waiting for the signal to be emitted"
+            ),
+        }
+    }
+}
+
+// Not derived, otherwise an extra bound `Output: Default` is required.
+struct SignalFutureData<T> {
+    state: TrySignalFutureState<T>,
+    waker: Option<Waker>,
+}
+
+impl<T> Default for SignalFutureData<T> {
+    fn default() -> Self {
+        Self {
+            state: Default::default(),
+            waker: None,
+        }
+    }
+}
+
+// Only public for itest.
+#[cfg_attr(feature = "trace", derive(Default))]
+pub struct SignalFutureResolver<R> {
+    data: Arc<Mutex<SignalFutureData<R>>>,
+}
+
+impl<R> Clone for SignalFutureResolver<R> {
+    fn clone(&self) -> Self {
+        Self {
+            data: self.data.clone(),
+        }
+    }
+}
+
+impl<R> SignalFutureResolver<R> {
+    fn new(data: Arc<Mutex<SignalFutureData<R>>>) -> Self {
+        Self { data }
+    }
+}
+
+impl<R> std::hash::Hash for SignalFutureResolver<R> {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        state.write_usize(Arc::as_ptr(&self.data) as usize);
+    }
+}
+
+impl<R> PartialEq for SignalFutureResolver<R> {
+    fn eq(&self, other: &Self) -> bool {
+        Arc::ptr_eq(&self.data, &other.data)
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> RustCallable for SignalFutureResolver<R> {
+    fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+        let mut data = self.data.lock().unwrap();
+        // We no longer need the waker after we resolved. If the future is polled again, we'll also get a new waker.
+        let waker = data.waker.take();
+
+        data.state = TrySignalFutureState::Ready(R::from_variant_array(args));
+        drop(data);
+
+        if let Some(waker) = waker {
+            waker.wake();
+        }
+
+        Ok(Variant::nil())
+    }
+}
+
+impl<R> Display for SignalFutureResolver<R> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "GuaranteedSignalFutureResolver::<{}>",
+            std::any::type_name::<R>()
+        )
+    }
+}
+
+// This resolver will change the futures state when it's being dropped (i.e. the engine removes all connected signal callables). By marking
+// the future as dead we can resolve it to an error value the next time it gets polled.
+impl<R> Drop for SignalFutureResolver<R> {
+    fn drop(&mut self) {
+        let mut data = self.data.lock().unwrap();
+
+        if !matches!(data.state, TrySignalFutureState::Pending) {
+            // The future is no longer pending, so no clean up is required.
+            return;
+        }
+
+        // We mark the future as dead, so the next time it gets polled we can react to it's inability to resolve.
+        data.state = TrySignalFutureState::Dead;
+
+        // If we got a waker we trigger it to get the future polled. If there is no waker, then the future has not been polled yet and we
+        // simply wait for the runtime to perform the first poll.
+        if let Some(ref waker) = data.waker {
+            waker.wake_by_ref();
+        }
+    }
+}
+
+#[derive(Default)]
+enum TrySignalFutureState<T> {
+    #[default]
+    Pending,
+    Ready(T),
+    Dead,
+    Dropped,
+}
+
+impl<T> TrySignalFutureState<T> {
+    fn take(&mut self) -> Self {
+        let new_value = match self {
+            Self::Pending => Self::Pending,
+            Self::Ready(_) | Self::Dead => Self::Dead,
+            Self::Dropped => Self::Dropped,
+        };
+
+        std::mem::replace(self, new_value)
+    }
+}
+
+/// A future that tries to resolve as soon as the provided Godot signal was emitted.
+///
+/// The future might resolve to an error if the signal object is freed before the signal is emitted.
+pub struct TrySignalFuture<R: ParamTuple + Sync + Send> {
+    data: Arc<Mutex<SignalFutureData<R>>>,
+    callable: SignalFutureResolver<R>,
+    signal: Signal,
+}
+
+impl<R: ParamTuple + Sync + Send> TrySignalFuture<R> {
+    fn new(signal: Signal) -> Self {
+        debug_assert!(
+            !signal.is_null(),
+            "Failed to create a future for an invalid Signal!\nEither the signal object was already freed or the signal was not registered in the object before using it.",
+        );
+
+        let data = Arc::new(Mutex::new(SignalFutureData::default()));
+
+        // The callable currently requires that the return value is Sync + Send.
+        let callable = SignalFutureResolver::new(data.clone());
+
+        signal.connect(
+            &Callable::from_custom(callable.clone()),
+            ConnectFlags::ONE_SHOT.ord() as i64,
+        );
+
+        Self {
+            data,
+            callable,
+            signal,
+        }
+    }
+    fn poll(&mut self, cx: &mut Context<'_>) -> Poll<Result<R, TrySignalFutureError>> {
+        let mut data = self.data.lock().unwrap();
+
+        data.waker.replace(cx.waker().clone());
+
+        let value = data.state.take();
+
+        match value {
+            TrySignalFutureState::Pending => Poll::Pending,
+            TrySignalFutureState::Dropped => unreachable!(),
+            TrySignalFutureState::Dead => Poll::Ready(Err(TrySignalFutureError)),
+            TrySignalFutureState::Ready(value) => Poll::Ready(Ok(value)),
+        }
+    }
+}
+
+pub struct TrySignalFutureError;
+
+impl<R: ParamTuple + Sync + Send> Future for TrySignalFuture<R> {
+    type Output = Result<R, TrySignalFutureError>;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        self.get_mut().poll(cx)
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> Drop for TrySignalFuture<R> {
+    fn drop(&mut self) {
+        // The callable might alredy be destroyed, this occurs during engine shutdown.
+        if self.signal.object().is_none() {
+            return;
+        }
+
+        let mut data = self.data.lock().unwrap();
+
+        data.state = TrySignalFutureState::Dropped;
+
+        drop(data);
+
+        let gd_callable = Callable::from_custom(self.callable.clone());
+
+        if self.signal.is_connected(&gd_callable) {
+            self.signal.disconnect(&gd_callable);
+        }
path: godot-core/src/builtin/signal/futures.rs
user: login: TitanNano
body: > It's why I can be a bit of a pain with the "please add a comment" reviews 😅 but I think it really helps to document the reasoning / invariants that allow such things to work 👍

Don't worry about it. I just lost track of what is obvious and what needs explanation due to all the iterations I have already done on this since September. 😆 
created_at: 2025-03-03T21:07:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,456 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, LocalKey, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_fallible_future`](crate::builtin::Signal::to_fallible_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+///
+/// # Example
+/// ```no_run
+/// # use godot::builtin::Signal;
+/// # use godot::classes::Node;
+/// # use godot::obj::NewAlloc;
+/// let node = Node::new_alloc();
+/// let signal = Signal::from_object_signal(&node, "signal");
+///
+/// godot::task::spawn(async move {
+///     println!("starting task...");
+///
+///     signal.to_future::<()>().await;
+///
+///     println!("node has changed: {}", node.get_name());
+/// });
+/// ```
+pub fn spawn(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
path: godot-core/src/task/async_runtime.rs
user: login: Bromeon
body: ```suggestion
        "godot_task() can only be used on the main thread"
```
created_at: 2025-03-05T21:48:11Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,456 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, LocalKey, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_fallible_future`](crate::builtin::Signal::to_fallible_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+///
+/// # Example
+/// ```no_run
+/// # use godot::builtin::Signal;
+/// # use godot::classes::Node;
+/// # use godot::obj::NewAlloc;
+/// let node = Node::new_alloc();
+/// let signal = Signal::from_object_signal(&node, "signal");
+///
+/// godot::task::spawn(async move {
+///     println!("starting task...");
+///
+///     signal.to_future::<()>().await;
+///
+///     println!("node has changed: {}", node.get_name());
+/// });
+/// ```
+pub fn spawn(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_runtime_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _no_send_sync: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _no_send_sync: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_runtime_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                // getting the task from the runtime might return None if the runtime has already been deinitialized. In this case we just
+                // ignore the cancel request as the entire runtime has already been canceled.
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_runtime(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+const ASYNC_RUNTIME_DEINIT_PANIC_MESSAGE: &str = "The async runtime is being accessed after it has been deinitialized. This Should not be possible and is most likely a bug.";
path: godot-core/src/task/async_runtime.rs
user: login: Bromeon
body: ```suggestion
const ASYNC_RUNTIME_DEINIT_PANIC_MESSAGE: &str = "The async runtime is being accessed after it has been deinitialized. This should not be possible and is most likely a bug.";
```
created_at: 2025-03-05T21:49:48Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,456 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, LocalKey, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_fallible_future`](crate::builtin::Signal::to_fallible_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+///
+/// # Example
+/// ```no_run
+/// # use godot::builtin::Signal;
+/// # use godot::classes::Node;
+/// # use godot::obj::NewAlloc;
+/// let node = Node::new_alloc();
+/// let signal = Signal::from_object_signal(&node, "signal");
+///
+/// godot::task::spawn(async move {
+///     println!("starting task...");
+///
+///     signal.to_future::<()>().await;
+///
+///     println!("node has changed: {}", node.get_name());
+/// });
+/// ```
+pub fn spawn(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_runtime_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _no_send_sync: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _no_send_sync: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_runtime_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                // getting the task from the runtime might return None if the runtime has already been deinitialized. In this case we just
+                // ignore the cancel request as the entire runtime has already been canceled.
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_runtime(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+const ASYNC_RUNTIME_DEINIT_PANIC_MESSAGE: &str = "The async runtime is being accessed after it has been deinitialized. This Should not be possible and is most likely a bug.";
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<Option<AsyncRuntime>> = RefCell::new(Some(AsyncRuntime::new()));
+}
+
+/// Will be called during engine shutdown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.set(None);
+}
+
+#[cfg(feature = "trace")]
+pub fn has_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_runtime(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    /// Attempts to extract the future with the given ID from the slot.
+    ///
+    /// Puts the slot into [`FutureSlotState::Polling`] state after taking the future out. It is expected that the future is either parked
+    /// again or the slot is cleared.
+    /// In cases were the slot state is not [`FutureSlotState::Pending`], a copy of the state is returned but the slot remains untouched.
+    fn take_for_polling(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
+
+    /// Parks the future in this slot again.
+    ///
+    /// # Panics
+    /// - If the slot is not in state [`FutureSlotState::Polling`].
+    fn park(&mut self, value: T) {
+        match self.value {
+            FutureSlotState::Empty | FutureSlotState::Gone => {
+                panic!("Future slot is currently unoccupied, future can not be parked here!");
+            }
+            FutureSlotState::Pending(_) => {
+                panic!("Future slot is already occupied by a different future!")
+            }
path: godot-core/src/task/async_runtime.rs
user: login: Bromeon
body: ```suggestion
            FutureSlotState::Empty | FutureSlotState::Gone => {
                panic!("cannot park future in slot which is unoccupied")
            }
            FutureSlotState::Pending(_) => {
                panic!("cannot park future in slot, which is already occupied by a different future")
            }
```
created_at: 2025-03-05T21:53:43Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,456 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, LocalKey, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_fallible_future`](crate::builtin::Signal::to_fallible_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+///
+/// # Example
+/// ```no_run
+/// # use godot::builtin::Signal;
+/// # use godot::classes::Node;
+/// # use godot::obj::NewAlloc;
+/// let node = Node::new_alloc();
+/// let signal = Signal::from_object_signal(&node, "signal");
+///
+/// godot::task::spawn(async move {
+///     println!("starting task...");
+///
+///     signal.to_future::<()>().await;
+///
+///     println!("node has changed: {}", node.get_name());
+/// });
+/// ```
+pub fn spawn(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_runtime_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _no_send_sync: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _no_send_sync: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_runtime_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                // getting the task from the runtime might return None if the runtime has already been deinitialized. In this case we just
+                // ignore the cancel request as the entire runtime has already been canceled.
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_runtime(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+const ASYNC_RUNTIME_DEINIT_PANIC_MESSAGE: &str = "The async runtime is being accessed after it has been deinitialized. This Should not be possible and is most likely a bug.";
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<Option<AsyncRuntime>> = RefCell::new(Some(AsyncRuntime::new()));
+}
+
+/// Will be called during engine shutdown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.set(None);
+}
+
+#[cfg(feature = "trace")]
+pub fn has_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_runtime(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    /// Attempts to extract the future with the given ID from the slot.
+    ///
+    /// Puts the slot into [`FutureSlotState::Polling`] state after taking the future out. It is expected that the future is either parked
+    /// again or the slot is cleared.
+    /// In cases were the slot state is not [`FutureSlotState::Pending`], a copy of the state is returned but the slot remains untouched.
+    fn take_for_polling(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
+
+    /// Parks the future in this slot again.
+    ///
+    /// # Panics
+    /// - If the slot is not in state [`FutureSlotState::Polling`].
+    fn park(&mut self, value: T) {
+        match self.value {
+            FutureSlotState::Empty | FutureSlotState::Gone => {
+                panic!("Future slot is currently unoccupied, future can not be parked here!");
+            }
+            FutureSlotState::Pending(_) => {
+                panic!("Future slot is already occupied by a different future!")
+            }
+            FutureSlotState::Polling => {
+                self.value = FutureSlotState::Pending(value);
+            }
+        }
+    }
+}
+
+#[derive(Default)]
+/// The storage for the pending tasks of the async runtime.
+struct AsyncRuntime {
path: godot-core/src/task/async_runtime.rs
user: login: Bromeon
body: ```suggestion
/// The storage for the pending tasks of the async runtime.
#[derive(Default)]
struct AsyncRuntime {
```
created_at: 2025-03-05T21:54:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,456 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, LocalKey, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_fallible_future`](crate::builtin::Signal::to_fallible_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+///
+/// # Example
+/// ```no_run
+/// # use godot::builtin::Signal;
+/// # use godot::classes::Node;
+/// # use godot::obj::NewAlloc;
+/// let node = Node::new_alloc();
+/// let signal = Signal::from_object_signal(&node, "signal");
+///
+/// godot::task::spawn(async move {
+///     println!("starting task...");
+///
+///     signal.to_future::<()>().await;
+///
+///     println!("node has changed: {}", node.get_name());
+/// });
+/// ```
+pub fn spawn(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_runtime_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _no_send_sync: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _no_send_sync: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_runtime_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                // getting the task from the runtime might return None if the runtime has already been deinitialized. In this case we just
+                // ignore the cancel request as the entire runtime has already been canceled.
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_runtime(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+const ASYNC_RUNTIME_DEINIT_PANIC_MESSAGE: &str = "The async runtime is being accessed after it has been deinitialized. This Should not be possible and is most likely a bug.";
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<Option<AsyncRuntime>> = RefCell::new(Some(AsyncRuntime::new()));
+}
+
+/// Will be called during engine shutdown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.set(None);
+}
+
+#[cfg(feature = "trace")]
+pub fn has_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_runtime(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    /// Attempts to extract the future with the given ID from the slot.
+    ///
+    /// Puts the slot into [`FutureSlotState::Polling`] state after taking the future out. It is expected that the future is either parked
+    /// again or the slot is cleared.
+    /// In cases were the slot state is not [`FutureSlotState::Pending`], a copy of the state is returned but the slot remains untouched.
+    fn take_for_polling(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
+
+    /// Parks the future in this slot again.
+    ///
+    /// # Panics
+    /// - If the slot is not in state [`FutureSlotState::Polling`].
+    fn park(&mut self, value: T) {
+        match self.value {
+            FutureSlotState::Empty | FutureSlotState::Gone => {
+                panic!("Future slot is currently unoccupied, future can not be parked here!");
+            }
+            FutureSlotState::Pending(_) => {
+                panic!("Future slot is already occupied by a different future!")
+            }
+            FutureSlotState::Polling => {
+                self.value = FutureSlotState::Pending(value);
+            }
+        }
+    }
+}
+
+#[derive(Default)]
+/// The storage for the pending tasks of the async runtime.
+struct AsyncRuntime {
+    tasks: Vec<FutureSlot<Pin<Box<dyn Future<Output = ()>>>>>,
+    next_task_id: u64,
+    #[cfg(feature = "trace")]
+    panicked_tasks: std::collections::HashSet<u64>,
+}
+
+impl AsyncRuntime {
+    fn new() -> Self {
+        Self {
+            tasks: Vec::with_capacity(10),
path: godot-core/src/task/async_runtime.rs
user: login: Bromeon
body: Quick question about the 10 here, is there a reason for this number?

I think it's also OK to start with an empty vector, as task management + Godot overhead is likely more than a few allocations (which are amortized over time anyway).
created_at: 2025-03-05T21:56:18Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,456 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, LocalKey, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_fallible_future`](crate::builtin::Signal::to_fallible_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+///
+/// # Example
+/// ```no_run
+/// # use godot::builtin::Signal;
+/// # use godot::classes::Node;
+/// # use godot::obj::NewAlloc;
+/// let node = Node::new_alloc();
+/// let signal = Signal::from_object_signal(&node, "signal");
+///
+/// godot::task::spawn(async move {
+///     println!("starting task...");
+///
+///     signal.to_future::<()>().await;
+///
+///     println!("node has changed: {}", node.get_name());
+/// });
+/// ```
+pub fn spawn(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_runtime_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _no_send_sync: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _no_send_sync: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_runtime_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                // getting the task from the runtime might return None if the runtime has already been deinitialized. In this case we just
+                // ignore the cancel request as the entire runtime has already been canceled.
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_runtime(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+const ASYNC_RUNTIME_DEINIT_PANIC_MESSAGE: &str = "The async runtime is being accessed after it has been deinitialized. This Should not be possible and is most likely a bug.";
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<Option<AsyncRuntime>> = RefCell::new(Some(AsyncRuntime::new()));
+}
+
+/// Will be called during engine shutdown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.set(None);
+}
+
+#[cfg(feature = "trace")]
+pub fn has_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_runtime(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    /// Attempts to extract the future with the given ID from the slot.
+    ///
+    /// Puts the slot into [`FutureSlotState::Polling`] state after taking the future out. It is expected that the future is either parked
+    /// again or the slot is cleared.
+    /// In cases were the slot state is not [`FutureSlotState::Pending`], a copy of the state is returned but the slot remains untouched.
+    fn take_for_polling(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
+
+    /// Parks the future in this slot again.
+    ///
+    /// # Panics
+    /// - If the slot is not in state [`FutureSlotState::Polling`].
+    fn park(&mut self, value: T) {
+        match self.value {
+            FutureSlotState::Empty | FutureSlotState::Gone => {
+                panic!("Future slot is currently unoccupied, future can not be parked here!");
+            }
+            FutureSlotState::Pending(_) => {
+                panic!("Future slot is already occupied by a different future!")
+            }
+            FutureSlotState::Polling => {
+                self.value = FutureSlotState::Pending(value);
+            }
+        }
+    }
+}
+
+#[derive(Default)]
+/// The storage for the pending tasks of the async runtime.
+struct AsyncRuntime {
+    tasks: Vec<FutureSlot<Pin<Box<dyn Future<Output = ()>>>>>,
+    next_task_id: u64,
+    #[cfg(feature = "trace")]
+    panicked_tasks: std::collections::HashSet<u64>,
+}
+
+impl AsyncRuntime {
+    fn new() -> Self {
+        Self {
+            tasks: Vec::with_capacity(10),
+            next_task_id: 0,
+            #[cfg(feature = "trace")]
+            panicked_tasks: std::collections::HashSet::default(),
+        }
+    }
+
+    /// Get the next task ID.
+    fn next_id(&mut self) -> u64 {
+        let id = self.next_task_id;
+        self.next_task_id += 1;
+        id
+    }
+
+    /// Store a new async task in the runtime.
+    ///
+    /// First, a linear search is performed to locate an already existing but currently unoccupied slot in the task buffer. If there is no
+    /// free slot, a new slot is added which may grow the underlying [`Vec`].
+    ///
+    /// The future storage always starts out with a capacity of 10 tasks.
+    fn add_task<F: Future<Output = ()> + 'static>(&mut self, future: F) -> TaskHandle {
+        let id = self.next_id();
+        let index_slot = self
+            .tasks
+            .iter_mut()
+            .enumerate()
+            .find(|(_, slot)| slot.is_empty());
path: godot-core/src/task/async_runtime.rs
user: login: Bromeon
body: `iter()` instead of `iter_mut()` would be enough here, no?
created_at: 2025-03-05T22:08:22Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,456 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, LocalKey, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_fallible_future`](crate::builtin::Signal::to_fallible_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+///
+/// # Example
+/// ```no_run
+/// # use godot::builtin::Signal;
+/// # use godot::classes::Node;
+/// # use godot::obj::NewAlloc;
+/// let node = Node::new_alloc();
+/// let signal = Signal::from_object_signal(&node, "signal");
+///
+/// godot::task::spawn(async move {
+///     println!("starting task...");
+///
+///     signal.to_future::<()>().await;
+///
+///     println!("node has changed: {}", node.get_name());
+/// });
+/// ```
+pub fn spawn(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_runtime_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _no_send_sync: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _no_send_sync: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_runtime_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                // getting the task from the runtime might return None if the runtime has already been deinitialized. In this case we just
+                // ignore the cancel request as the entire runtime has already been canceled.
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_runtime(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+const ASYNC_RUNTIME_DEINIT_PANIC_MESSAGE: &str = "The async runtime is being accessed after it has been deinitialized. This Should not be possible and is most likely a bug.";
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<Option<AsyncRuntime>> = RefCell::new(Some(AsyncRuntime::new()));
+}
+
+/// Will be called during engine shutdown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.set(None);
+}
+
+#[cfg(feature = "trace")]
+pub fn has_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_runtime(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    /// Attempts to extract the future with the given ID from the slot.
+    ///
+    /// Puts the slot into [`FutureSlotState::Polling`] state after taking the future out. It is expected that the future is either parked
+    /// again or the slot is cleared.
+    /// In cases were the slot state is not [`FutureSlotState::Pending`], a copy of the state is returned but the slot remains untouched.
+    fn take_for_polling(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
+
+    /// Parks the future in this slot again.
+    ///
+    /// # Panics
+    /// - If the slot is not in state [`FutureSlotState::Polling`].
+    fn park(&mut self, value: T) {
+        match self.value {
+            FutureSlotState::Empty | FutureSlotState::Gone => {
+                panic!("Future slot is currently unoccupied, future can not be parked here!");
+            }
+            FutureSlotState::Pending(_) => {
+                panic!("Future slot is already occupied by a different future!")
+            }
+            FutureSlotState::Polling => {
+                self.value = FutureSlotState::Pending(value);
+            }
+        }
+    }
+}
+
+#[derive(Default)]
+/// The storage for the pending tasks of the async runtime.
+struct AsyncRuntime {
+    tasks: Vec<FutureSlot<Pin<Box<dyn Future<Output = ()>>>>>,
+    next_task_id: u64,
+    #[cfg(feature = "trace")]
+    panicked_tasks: std::collections::HashSet<u64>,
+}
+
+impl AsyncRuntime {
+    fn new() -> Self {
+        Self {
+            tasks: Vec::with_capacity(10),
+            next_task_id: 0,
+            #[cfg(feature = "trace")]
+            panicked_tasks: std::collections::HashSet::default(),
+        }
+    }
+
+    /// Get the next task ID.
+    fn next_id(&mut self) -> u64 {
+        let id = self.next_task_id;
+        self.next_task_id += 1;
+        id
+    }
+
+    /// Store a new async task in the runtime.
+    ///
+    /// First, a linear search is performed to locate an already existing but currently unoccupied slot in the task buffer. If there is no
+    /// free slot, a new slot is added which may grow the underlying [`Vec`].
+    ///
+    /// The future storage always starts out with a capacity of 10 tasks.
+    fn add_task<F: Future<Output = ()> + 'static>(&mut self, future: F) -> TaskHandle {
+        let id = self.next_id();
+        let index_slot = self
+            .tasks
+            .iter_mut()
+            .enumerate()
+            .find(|(_, slot)| slot.is_empty());
+
+        let boxed = Box::pin(future);
+
+        let index = match index_slot {
+            Some((index, slot)) => {
+                *slot = FutureSlot::pending(id, boxed);
+                index
+            }
+            None => {
+                self.tasks.push(FutureSlot::pending(id, boxed));
+                self.tasks.len() - 1
+            }
+        };
+
+        TaskHandle::new(index, id)
+    }
+
+    /// Extract a pending task from the storage.
+    ///
+    /// Attempts to extract a future with the given ID from the specified index and leaves the slot in state [`FutureSlotState::Polling`].
+    /// In cases were the slot state is not [`FutureSlotState::Pending`], a copy of the state is returned but the slot remains untouched.
+    fn take_task_for_polling(
+        &mut self,
+        index: usize,
+        id: u64,
+    ) -> FutureSlotState<Pin<Box<dyn Future<Output = ()> + 'static>>> {
+        let slot = self.tasks.get_mut(index);
+        slot.map(|inner| inner.take_for_polling(id))
+            .unwrap_or(FutureSlotState::Empty)
+    }
+
+    /// Remove a future from the storage and free up its slot.
+    ///
+    /// The slot is left in the [`FutureSlotState::Gone`] state.
+    fn clear_task(&mut self, index: usize) {
+        self.tasks[index].clear();
+    }
+
+    /// Move a future back into its slot.
+    ///
+    /// # Panic
+    /// - If the underlying slot is not in the [`FutureSlotState::Polling`] state.
+    fn park_task(&mut self, index: usize, future: Pin<Box<dyn Future<Output = ()>>>) {
+        self.tasks[index].park(future);
+    }
+
+    /// Track that a future caused a panic.
+    ///
+    /// This is only available for itest.
+    #[cfg(feature = "trace")]
+    fn track_panic(&mut self, task_id: u64) {
+        self.panicked_tasks.insert(task_id);
+    }
+}
+
+trait WithRuntime {
+    fn with_runtime<R>(&'static self, f: impl FnOnce(&AsyncRuntime) -> R) -> R;
+    fn with_runtime_mut<R>(&'static self, f: impl FnOnce(&mut AsyncRuntime) -> R) -> R;
+}
+
+impl WithRuntime for LocalKey<RefCell<Option<AsyncRuntime>>> {
+    fn with_runtime<R>(&'static self, f: impl FnOnce(&AsyncRuntime) -> R) -> R {
+        self.with_borrow(|rt| {
+            let rt_ref = rt.as_ref().expect(ASYNC_RUNTIME_DEINIT_PANIC_MESSAGE);
+
+            f(rt_ref)
+        })
+    }
+
+    fn with_runtime_mut<R>(&'static self, f: impl FnOnce(&mut AsyncRuntime) -> R) -> R {
+        self.with_borrow_mut(|rt| {
+            let rt_ref = rt.as_mut().expect(ASYNC_RUNTIME_DEINIT_PANIC_MESSAGE);
+
+            f(rt_ref)
+        })
+    }
+}
+
+/// Use a godot waker to poll it's associated future.
+///
+/// # Panics
+/// - If called from a thread other than the main-thread.
+fn poll_future(godot_waker: Arc<GodotWaker>) {
+    let current_thread = thread::current().id();
+
+    assert_eq!(
+        godot_waker.thread_id,
+        current_thread,
+        "trying to poll future on a different thread!\n  Current thread: {:?}\n  Future thread: {:?}",
+        current_thread,
+        godot_waker.thread_id,
+    );
+
+    let waker = Waker::from(godot_waker.clone());
+    let mut ctx = Context::from_waker(&waker);
+
+    // Move future out of the runtime while we are polling it to avoid holding a mutable reference for the entire runtime.
+    let future = ASYNC_RUNTIME.with_runtime_mut(|rt| {
+        match rt.take_task_for_polling(godot_waker.runtime_index, godot_waker.task_id) {
+            FutureSlotState::Empty => {
+                panic!("Future slot is empty when waking it! This is a bug!");
+            }
+
+            FutureSlotState::Gone => None,
+
+            FutureSlotState::Polling => {
+                panic!("The same GodotWaker has been called recursively, this is not expected!");
path: godot-core/src/task/async_runtime.rs
user: login: Bromeon
body: If it's not expected because the implementation shouldn't allow it, you could use `unreachable!`:
```suggestion
                unreachable!("the same GodotWaker has been called recursively");
```

If it's because of user error, `panic!` is good but would be good to mention a possible cause (if known here).
created_at: 2025-03-05T22:10:02Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,456 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, LocalKey, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_fallible_future`](crate::builtin::Signal::to_fallible_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+///
+/// # Example
+/// ```no_run
+/// # use godot::builtin::Signal;
+/// # use godot::classes::Node;
+/// # use godot::obj::NewAlloc;
+/// let node = Node::new_alloc();
+/// let signal = Signal::from_object_signal(&node, "signal");
+///
+/// godot::task::spawn(async move {
+///     println!("starting task...");
+///
+///     signal.to_future::<()>().await;
+///
+///     println!("node has changed: {}", node.get_name());
+/// });
+/// ```
+pub fn spawn(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_runtime_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _no_send_sync: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _no_send_sync: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_runtime_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                // getting the task from the runtime might return None if the runtime has already been deinitialized. In this case we just
+                // ignore the cancel request as the entire runtime has already been canceled.
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_runtime(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+const ASYNC_RUNTIME_DEINIT_PANIC_MESSAGE: &str = "The async runtime is being accessed after it has been deinitialized. This Should not be possible and is most likely a bug.";
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<Option<AsyncRuntime>> = RefCell::new(Some(AsyncRuntime::new()));
+}
+
+/// Will be called during engine shutdown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.set(None);
+}
+
+#[cfg(feature = "trace")]
+pub fn has_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_runtime(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    /// Attempts to extract the future with the given ID from the slot.
+    ///
+    /// Puts the slot into [`FutureSlotState::Polling`] state after taking the future out. It is expected that the future is either parked
+    /// again or the slot is cleared.
+    /// In cases were the slot state is not [`FutureSlotState::Pending`], a copy of the state is returned but the slot remains untouched.
+    fn take_for_polling(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
+
+    /// Parks the future in this slot again.
+    ///
+    /// # Panics
+    /// - If the slot is not in state [`FutureSlotState::Polling`].
+    fn park(&mut self, value: T) {
+        match self.value {
+            FutureSlotState::Empty | FutureSlotState::Gone => {
+                panic!("Future slot is currently unoccupied, future can not be parked here!");
+            }
+            FutureSlotState::Pending(_) => {
+                panic!("Future slot is already occupied by a different future!")
+            }
+            FutureSlotState::Polling => {
+                self.value = FutureSlotState::Pending(value);
+            }
+        }
+    }
+}
+
+#[derive(Default)]
+/// The storage for the pending tasks of the async runtime.
+struct AsyncRuntime {
+    tasks: Vec<FutureSlot<Pin<Box<dyn Future<Output = ()>>>>>,
+    next_task_id: u64,
+    #[cfg(feature = "trace")]
+    panicked_tasks: std::collections::HashSet<u64>,
+}
+
+impl AsyncRuntime {
+    fn new() -> Self {
+        Self {
+            tasks: Vec::with_capacity(10),
+            next_task_id: 0,
+            #[cfg(feature = "trace")]
+            panicked_tasks: std::collections::HashSet::default(),
+        }
+    }
+
+    /// Get the next task ID.
+    fn next_id(&mut self) -> u64 {
+        let id = self.next_task_id;
+        self.next_task_id += 1;
+        id
+    }
+
+    /// Store a new async task in the runtime.
+    ///
+    /// First, a linear search is performed to locate an already existing but currently unoccupied slot in the task buffer. If there is no
+    /// free slot, a new slot is added which may grow the underlying [`Vec`].
+    ///
+    /// The future storage always starts out with a capacity of 10 tasks.
+    fn add_task<F: Future<Output = ()> + 'static>(&mut self, future: F) -> TaskHandle {
+        let id = self.next_id();
+        let index_slot = self
+            .tasks
+            .iter_mut()
+            .enumerate()
+            .find(|(_, slot)| slot.is_empty());
+
+        let boxed = Box::pin(future);
+
+        let index = match index_slot {
+            Some((index, slot)) => {
+                *slot = FutureSlot::pending(id, boxed);
+                index
+            }
+            None => {
+                self.tasks.push(FutureSlot::pending(id, boxed));
+                self.tasks.len() - 1
+            }
+        };
+
+        TaskHandle::new(index, id)
+    }
+
+    /// Extract a pending task from the storage.
+    ///
+    /// Attempts to extract a future with the given ID from the specified index and leaves the slot in state [`FutureSlotState::Polling`].
+    /// In cases were the slot state is not [`FutureSlotState::Pending`], a copy of the state is returned but the slot remains untouched.
+    fn take_task_for_polling(
+        &mut self,
+        index: usize,
+        id: u64,
+    ) -> FutureSlotState<Pin<Box<dyn Future<Output = ()> + 'static>>> {
+        let slot = self.tasks.get_mut(index);
+        slot.map(|inner| inner.take_for_polling(id))
+            .unwrap_or(FutureSlotState::Empty)
+    }
+
+    /// Remove a future from the storage and free up its slot.
+    ///
+    /// The slot is left in the [`FutureSlotState::Gone`] state.
+    fn clear_task(&mut self, index: usize) {
+        self.tasks[index].clear();
+    }
+
+    /// Move a future back into its slot.
+    ///
+    /// # Panic
+    /// - If the underlying slot is not in the [`FutureSlotState::Polling`] state.
+    fn park_task(&mut self, index: usize, future: Pin<Box<dyn Future<Output = ()>>>) {
+        self.tasks[index].park(future);
+    }
+
+    /// Track that a future caused a panic.
+    ///
+    /// This is only available for itest.
+    #[cfg(feature = "trace")]
+    fn track_panic(&mut self, task_id: u64) {
+        self.panicked_tasks.insert(task_id);
+    }
+}
+
+trait WithRuntime {
+    fn with_runtime<R>(&'static self, f: impl FnOnce(&AsyncRuntime) -> R) -> R;
+    fn with_runtime_mut<R>(&'static self, f: impl FnOnce(&mut AsyncRuntime) -> R) -> R;
+}
+
+impl WithRuntime for LocalKey<RefCell<Option<AsyncRuntime>>> {
+    fn with_runtime<R>(&'static self, f: impl FnOnce(&AsyncRuntime) -> R) -> R {
+        self.with_borrow(|rt| {
+            let rt_ref = rt.as_ref().expect(ASYNC_RUNTIME_DEINIT_PANIC_MESSAGE);
+
+            f(rt_ref)
+        })
+    }
+
+    fn with_runtime_mut<R>(&'static self, f: impl FnOnce(&mut AsyncRuntime) -> R) -> R {
+        self.with_borrow_mut(|rt| {
+            let rt_ref = rt.as_mut().expect(ASYNC_RUNTIME_DEINIT_PANIC_MESSAGE);
+
+            f(rt_ref)
+        })
+    }
+}
+
+/// Use a godot waker to poll it's associated future.
+///
+/// # Panics
+/// - If called from a thread other than the main-thread.
+fn poll_future(godot_waker: Arc<GodotWaker>) {
+    let current_thread = thread::current().id();
+
+    assert_eq!(
+        godot_waker.thread_id,
+        current_thread,
+        "trying to poll future on a different thread!\n  Current thread: {:?}\n  Future thread: {:?}",
+        current_thread,
+        godot_waker.thread_id,
+    );
+
+    let waker = Waker::from(godot_waker.clone());
+    let mut ctx = Context::from_waker(&waker);
+
+    // Move future out of the runtime while we are polling it to avoid holding a mutable reference for the entire runtime.
+    let future = ASYNC_RUNTIME.with_runtime_mut(|rt| {
+        match rt.take_task_for_polling(godot_waker.runtime_index, godot_waker.task_id) {
+            FutureSlotState::Empty => {
+                panic!("Future slot is empty when waking it! This is a bug!");
+            }
+
+            FutureSlotState::Gone => None,
+
+            FutureSlotState::Polling => {
+                panic!("The same GodotWaker has been called recursively, this is not expected!");
+            }
+
+            FutureSlotState::Pending(future) => Some(future),
+        }
+    });
+
+    let Some(future) = future else {
+        // Future has been canceled while the waker was already triggered.
+        return;
+    };
+
+    let error_context = || "Godot async task failed";
+
+    // Future should be unwind-safe as we will immediately drop it in cases where a panic occurs.
+    let mut future = AssertUnwindSafe(future);
path: godot-core/src/task/async_runtime.rs
user: login: Bromeon
body: ```suggestion
    // If Future::poll() panics, the future is immediately dropped and cannot be accessed again,
    // thus any state that may not have been unwind-safe cannot be observed later.
    let mut future = AssertUnwindSafe(future);
```

(How I interpreted our earlier discussion in https://github.com/godot-rust/gdext/pull/1043#discussion_r1976712715)
created_at: 2025-03-05T22:30:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,334 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use core::panic;
+use std::fmt::Display;
+use std::future::{Future, IntoFuture};
+use std::pin::Pin;
+use std::sync::{Arc, Mutex};
+use std::task::{Context, Poll, Waker};
+
+use crate::builtin::{Callable, RustCallable, Signal, Variant};
+use crate::classes::object::ConnectFlags;
+use crate::meta::ParamTuple;
+use crate::obj::{EngineBitfield, WithBaseField};
+use crate::registry::signal::TypedSignal;
+
+/// The panicking counter part to the [`FallibleSignalFuture`].
+///
+/// This future works in the same ways as the `FallibleSignalFuture`, but panics when the signal object is freed instead of resolving to an
+/// [`Result::Err`]
path: godot-core/src/task/futures.rs
user: login: Bromeon
body: ```suggestion
/// The panicking counter part to the [`FallibleSignalFuture`].
///
/// This future works in the same way as `FallibleSignalFuture`, but panics when the signal object is freed, instead of resolving to a
/// [`Result::Err`].
```
created_at: 2025-03-05T22:32:26Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,334 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use core::panic;
+use std::fmt::Display;
+use std::future::{Future, IntoFuture};
+use std::pin::Pin;
+use std::sync::{Arc, Mutex};
+use std::task::{Context, Poll, Waker};
+
+use crate::builtin::{Callable, RustCallable, Signal, Variant};
+use crate::classes::object::ConnectFlags;
+use crate::meta::ParamTuple;
+use crate::obj::{EngineBitfield, WithBaseField};
+use crate::registry::signal::TypedSignal;
+
+/// The panicking counter part to the [`FallibleSignalFuture`].
+///
+/// This future works in the same ways as the `FallibleSignalFuture`, but panics when the signal object is freed instead of resolving to an
+/// [`Result::Err`]
+pub struct SignalFuture<R: ParamTuple + Sync + Send>(FallibleSignalFuture<R>);
+
+impl<R: ParamTuple + Sync + Send> SignalFuture<R> {
+    fn new(signal: Signal) -> Self {
+        Self(FallibleSignalFuture::new(signal))
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> Future for SignalFuture<R> {
+    type Output = R;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        let poll_result = self.get_mut().0.poll(cx);
+
+        match poll_result {
+            Poll::Pending => Poll::Pending,
+            Poll::Ready(Ok(value)) => Poll::Ready(value),
+            Poll::Ready(Err(FallibleSignalFutureError)) => panic!(
+                "the signal object of a SignalFuture was freed, while the future was still waiting for the signal to be emitted"
+            ),
+        }
+    }
+}
+
+// Not derived, otherwise an extra bound `Output: Default` is required.
+struct SignalFutureData<T> {
+    state: SignalFutureState<T>,
+    waker: Option<Waker>,
+}
+
+impl<T> Default for SignalFutureData<T> {
+    fn default() -> Self {
+        Self {
+            state: Default::default(),
+            waker: None,
+        }
+    }
+}
+
+// Only public for itest.
+#[cfg_attr(feature = "trace", derive(Default))]
+pub struct SignalFutureResolver<R> {
+    data: Arc<Mutex<SignalFutureData<R>>>,
+}
+
+impl<R> Clone for SignalFutureResolver<R> {
+    fn clone(&self) -> Self {
+        Self {
+            data: self.data.clone(),
+        }
+    }
+}
+
+impl<R> SignalFutureResolver<R> {
+    fn new(data: Arc<Mutex<SignalFutureData<R>>>) -> Self {
+        Self { data }
+    }
+}
+
+impl<R> std::hash::Hash for SignalFutureResolver<R> {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        state.write_usize(Arc::as_ptr(&self.data) as usize);
+    }
+}
+
+impl<R> PartialEq for SignalFutureResolver<R> {
+    fn eq(&self, other: &Self) -> bool {
+        Arc::ptr_eq(&self.data, &other.data)
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> RustCallable for SignalFutureResolver<R> {
+    fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+        let waker = {
+            let mut data = self.data.lock().unwrap();
+            data.state = SignalFutureState::Ready(R::from_variant_array(args));
+
+            // We no longer need the waker after we resolved. If the future is polled again, we'll also get a new waker.
+            data.waker.take()
+        };
+
+        if let Some(waker) = waker {
+            waker.wake();
+        }
+
+        Ok(Variant::nil())
+    }
+}
+
+impl<R> Display for SignalFutureResolver<R> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "GuaranteedSignalFutureResolver::<{}>",
path: godot-core/src/task/futures.rs
user: login: Bromeon
body: ```suggestion
            "SignalFutureResolver::<{}>",
```
created_at: 2025-03-05T22:39:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,334 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use core::panic;
+use std::fmt::Display;
+use std::future::{Future, IntoFuture};
+use std::pin::Pin;
+use std::sync::{Arc, Mutex};
+use std::task::{Context, Poll, Waker};
+
+use crate::builtin::{Callable, RustCallable, Signal, Variant};
+use crate::classes::object::ConnectFlags;
+use crate::meta::ParamTuple;
+use crate::obj::{EngineBitfield, WithBaseField};
+use crate::registry::signal::TypedSignal;
+
+/// The panicking counter part to the [`FallibleSignalFuture`].
+///
+/// This future works in the same ways as the `FallibleSignalFuture`, but panics when the signal object is freed instead of resolving to an
+/// [`Result::Err`]
+pub struct SignalFuture<R: ParamTuple + Sync + Send>(FallibleSignalFuture<R>);
+
+impl<R: ParamTuple + Sync + Send> SignalFuture<R> {
+    fn new(signal: Signal) -> Self {
+        Self(FallibleSignalFuture::new(signal))
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> Future for SignalFuture<R> {
+    type Output = R;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        let poll_result = self.get_mut().0.poll(cx);
+
+        match poll_result {
+            Poll::Pending => Poll::Pending,
+            Poll::Ready(Ok(value)) => Poll::Ready(value),
+            Poll::Ready(Err(FallibleSignalFutureError)) => panic!(
+                "the signal object of a SignalFuture was freed, while the future was still waiting for the signal to be emitted"
+            ),
+        }
+    }
+}
+
+// Not derived, otherwise an extra bound `Output: Default` is required.
+struct SignalFutureData<T> {
+    state: SignalFutureState<T>,
+    waker: Option<Waker>,
+}
+
+impl<T> Default for SignalFutureData<T> {
+    fn default() -> Self {
+        Self {
+            state: Default::default(),
+            waker: None,
+        }
+    }
+}
+
+// Only public for itest.
+#[cfg_attr(feature = "trace", derive(Default))]
+pub struct SignalFutureResolver<R> {
+    data: Arc<Mutex<SignalFutureData<R>>>,
+}
+
+impl<R> Clone for SignalFutureResolver<R> {
+    fn clone(&self) -> Self {
+        Self {
+            data: self.data.clone(),
+        }
+    }
+}
+
+impl<R> SignalFutureResolver<R> {
+    fn new(data: Arc<Mutex<SignalFutureData<R>>>) -> Self {
+        Self { data }
+    }
+}
+
+impl<R> std::hash::Hash for SignalFutureResolver<R> {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        state.write_usize(Arc::as_ptr(&self.data) as usize);
+    }
+}
+
+impl<R> PartialEq for SignalFutureResolver<R> {
+    fn eq(&self, other: &Self) -> bool {
+        Arc::ptr_eq(&self.data, &other.data)
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> RustCallable for SignalFutureResolver<R> {
+    fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+        let waker = {
+            let mut data = self.data.lock().unwrap();
+            data.state = SignalFutureState::Ready(R::from_variant_array(args));
+
+            // We no longer need the waker after we resolved. If the future is polled again, we'll also get a new waker.
+            data.waker.take()
+        };
+
+        if let Some(waker) = waker {
+            waker.wake();
+        }
+
+        Ok(Variant::nil())
+    }
+}
+
+impl<R> Display for SignalFutureResolver<R> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "GuaranteedSignalFutureResolver::<{}>",
+            std::any::type_name::<R>()
+        )
+    }
+}
+
+// This resolver will change the futures state when it's being dropped (i.e. the engine removes all connected signal callables). By marking
+// the future as dead we can resolve it to an error value the next time it gets polled.
+impl<R> Drop for SignalFutureResolver<R> {
+    fn drop(&mut self) {
+        let mut data = self.data.lock().unwrap();
+
+        if !matches!(data.state, SignalFutureState::Pending) {
+            // The future is no longer pending, so no clean up is required.
+            return;
+        }
+
+        // We mark the future as dead, so the next time it gets polled we can react to it's inability to resolve.
+        data.state = SignalFutureState::Dead;
+
+        // If we got a waker we trigger it to get the future polled. If there is no waker, then the future has not been polled yet and we
+        // simply wait for the runtime to perform the first poll.
+        if let Some(ref waker) = data.waker {
+            waker.wake_by_ref();
+        }
+    }
+}
+
+#[derive(Default)]
+enum SignalFutureState<T> {
+    #[default]
+    Pending,
+    Ready(T),
+    Dead,
+    Dropped,
+}
+
+impl<T> SignalFutureState<T> {
+    fn take(&mut self) -> Self {
+        let new_value = match self {
+            Self::Pending => Self::Pending,
+            Self::Ready(_) | Self::Dead => Self::Dead,
+            Self::Dropped => Self::Dropped,
+        };
+
+        std::mem::replace(self, new_value)
+    }
+}
+
+/// A future that tries to resolve as soon as the provided Godot signal was emitted.
+///
+/// The future might resolve to an error if the signal object is freed before the signal is emitted.
+pub struct FallibleSignalFuture<R: ParamTuple + Sync + Send> {
+    data: Arc<Mutex<SignalFutureData<R>>>,
+    callable: SignalFutureResolver<R>,
+    signal: Signal,
+}
+
+impl<R: ParamTuple + Sync + Send> FallibleSignalFuture<R> {
+    fn new(signal: Signal) -> Self {
+        debug_assert!(
+            !signal.is_null(),
+            "Failed to create a future for an invalid Signal!\nEither the signal object was already freed or the signal was not registered in the object before using it.",
+        );
+
+        let data = Arc::new(Mutex::new(SignalFutureData::default()));
+
+        // The callable currently requires that the return value is Sync + Send.
+        let callable = SignalFutureResolver::new(data.clone());
+
+        signal.connect(
+            &Callable::from_custom(callable.clone()),
+            ConnectFlags::ONE_SHOT.ord() as i64,
+        );
+
+        Self {
+            data,
+            callable,
+            signal,
+        }
+    }
+    fn poll(&mut self, cx: &mut Context<'_>) -> Poll<Result<R, FallibleSignalFutureError>> {
+        let mut data = self.data.lock().unwrap();
+
+        data.waker.replace(cx.waker().clone());
+
+        let value = data.state.take();
+
+        match value {
+            SignalFutureState::Pending => Poll::Pending,
+            SignalFutureState::Dropped => unreachable!(),
+            SignalFutureState::Dead => Poll::Ready(Err(FallibleSignalFutureError)),
+            SignalFutureState::Ready(value) => Poll::Ready(Ok(value)),
+        }
+    }
+}
+
+/// Error that might be returned  by the [`FallibleSignalFuture`].
+///
+/// This error is being resolved to when the signal object is freed before the awaited singal is emitted.
+#[derive(Debug)]
+pub struct FallibleSignalFutureError;
+
+impl Display for FallibleSignalFutureError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "The signal object was freed before the awaited signal was emitted"
+        )
+    }
+}
+
+impl std::error::Error for FallibleSignalFutureError {}
+
+impl<R: ParamTuple + Sync + Send> Future for FallibleSignalFuture<R> {
+    type Output = Result<R, FallibleSignalFutureError>;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        self.get_mut().poll(cx)
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> Drop for FallibleSignalFuture<R> {
+    fn drop(&mut self) {
+        // The callable might alredy be destroyed, this occurs during engine shutdown.
+        if self.signal.object().is_none() {
+            return;
+        }
+
+        let mut data_lock = self.data.lock().unwrap();
+
+        data_lock.state = SignalFutureState::Dropped;
+
+        drop(data_lock);
+
+        // We create a new Godot Callable from our RustCallable so we get independent reference counting.
+        let gd_callable = Callable::from_custom(self.callable.clone());
+
+        // is_connected will return true if the signal was never emited before the future is dropped.
+        if self.signal.is_connected(&gd_callable) {
+            self.signal.disconnect(&gd_callable);
+        }
+    }
+}
+
+impl Signal {
+    /// Creates a fallible future for this signal.
+    ///
+    /// The future will resolve the next time the signal is emitted.
+    /// See [`TrySignalFuture`] for details.
+    ///
+    /// Since the `Signal` type does not contain information on the signal argument types, the future output type has to be inferred from
+    /// the call to this function.
+    pub fn to_fallible_future<R: ParamTuple + Sync + Send>(&self) -> FallibleSignalFuture<R> {
+        FallibleSignalFuture::new(self.clone())
+    }
+
+    /// Creates a future for this signal.
+    ///
+    /// The future will resolve the next time the `Signal` is emitted, but might panic if the signal object is freed.
+    /// See [`SignalFuture`] for details.
+    ///
+    /// Since the `Signal` type does not contain information on the signal argument types, the future output type has to be inferred from
+    /// the call to this function.
+    pub fn to_future<R: ParamTuple + Sync + Send>(&self) -> SignalFuture<R> {
+        SignalFuture::new(self.clone())
+    }
+}
+
+impl<C: WithBaseField, R: ParamTuple + Sync + Send> TypedSignal<'_, C, R> {
+    /// Creates a fallible future for this signal.
+    ///
+    /// The future will resolve the next time the signal is emitted.
+    /// See [`FallibleSignalFuture`] for details.
+    pub fn to_fallible_future(&self) -> FallibleSignalFuture<R> {
+        FallibleSignalFuture::new(self.to_untyped())
+    }
+
+    /// Creates a future for this signal.
+    ///
+    /// The future will resolve the next time the `Signal` is emitted, but might panic if the signal object is freed.
+    /// See [`SignalFuture`] for details.
+    pub fn to_future(&self) -> SignalFuture<R> {
+        SignalFuture::new(self.to_untyped())
+    }
+}
+
+impl<C: WithBaseField, R: ParamTuple + Sync + Send> IntoFuture for &TypedSignal<'_, C, R> {
+    type Output = R;
+
+    type IntoFuture = SignalFuture<R>;
+
+    fn into_future(self) -> Self::IntoFuture {
+        self.to_future()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::sys;
+    use std::sync::Arc;
+
+    use super::SignalFutureResolver;
+
+    /// Test that the hash of a cloned future resolver is equal to its original version. With this equality in place, we can create new
+    /// Callables that are equal to their original version but have separate reference counting.
+    #[test]
+    fn guaranteed_future_resolver_cloned_hash() {
path: godot-core/src/task/futures.rs
user: login: Bromeon
body: "Guaranteed" name is outdated.
created_at: 2025-03-05T22:40:19Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,112 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::ops::Deref;
+
+use godot::builtin::{Callable, Signal, Variant};
+use godot::classes::{Object, RefCounted};
+use godot::meta::ToGodot;
+use godot::obj::{Base, Gd, NewAlloc, NewGd};
+use godot::prelude::{godot_api, GodotClass};
+use godot::task::{self, SignalFuture, SignalFutureResolver, TaskHandle};
+
+use crate::framework::{itest, TestContext};
+
+#[derive(GodotClass)]
+#[class(init)]
+struct AsyncRefCounted {
+    base: Base<RefCounted>,
+}
+
+#[godot_api]
+impl AsyncRefCounted {
+    #[signal]
+    fn custom_signal(value: u32);
+}
+
+#[itest(async)]
+fn start_async_task() -> TaskHandle {
+    let mut object = RefCounted::new_gd();
+    let object_ref = object.clone();
+    let signal = Signal::from_object_signal(&object, "custom_signal");
+
+    object.add_user_signal("custom_signal");
+
+    let task_handle = task::spawn(async move {
+        let signal_future: SignalFuture<(u8,)> = signal.to_future();
+        let (result,) = signal_future.await;
+
+        assert_eq!(result, 10);
+        drop(object_ref);
+    });
+
+    object.emit_signal("custom_signal", &[10.to_variant()]);
+
+    task_handle
+}
+
+#[itest]
+fn cancel_async_task(ctx: &TestContext) {
+    let tree = ctx.scene_tree.get_tree().unwrap();
+    let signal = Signal::from_object_signal(&tree, "process_frame");
+
+    let handle = task::spawn(async move {
+        let _: () = signal.to_future().await;
+
+        unreachable!();
+    });
+
+    handle.cancel();
+}
+
+#[itest(async)]
+fn async_task_guaranteed_signal_future() -> TaskHandle {
path: itest/rust/src/engine_tests/async_test.rs
user: login: Bromeon
body: "Guaranteed" name is outdated.
created_at: 2025-03-05T22:40:46Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,456 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, LocalKey, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_fallible_future`](crate::builtin::Signal::to_fallible_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+///
+/// # Example
+/// ```no_run
+/// # use godot::builtin::Signal;
+/// # use godot::classes::Node;
+/// # use godot::obj::NewAlloc;
+/// let node = Node::new_alloc();
+/// let signal = Signal::from_object_signal(&node, "signal");
+///
+/// godot::task::spawn(async move {
+///     println!("starting task...");
+///
+///     signal.to_future::<()>().await;
+///
+///     println!("node has changed: {}", node.get_name());
+/// });
+/// ```
+pub fn spawn(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_runtime_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _no_send_sync: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _no_send_sync: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_runtime_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                // getting the task from the runtime might return None if the runtime has already been deinitialized. In this case we just
+                // ignore the cancel request as the entire runtime has already been canceled.
path: godot-core/src/task/async_runtime.rs
user: login: Bromeon
body: ```suggestion
                // Getting the task from the runtime might return None if the runtime has already been deinitialized. In this case, we just
                // ignore the cancel request, as the entire runtime has already been canceled.
```
created_at: 2025-03-05T22:43:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,456 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, LocalKey, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_fallible_future`](crate::builtin::Signal::to_fallible_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+///
+/// # Example
+/// ```no_run
+/// # use godot::builtin::Signal;
+/// # use godot::classes::Node;
+/// # use godot::obj::NewAlloc;
+/// let node = Node::new_alloc();
+/// let signal = Signal::from_object_signal(&node, "signal");
+///
+/// godot::task::spawn(async move {
+///     println!("starting task...");
+///
+///     signal.to_future::<()>().await;
+///
+///     println!("node has changed: {}", node.get_name());
+/// });
+/// ```
+pub fn spawn(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_runtime_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _no_send_sync: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _no_send_sync: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_runtime_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                // getting the task from the runtime might return None if the runtime has already been deinitialized. In this case we just
+                // ignore the cancel request as the entire runtime has already been canceled.
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_runtime(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
path: godot-core/src/task/async_runtime.rs
user: login: Bromeon
body: ```suggestion
            let slot = rt.tasks.get(self.index).unwrap_or_else(|| panic!("missing future slot at index {}", self.index));
```

Is this btw. something the user can trigger? If yes, should maybe be annotated in `# Panics` section. If it shouldn't happen (assuming no bugs), then it's fine as-is.
created_at: 2025-03-05T22:45:13Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,409 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_try_future`](crate::builtin::Signal::to_try_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+pub fn godot_task(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _pd: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _pd: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_borrow_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_borrow(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new());
+}
+
+/// Will be called during engine shudown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.take();
+}
+
+#[cfg(feature = "trace")]
+pub fn is_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_borrow(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+#[derive(Default)]
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    #[default]
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    /// Attempts to extract the future with the given ID from the slot.
+    ///
+    /// Puts the slot into [`FutureSlotState::Polling`] state after taking the future out. It is expected that the future is either parked
+    /// again or the slot is cleared.
+    fn take(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
path: godot-core/src/builtin/async_runtime.rs
user: login: Bromeon
body: > The reason why we want to take the future out of the runtime and not have a mutable reference into the runtime is so we can poll multiple tasks at the same time.

Very good point, something I didn't think of.
Could you copy-paste this into a code comment?


> I would go with `take_for_polling` then.

Sounds good! :slightly_smiling_face:  
created_at: 2025-03-05T22:48:44Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,334 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use core::panic;
+use std::fmt::Display;
+use std::future::{Future, IntoFuture};
+use std::pin::Pin;
+use std::sync::{Arc, Mutex};
+use std::task::{Context, Poll, Waker};
+
+use crate::builtin::{Callable, RustCallable, Signal, Variant};
+use crate::classes::object::ConnectFlags;
+use crate::meta::ParamTuple;
+use crate::obj::{EngineBitfield, WithBaseField};
+use crate::registry::signal::TypedSignal;
+
+/// The panicking counter part to the [`FallibleSignalFuture`].
+///
+/// This future works in the same ways as the `FallibleSignalFuture`, but panics when the signal object is freed instead of resolving to an
+/// [`Result::Err`]
+pub struct SignalFuture<R: ParamTuple + Sync + Send>(FallibleSignalFuture<R>);
+
+impl<R: ParamTuple + Sync + Send> SignalFuture<R> {
+    fn new(signal: Signal) -> Self {
+        Self(FallibleSignalFuture::new(signal))
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> Future for SignalFuture<R> {
+    type Output = R;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        let poll_result = self.get_mut().0.poll(cx);
+
+        match poll_result {
+            Poll::Pending => Poll::Pending,
+            Poll::Ready(Ok(value)) => Poll::Ready(value),
+            Poll::Ready(Err(FallibleSignalFutureError)) => panic!(
+                "the signal object of a SignalFuture was freed, while the future was still waiting for the signal to be emitted"
+            ),
+        }
+    }
+}
+
+// Not derived, otherwise an extra bound `Output: Default` is required.
+struct SignalFutureData<T> {
+    state: SignalFutureState<T>,
+    waker: Option<Waker>,
+}
+
+impl<T> Default for SignalFutureData<T> {
+    fn default() -> Self {
+        Self {
+            state: Default::default(),
+            waker: None,
+        }
+    }
+}
+
+// Only public for itest.
+#[cfg_attr(feature = "trace", derive(Default))]
+pub struct SignalFutureResolver<R> {
+    data: Arc<Mutex<SignalFutureData<R>>>,
+}
+
+impl<R> Clone for SignalFutureResolver<R> {
+    fn clone(&self) -> Self {
+        Self {
+            data: self.data.clone(),
+        }
+    }
+}
+
+impl<R> SignalFutureResolver<R> {
+    fn new(data: Arc<Mutex<SignalFutureData<R>>>) -> Self {
+        Self { data }
+    }
+}
+
+impl<R> std::hash::Hash for SignalFutureResolver<R> {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        state.write_usize(Arc::as_ptr(&self.data) as usize);
+    }
+}
+
+impl<R> PartialEq for SignalFutureResolver<R> {
+    fn eq(&self, other: &Self) -> bool {
+        Arc::ptr_eq(&self.data, &other.data)
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> RustCallable for SignalFutureResolver<R> {
+    fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+        let waker = {
+            let mut data = self.data.lock().unwrap();
+            data.state = SignalFutureState::Ready(R::from_variant_array(args));
+
+            // We no longer need the waker after we resolved. If the future is polled again, we'll also get a new waker.
+            data.waker.take()
+        };
+
+        if let Some(waker) = waker {
+            waker.wake();
+        }
+
+        Ok(Variant::nil())
+    }
+}
+
+impl<R> Display for SignalFutureResolver<R> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "GuaranteedSignalFutureResolver::<{}>",
+            std::any::type_name::<R>()
+        )
+    }
+}
+
+// This resolver will change the futures state when it's being dropped (i.e. the engine removes all connected signal callables). By marking
+// the future as dead we can resolve it to an error value the next time it gets polled.
+impl<R> Drop for SignalFutureResolver<R> {
+    fn drop(&mut self) {
+        let mut data = self.data.lock().unwrap();
+
+        if !matches!(data.state, SignalFutureState::Pending) {
+            // The future is no longer pending, so no clean up is required.
+            return;
+        }
+
+        // We mark the future as dead, so the next time it gets polled we can react to it's inability to resolve.
+        data.state = SignalFutureState::Dead;
+
+        // If we got a waker we trigger it to get the future polled. If there is no waker, then the future has not been polled yet and we
+        // simply wait for the runtime to perform the first poll.
+        if let Some(ref waker) = data.waker {
+            waker.wake_by_ref();
+        }
+    }
+}
+
+#[derive(Default)]
+enum SignalFutureState<T> {
+    #[default]
+    Pending,
+    Ready(T),
+    Dead,
+    Dropped,
+}
+
+impl<T> SignalFutureState<T> {
+    fn take(&mut self) -> Self {
+        let new_value = match self {
+            Self::Pending => Self::Pending,
+            Self::Ready(_) | Self::Dead => Self::Dead,
+            Self::Dropped => Self::Dropped,
+        };
+
+        std::mem::replace(self, new_value)
+    }
+}
+
+/// A future that tries to resolve as soon as the provided Godot signal was emitted.
+///
+/// The future might resolve to an error if the signal object is freed before the signal is emitted.
+pub struct FallibleSignalFuture<R: ParamTuple + Sync + Send> {
+    data: Arc<Mutex<SignalFutureData<R>>>,
+    callable: SignalFutureResolver<R>,
+    signal: Signal,
+}
+
+impl<R: ParamTuple + Sync + Send> FallibleSignalFuture<R> {
+    fn new(signal: Signal) -> Self {
+        debug_assert!(
+            !signal.is_null(),
+            "Failed to create a future for an invalid Signal!\nEither the signal object was already freed or the signal was not registered in the object before using it.",
+        );
+
+        let data = Arc::new(Mutex::new(SignalFutureData::default()));
+
+        // The callable currently requires that the return value is Sync + Send.
+        let callable = SignalFutureResolver::new(data.clone());
+
+        signal.connect(
+            &Callable::from_custom(callable.clone()),
+            ConnectFlags::ONE_SHOT.ord() as i64,
+        );
+
+        Self {
+            data,
+            callable,
+            signal,
+        }
+    }
+    fn poll(&mut self, cx: &mut Context<'_>) -> Poll<Result<R, FallibleSignalFutureError>> {
+        let mut data = self.data.lock().unwrap();
+
+        data.waker.replace(cx.waker().clone());
+
+        let value = data.state.take();
+
+        match value {
+            SignalFutureState::Pending => Poll::Pending,
+            SignalFutureState::Dropped => unreachable!(),
+            SignalFutureState::Dead => Poll::Ready(Err(FallibleSignalFutureError)),
+            SignalFutureState::Ready(value) => Poll::Ready(Ok(value)),
+        }
+    }
+}
+
+/// Error that might be returned  by the [`FallibleSignalFuture`].
+///
+/// This error is being resolved to when the signal object is freed before the awaited singal is emitted.
+#[derive(Debug)]
+pub struct FallibleSignalFutureError;
+
+impl Display for FallibleSignalFutureError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "The signal object was freed before the awaited signal was emitted"
+        )
+    }
+}
+
+impl std::error::Error for FallibleSignalFutureError {}
+
+impl<R: ParamTuple + Sync + Send> Future for FallibleSignalFuture<R> {
+    type Output = Result<R, FallibleSignalFutureError>;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        self.get_mut().poll(cx)
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> Drop for FallibleSignalFuture<R> {
+    fn drop(&mut self) {
+        // The callable might alredy be destroyed, this occurs during engine shutdown.
+        if self.signal.object().is_none() {
+            return;
+        }
+
+        let mut data_lock = self.data.lock().unwrap();
+
+        data_lock.state = SignalFutureState::Dropped;
+
+        drop(data_lock);
+
+        // We create a new Godot Callable from our RustCallable so we get independent reference counting.
+        let gd_callable = Callable::from_custom(self.callable.clone());
+
+        // is_connected will return true if the signal was never emited before the future is dropped.
+        if self.signal.is_connected(&gd_callable) {
+            self.signal.disconnect(&gd_callable);
+        }
+    }
+}
+
+impl Signal {
+    /// Creates a fallible future for this signal.
+    ///
+    /// The future will resolve the next time the signal is emitted.
+    /// See [`TrySignalFuture`] for details.
+    ///
+    /// Since the `Signal` type does not contain information on the signal argument types, the future output type has to be inferred from
+    /// the call to this function.
+    pub fn to_fallible_future<R: ParamTuple + Sync + Send>(&self) -> FallibleSignalFuture<R> {
+        FallibleSignalFuture::new(self.clone())
+    }
+
+    /// Creates a future for this signal.
+    ///
+    /// The future will resolve the next time the `Signal` is emitted, but might panic if the signal object is freed.
path: godot-core/src/task/futures.rs
user: login: Bromeon
body: ```suggestion
    /// The future will resolve the next time the signal is emitted, but might panic if the signal object is freed.
```
created_at: 2025-03-05T22:52:45Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,334 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use core::panic;
+use std::fmt::Display;
+use std::future::{Future, IntoFuture};
+use std::pin::Pin;
+use std::sync::{Arc, Mutex};
+use std::task::{Context, Poll, Waker};
+
+use crate::builtin::{Callable, RustCallable, Signal, Variant};
+use crate::classes::object::ConnectFlags;
+use crate::meta::ParamTuple;
+use crate::obj::{EngineBitfield, WithBaseField};
+use crate::registry::signal::TypedSignal;
+
+/// The panicking counter part to the [`FallibleSignalFuture`].
+///
+/// This future works in the same ways as the `FallibleSignalFuture`, but panics when the signal object is freed instead of resolving to an
+/// [`Result::Err`]
+pub struct SignalFuture<R: ParamTuple + Sync + Send>(FallibleSignalFuture<R>);
+
+impl<R: ParamTuple + Sync + Send> SignalFuture<R> {
+    fn new(signal: Signal) -> Self {
+        Self(FallibleSignalFuture::new(signal))
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> Future for SignalFuture<R> {
+    type Output = R;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        let poll_result = self.get_mut().0.poll(cx);
+
+        match poll_result {
+            Poll::Pending => Poll::Pending,
+            Poll::Ready(Ok(value)) => Poll::Ready(value),
+            Poll::Ready(Err(FallibleSignalFutureError)) => panic!(
+                "the signal object of a SignalFuture was freed, while the future was still waiting for the signal to be emitted"
+            ),
+        }
+    }
+}
+
+// Not derived, otherwise an extra bound `Output: Default` is required.
+struct SignalFutureData<T> {
+    state: SignalFutureState<T>,
+    waker: Option<Waker>,
+}
+
+impl<T> Default for SignalFutureData<T> {
+    fn default() -> Self {
+        Self {
+            state: Default::default(),
+            waker: None,
+        }
+    }
+}
+
+// Only public for itest.
+#[cfg_attr(feature = "trace", derive(Default))]
+pub struct SignalFutureResolver<R> {
+    data: Arc<Mutex<SignalFutureData<R>>>,
+}
+
+impl<R> Clone for SignalFutureResolver<R> {
+    fn clone(&self) -> Self {
+        Self {
+            data: self.data.clone(),
+        }
+    }
+}
+
+impl<R> SignalFutureResolver<R> {
+    fn new(data: Arc<Mutex<SignalFutureData<R>>>) -> Self {
+        Self { data }
+    }
+}
+
+impl<R> std::hash::Hash for SignalFutureResolver<R> {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        state.write_usize(Arc::as_ptr(&self.data) as usize);
+    }
+}
+
+impl<R> PartialEq for SignalFutureResolver<R> {
+    fn eq(&self, other: &Self) -> bool {
+        Arc::ptr_eq(&self.data, &other.data)
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> RustCallable for SignalFutureResolver<R> {
+    fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()> {
+        let waker = {
+            let mut data = self.data.lock().unwrap();
+            data.state = SignalFutureState::Ready(R::from_variant_array(args));
+
+            // We no longer need the waker after we resolved. If the future is polled again, we'll also get a new waker.
+            data.waker.take()
+        };
+
+        if let Some(waker) = waker {
+            waker.wake();
+        }
+
+        Ok(Variant::nil())
+    }
+}
+
+impl<R> Display for SignalFutureResolver<R> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "GuaranteedSignalFutureResolver::<{}>",
+            std::any::type_name::<R>()
+        )
+    }
+}
+
+// This resolver will change the futures state when it's being dropped (i.e. the engine removes all connected signal callables). By marking
+// the future as dead we can resolve it to an error value the next time it gets polled.
+impl<R> Drop for SignalFutureResolver<R> {
+    fn drop(&mut self) {
+        let mut data = self.data.lock().unwrap();
+
+        if !matches!(data.state, SignalFutureState::Pending) {
+            // The future is no longer pending, so no clean up is required.
+            return;
+        }
+
+        // We mark the future as dead, so the next time it gets polled we can react to it's inability to resolve.
+        data.state = SignalFutureState::Dead;
+
+        // If we got a waker we trigger it to get the future polled. If there is no waker, then the future has not been polled yet and we
+        // simply wait for the runtime to perform the first poll.
+        if let Some(ref waker) = data.waker {
+            waker.wake_by_ref();
+        }
+    }
+}
+
+#[derive(Default)]
+enum SignalFutureState<T> {
+    #[default]
+    Pending,
+    Ready(T),
+    Dead,
+    Dropped,
+}
+
+impl<T> SignalFutureState<T> {
+    fn take(&mut self) -> Self {
+        let new_value = match self {
+            Self::Pending => Self::Pending,
+            Self::Ready(_) | Self::Dead => Self::Dead,
+            Self::Dropped => Self::Dropped,
+        };
+
+        std::mem::replace(self, new_value)
+    }
+}
+
+/// A future that tries to resolve as soon as the provided Godot signal was emitted.
+///
+/// The future might resolve to an error if the signal object is freed before the signal is emitted.
+pub struct FallibleSignalFuture<R: ParamTuple + Sync + Send> {
+    data: Arc<Mutex<SignalFutureData<R>>>,
+    callable: SignalFutureResolver<R>,
+    signal: Signal,
+}
+
+impl<R: ParamTuple + Sync + Send> FallibleSignalFuture<R> {
+    fn new(signal: Signal) -> Self {
+        debug_assert!(
+            !signal.is_null(),
+            "Failed to create a future for an invalid Signal!\nEither the signal object was already freed or the signal was not registered in the object before using it.",
+        );
+
+        let data = Arc::new(Mutex::new(SignalFutureData::default()));
+
+        // The callable currently requires that the return value is Sync + Send.
+        let callable = SignalFutureResolver::new(data.clone());
+
+        signal.connect(
+            &Callable::from_custom(callable.clone()),
+            ConnectFlags::ONE_SHOT.ord() as i64,
+        );
+
+        Self {
+            data,
+            callable,
+            signal,
+        }
+    }
+    fn poll(&mut self, cx: &mut Context<'_>) -> Poll<Result<R, FallibleSignalFutureError>> {
+        let mut data = self.data.lock().unwrap();
+
+        data.waker.replace(cx.waker().clone());
+
+        let value = data.state.take();
+
+        match value {
+            SignalFutureState::Pending => Poll::Pending,
+            SignalFutureState::Dropped => unreachable!(),
+            SignalFutureState::Dead => Poll::Ready(Err(FallibleSignalFutureError)),
+            SignalFutureState::Ready(value) => Poll::Ready(Ok(value)),
+        }
+    }
+}
+
+/// Error that might be returned  by the [`FallibleSignalFuture`].
+///
+/// This error is being resolved to when the signal object is freed before the awaited singal is emitted.
+#[derive(Debug)]
+pub struct FallibleSignalFutureError;
+
+impl Display for FallibleSignalFutureError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "The signal object was freed before the awaited signal was emitted"
+        )
+    }
+}
+
+impl std::error::Error for FallibleSignalFutureError {}
+
+impl<R: ParamTuple + Sync + Send> Future for FallibleSignalFuture<R> {
+    type Output = Result<R, FallibleSignalFutureError>;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        self.get_mut().poll(cx)
+    }
+}
+
+impl<R: ParamTuple + Sync + Send> Drop for FallibleSignalFuture<R> {
+    fn drop(&mut self) {
+        // The callable might alredy be destroyed, this occurs during engine shutdown.
+        if self.signal.object().is_none() {
+            return;
+        }
+
+        let mut data_lock = self.data.lock().unwrap();
+
+        data_lock.state = SignalFutureState::Dropped;
+
+        drop(data_lock);
+
+        // We create a new Godot Callable from our RustCallable so we get independent reference counting.
+        let gd_callable = Callable::from_custom(self.callable.clone());
+
+        // is_connected will return true if the signal was never emited before the future is dropped.
+        if self.signal.is_connected(&gd_callable) {
+            self.signal.disconnect(&gd_callable);
+        }
+    }
+}
+
+impl Signal {
+    /// Creates a fallible future for this signal.
+    ///
+    /// The future will resolve the next time the signal is emitted.
+    /// See [`TrySignalFuture`] for details.
+    ///
+    /// Since the `Signal` type does not contain information on the signal argument types, the future output type has to be inferred from
+    /// the call to this function.
+    pub fn to_fallible_future<R: ParamTuple + Sync + Send>(&self) -> FallibleSignalFuture<R> {
+        FallibleSignalFuture::new(self.clone())
+    }
+
+    /// Creates a future for this signal.
+    ///
+    /// The future will resolve the next time the `Signal` is emitted, but might panic if the signal object is freed.
+    /// See [`SignalFuture`] for details.
+    ///
+    /// Since the `Signal` type does not contain information on the signal argument types, the future output type has to be inferred from
+    /// the call to this function.
+    pub fn to_future<R: ParamTuple + Sync + Send>(&self) -> SignalFuture<R> {
+        SignalFuture::new(self.clone())
+    }
+}
+
+impl<C: WithBaseField, R: ParamTuple + Sync + Send> TypedSignal<'_, C, R> {
+    /// Creates a fallible future for this signal.
+    ///
+    /// The future will resolve the next time the signal is emitted.
+    /// See [`FallibleSignalFuture`] for details.
+    pub fn to_fallible_future(&self) -> FallibleSignalFuture<R> {
+        FallibleSignalFuture::new(self.to_untyped())
+    }
+
+    /// Creates a future for this signal.
+    ///
+    /// The future will resolve the next time the `Signal` is emitted, but might panic if the signal object is freed.
path: godot-core/src/task/futures.rs
user: login: Bromeon
body: ```suggestion
    /// The future will resolve the next time the signal is emitted, but might panic if the signal object is freed.
```
created_at: 2025-03-05T22:53:04Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,456 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, LocalKey, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_fallible_future`](crate::builtin::Signal::to_fallible_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+///
+/// # Example
+/// ```no_run
+/// # use godot::builtin::Signal;
+/// # use godot::classes::Node;
+/// # use godot::obj::NewAlloc;
+/// let node = Node::new_alloc();
+/// let signal = Signal::from_object_signal(&node, "signal");
+///
+/// godot::task::spawn(async move {
+///     println!("starting task...");
+///
+///     signal.to_future::<()>().await;
+///
+///     println!("node has changed: {}", node.get_name());
+/// });
+/// ```
+pub fn spawn(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_runtime_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _no_send_sync: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _no_send_sync: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_runtime_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                // getting the task from the runtime might return None if the runtime has already been deinitialized. In this case we just
+                // ignore the cancel request as the entire runtime has already been canceled.
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_runtime(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
path: godot-core/src/task/async_runtime.rs
user: login: TitanNano
body: No, it's a bug if we actually ever hit this.
created_at: 2025-03-05T23:01:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,456 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, LocalKey, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_fallible_future`](crate::builtin::Signal::to_fallible_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+///
+/// # Example
+/// ```no_run
+/// # use godot::builtin::Signal;
+/// # use godot::classes::Node;
+/// # use godot::obj::NewAlloc;
+/// let node = Node::new_alloc();
+/// let signal = Signal::from_object_signal(&node, "signal");
+///
+/// godot::task::spawn(async move {
+///     println!("starting task...");
+///
+///     signal.to_future::<()>().await;
+///
+///     println!("node has changed: {}", node.get_name());
+/// });
+/// ```
+pub fn spawn(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_runtime_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _no_send_sync: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _no_send_sync: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_runtime_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                // getting the task from the runtime might return None if the runtime has already been deinitialized. In this case we just
+                // ignore the cancel request as the entire runtime has already been canceled.
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_runtime(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
path: godot-core/src/task/async_runtime.rs
user: login: Bromeon
body: We might then also consider `unreachable!`, although it's not that important. `expect()` also works in such a case, as it would anyway require more debugging...
created_at: 2025-03-05T23:03:19Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,456 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, LocalKey, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_fallible_future`](crate::builtin::Signal::to_fallible_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+///
+/// # Example
+/// ```no_run
+/// # use godot::builtin::Signal;
+/// # use godot::classes::Node;
+/// # use godot::obj::NewAlloc;
+/// let node = Node::new_alloc();
+/// let signal = Signal::from_object_signal(&node, "signal");
+///
+/// godot::task::spawn(async move {
+///     println!("starting task...");
+///
+///     signal.to_future::<()>().await;
+///
+///     println!("node has changed: {}", node.get_name());
+/// });
+/// ```
+pub fn spawn(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_runtime_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _no_send_sync: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _no_send_sync: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_runtime_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                // getting the task from the runtime might return None if the runtime has already been deinitialized. In this case we just
+                // ignore the cancel request as the entire runtime has already been canceled.
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_runtime(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+const ASYNC_RUNTIME_DEINIT_PANIC_MESSAGE: &str = "The async runtime is being accessed after it has been deinitialized. This Should not be possible and is most likely a bug.";
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<Option<AsyncRuntime>> = RefCell::new(Some(AsyncRuntime::new()));
+}
+
+/// Will be called during engine shutdown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.set(None);
+}
+
+#[cfg(feature = "trace")]
+pub fn has_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_runtime(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    /// Attempts to extract the future with the given ID from the slot.
+    ///
+    /// Puts the slot into [`FutureSlotState::Polling`] state after taking the future out. It is expected that the future is either parked
+    /// again or the slot is cleared.
+    /// In cases were the slot state is not [`FutureSlotState::Pending`], a copy of the state is returned but the slot remains untouched.
+    fn take_for_polling(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
+
+    /// Parks the future in this slot again.
+    ///
+    /// # Panics
+    /// - If the slot is not in state [`FutureSlotState::Polling`].
+    fn park(&mut self, value: T) {
+        match self.value {
+            FutureSlotState::Empty | FutureSlotState::Gone => {
+                panic!("Future slot is currently unoccupied, future can not be parked here!");
+            }
+            FutureSlotState::Pending(_) => {
+                panic!("Future slot is already occupied by a different future!")
+            }
+            FutureSlotState::Polling => {
+                self.value = FutureSlotState::Pending(value);
+            }
+        }
+    }
+}
+
+#[derive(Default)]
+/// The storage for the pending tasks of the async runtime.
+struct AsyncRuntime {
+    tasks: Vec<FutureSlot<Pin<Box<dyn Future<Output = ()>>>>>,
+    next_task_id: u64,
+    #[cfg(feature = "trace")]
+    panicked_tasks: std::collections::HashSet<u64>,
+}
+
+impl AsyncRuntime {
+    fn new() -> Self {
+        Self {
+            tasks: Vec::with_capacity(10),
path: godot-core/src/task/async_runtime.rs
user: login: TitanNano
body: No, it was arbitrary chosen. Since the `AsyncRuntime` is being initialized on demand at first use, I thought it might be nice to have a couple of slots already allocated. But I have no idea if it actually has any positive or negative impact. We can also set it to 0 / `Vec::new()`. 
created_at: 2025-03-05T23:09:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,456 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, LocalKey, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_fallible_future`](crate::builtin::Signal::to_fallible_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+///
+/// # Example
+/// ```no_run
+/// # use godot::builtin::Signal;
+/// # use godot::classes::Node;
+/// # use godot::obj::NewAlloc;
+/// let node = Node::new_alloc();
+/// let signal = Signal::from_object_signal(&node, "signal");
+///
+/// godot::task::spawn(async move {
+///     println!("starting task...");
+///
+///     signal.to_future::<()>().await;
+///
+///     println!("node has changed: {}", node.get_name());
+/// });
+/// ```
+pub fn spawn(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_runtime_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _no_send_sync: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _no_send_sync: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_runtime_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                // getting the task from the runtime might return None if the runtime has already been deinitialized. In this case we just
+                // ignore the cancel request as the entire runtime has already been canceled.
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_runtime(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+const ASYNC_RUNTIME_DEINIT_PANIC_MESSAGE: &str = "The async runtime is being accessed after it has been deinitialized. This Should not be possible and is most likely a bug.";
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<Option<AsyncRuntime>> = RefCell::new(Some(AsyncRuntime::new()));
+}
+
+/// Will be called during engine shutdown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.set(None);
+}
+
+#[cfg(feature = "trace")]
+pub fn has_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_runtime(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    /// Attempts to extract the future with the given ID from the slot.
+    ///
+    /// Puts the slot into [`FutureSlotState::Polling`] state after taking the future out. It is expected that the future is either parked
+    /// again or the slot is cleared.
+    /// In cases were the slot state is not [`FutureSlotState::Pending`], a copy of the state is returned but the slot remains untouched.
+    fn take_for_polling(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
+
+    /// Parks the future in this slot again.
+    ///
+    /// # Panics
+    /// - If the slot is not in state [`FutureSlotState::Polling`].
+    fn park(&mut self, value: T) {
+        match self.value {
+            FutureSlotState::Empty | FutureSlotState::Gone => {
+                panic!("Future slot is currently unoccupied, future can not be parked here!");
+            }
+            FutureSlotState::Pending(_) => {
+                panic!("Future slot is already occupied by a different future!")
+            }
+            FutureSlotState::Polling => {
+                self.value = FutureSlotState::Pending(value);
+            }
+        }
+    }
+}
+
+#[derive(Default)]
+/// The storage for the pending tasks of the async runtime.
+struct AsyncRuntime {
+    tasks: Vec<FutureSlot<Pin<Box<dyn Future<Output = ()>>>>>,
+    next_task_id: u64,
+    #[cfg(feature = "trace")]
+    panicked_tasks: std::collections::HashSet<u64>,
+}
+
+impl AsyncRuntime {
+    fn new() -> Self {
+        Self {
+            tasks: Vec::with_capacity(10),
+            next_task_id: 0,
+            #[cfg(feature = "trace")]
+            panicked_tasks: std::collections::HashSet::default(),
+        }
+    }
+
+    /// Get the next task ID.
+    fn next_id(&mut self) -> u64 {
+        let id = self.next_task_id;
+        self.next_task_id += 1;
+        id
+    }
+
+    /// Store a new async task in the runtime.
+    ///
+    /// First, a linear search is performed to locate an already existing but currently unoccupied slot in the task buffer. If there is no
+    /// free slot, a new slot is added which may grow the underlying [`Vec`].
+    ///
+    /// The future storage always starts out with a capacity of 10 tasks.
+    fn add_task<F: Future<Output = ()> + 'static>(&mut self, future: F) -> TaskHandle {
+        let id = self.next_id();
+        let index_slot = self
+            .tasks
+            .iter_mut()
+            .enumerate()
+            .find(|(_, slot)| slot.is_empty());
path: godot-core/src/task/async_runtime.rs
user: login: TitanNano
body: Yes 👍 good catch.
created_at: 2025-03-05T23:10:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,456 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, LocalKey, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_fallible_future`](crate::builtin::Signal::to_fallible_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+///
+/// # Example
+/// ```no_run
+/// # use godot::builtin::Signal;
+/// # use godot::classes::Node;
+/// # use godot::obj::NewAlloc;
+/// let node = Node::new_alloc();
+/// let signal = Signal::from_object_signal(&node, "signal");
+///
+/// godot::task::spawn(async move {
+///     println!("starting task...");
+///
+///     signal.to_future::<()>().await;
+///
+///     println!("node has changed: {}", node.get_name());
+/// });
+/// ```
+pub fn spawn(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_runtime_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _no_send_sync: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _no_send_sync: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_runtime_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                // getting the task from the runtime might return None if the runtime has already been deinitialized. In this case we just
+                // ignore the cancel request as the entire runtime has already been canceled.
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_runtime(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+const ASYNC_RUNTIME_DEINIT_PANIC_MESSAGE: &str = "The async runtime is being accessed after it has been deinitialized. This Should not be possible and is most likely a bug.";
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<Option<AsyncRuntime>> = RefCell::new(Some(AsyncRuntime::new()));
+}
+
+/// Will be called during engine shutdown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.set(None);
+}
+
+#[cfg(feature = "trace")]
+pub fn has_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_runtime(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    /// Attempts to extract the future with the given ID from the slot.
+    ///
+    /// Puts the slot into [`FutureSlotState::Polling`] state after taking the future out. It is expected that the future is either parked
+    /// again or the slot is cleared.
+    /// In cases were the slot state is not [`FutureSlotState::Pending`], a copy of the state is returned but the slot remains untouched.
+    fn take_for_polling(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
+
+    /// Parks the future in this slot again.
+    ///
+    /// # Panics
+    /// - If the slot is not in state [`FutureSlotState::Polling`].
+    fn park(&mut self, value: T) {
+        match self.value {
+            FutureSlotState::Empty | FutureSlotState::Gone => {
+                panic!("Future slot is currently unoccupied, future can not be parked here!");
+            }
+            FutureSlotState::Pending(_) => {
+                panic!("Future slot is already occupied by a different future!")
+            }
+            FutureSlotState::Polling => {
+                self.value = FutureSlotState::Pending(value);
+            }
+        }
+    }
+}
+
+#[derive(Default)]
+/// The storage for the pending tasks of the async runtime.
+struct AsyncRuntime {
+    tasks: Vec<FutureSlot<Pin<Box<dyn Future<Output = ()>>>>>,
+    next_task_id: u64,
+    #[cfg(feature = "trace")]
+    panicked_tasks: std::collections::HashSet<u64>,
+}
+
+impl AsyncRuntime {
+    fn new() -> Self {
+        Self {
+            tasks: Vec::with_capacity(10),
path: godot-core/src/task/async_runtime.rs
user: login: Bromeon
body: > Since the `AsyncRuntime` is being initialized on demand at first use, I thought it might be nice to have a couple of slots already allocated.

Ah, that's a good point, so maybe leave it at a power of 2, like 16?

And a small comment about the lazy initialization would be nice, so a reader is aware that this allocation is only paid if someone actually uses signal futures.
created_at: 2025-03-06T13:15:37Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,456 @@
+/*
+ * Copyright (c) godot-rust; Bromeon and contributors.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::cell::RefCell;
+use std::future::Future;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll, Wake, Waker};
+use std::thread::{self, LocalKey, ThreadId};
+
+use crate::builtin::{Callable, Variant};
+use crate::private::handle_panic;
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Public interface
+
+/// Create a new async background task.
+///
+/// This function allows creating a new async task in which Godot signals can be awaited, like it is possible in GDScript. The
+/// [`TaskHandle`] that is returned provides synchronous introspection into the current state of the task.
+///
+/// Refer to [`Signal::to_future`](crate::builtin::Signal::to_future) and [`Signal::to_fallible_future`](crate::builtin::Signal::to_fallible_future)
+/// for details on how to await a signal.
+///
+/// # Panics
+/// - If called from any other thread than the main-thread.
+///
+/// # Example
+/// ```no_run
+/// # use godot::builtin::Signal;
+/// # use godot::classes::Node;
+/// # use godot::obj::NewAlloc;
+/// let node = Node::new_alloc();
+/// let signal = Signal::from_object_signal(&node, "signal");
+///
+/// godot::task::spawn(async move {
+///     println!("starting task...");
+///
+///     signal.to_future::<()>().await;
+///
+///     println!("node has changed: {}", node.get_name());
+/// });
+/// ```
+pub fn spawn(future: impl Future<Output = ()> + 'static) -> TaskHandle {
+    // Spawning new tasks is only allowed on the main thread for now.
+    // We can not accept Sync + Send futures since all object references (i.e. Gd<T>) are not thread-safe. So a future has to remain on the
+    // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread
+    // a future will be polled.
+    // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
+    //
+    // Once thread-safe futures are possible the restriction can be lifted.
+    assert!(
+        crate::init::is_main_thread(),
+        "godot_task can only be used on the main thread!"
+    );
+
+    let (task_handle, godot_waker) = ASYNC_RUNTIME.with_runtime_mut(move |rt| {
+        let task_handle = rt.add_task(Box::pin(future));
+        let godot_waker = Arc::new(GodotWaker::new(
+            task_handle.index,
+            task_handle.id,
+            thread::current().id(),
+        ));
+
+        (task_handle, godot_waker)
+    });
+
+    poll_future(godot_waker);
+    task_handle
+}
+
+/// Handle for an active background task.
+///
+/// This handle provides introspection into the current state of the task, as well as providing a way to cancel it.
+///
+/// The associated task will **not** be canceled if this handle is dropped.
+pub struct TaskHandle {
+    index: usize,
+    id: u64,
+    _no_send_sync: PhantomData<*const ()>,
+}
+
+impl TaskHandle {
+    fn new(index: usize, id: u64) -> Self {
+        Self {
+            index,
+            id,
+            _no_send_sync: PhantomData,
+        }
+    }
+
+    /// Cancels the task if it is still pending and does nothing if it is already completed.
+    pub fn cancel(self) {
+        ASYNC_RUNTIME.with_runtime_mut(|rt| {
+            let Some(task) = rt.tasks.get(self.index) else {
+                // getting the task from the runtime might return None if the runtime has already been deinitialized. In this case we just
+                // ignore the cancel request as the entire runtime has already been canceled.
+                return;
+            };
+
+            let alive = match task.value {
+                FutureSlotState::Empty => {
+                    panic!("Future slot is empty when canceling it! This is a bug!")
+                }
+                FutureSlotState::Gone => false,
+                FutureSlotState::Pending(_) => task.id == self.id,
+                FutureSlotState::Polling => panic!("Can not cancel future from inside it!"),
+            };
+
+            if !alive {
+                return;
+            }
+
+            rt.clear_task(self.index);
+        })
+    }
+
+    /// Synchronously checks if the task is still pending or has already completed.
+    pub fn is_pending(&self) -> bool {
+        ASYNC_RUNTIME.with_runtime(|rt| {
+            let slot = rt.tasks.get(self.index).expect("Slot at index must exist!");
+
+            if slot.id != self.id {
+                return false;
+            }
+
+            matches!(
+                slot.value,
+                FutureSlotState::Pending(_) | FutureSlotState::Polling
+            )
+        })
+    }
+}
+
+// ----------------------------------------------------------------------------------------------------------------------------------------------
+// Async Runtime
+
+const ASYNC_RUNTIME_DEINIT_PANIC_MESSAGE: &str = "The async runtime is being accessed after it has been deinitialized. This Should not be possible and is most likely a bug.";
+
+thread_local! {
+    static ASYNC_RUNTIME: RefCell<Option<AsyncRuntime>> = RefCell::new(Some(AsyncRuntime::new()));
+}
+
+/// Will be called during engine shutdown.
+///
+/// We have to drop all the remaining Futures during engine shutdown. This avoids them being dropped at process termination where they would
+/// try to access engine resources, which leads to SEGFAULTs.
+pub(crate) fn cleanup() {
+    ASYNC_RUNTIME.set(None);
+}
+
+#[cfg(feature = "trace")]
+pub fn has_godot_task_panicked(task_handle: TaskHandle) -> bool {
+    ASYNC_RUNTIME.with_runtime(|rt| rt.panicked_tasks.contains(&task_handle.id))
+}
+
+/// The current state of a future inside the async runtime.
+enum FutureSlotState<T> {
+    /// Slot is currently empty.
+    Empty,
+    /// Slot was previously occupied but the future has been canceled or the slot reused.
+    Gone,
+    /// Slot contains a pending future.
+    Pending(T),
+    /// Slot contains a future which is currently being polled.
+    Polling,
+}
+
+/// Wrapper around a future that is being stored in the async runtime.
+///
+/// This wrapper contains additional metadata for the async runtime.
+struct FutureSlot<T> {
+    value: FutureSlotState<T>,
+    id: u64,
+}
+
+impl<T> FutureSlot<T> {
+    /// Create a new slot with a pending future.
+    fn pending(id: u64, value: T) -> Self {
+        Self {
+            value: FutureSlotState::Pending(value),
+            id,
+        }
+    }
+
+    /// Checks if the future slot is either still empty or has become unoccupied due to a future completing.
+    fn is_empty(&self) -> bool {
+        matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone)
+    }
+
+    /// Drop the future from this slot.
+    ///
+    /// This transitions the slot into the [`FutureSlotState::Gone`] state.
+    fn clear(&mut self) {
+        self.value = FutureSlotState::Gone;
+    }
+
+    /// Attempts to extract the future with the given ID from the slot.
+    ///
+    /// Puts the slot into [`FutureSlotState::Polling`] state after taking the future out. It is expected that the future is either parked
+    /// again or the slot is cleared.
+    /// In cases were the slot state is not [`FutureSlotState::Pending`], a copy of the state is returned but the slot remains untouched.
+    fn take_for_polling(&mut self, id: u64) -> FutureSlotState<T> {
+        match self.value {
+            FutureSlotState::Empty => FutureSlotState::Empty,
+            FutureSlotState::Polling => FutureSlotState::Polling,
+            FutureSlotState::Gone => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone,
+            FutureSlotState::Pending(_) => {
+                std::mem::replace(&mut self.value, FutureSlotState::Polling)
+            }
+        }
+    }
+
+    /// Parks the future in this slot again.
+    ///
+    /// # Panics
+    /// - If the slot is not in state [`FutureSlotState::Polling`].
+    fn park(&mut self, value: T) {
+        match self.value {
+            FutureSlotState::Empty | FutureSlotState::Gone => {
+                panic!("Future slot is currently unoccupied, future can not be parked here!");
+            }
+            FutureSlotState::Pending(_) => {
+                panic!("Future slot is already occupied by a different future!")
+            }
+            FutureSlotState::Polling => {
+                self.value = FutureSlotState::Pending(value);
+            }
+        }
+    }
+}
+
+#[derive(Default)]
+/// The storage for the pending tasks of the async runtime.
+struct AsyncRuntime {
+    tasks: Vec<FutureSlot<Pin<Box<dyn Future<Output = ()>>>>>,
+    next_task_id: u64,
+    #[cfg(feature = "trace")]
+    panicked_tasks: std::collections::HashSet<u64>,
+}
+
+impl AsyncRuntime {
+    fn new() -> Self {
+        Self {
+            tasks: Vec::with_capacity(10),
+            next_task_id: 0,
+            #[cfg(feature = "trace")]
+            panicked_tasks: std::collections::HashSet::default(),
+        }
+    }
+
+    /// Get the next task ID.
+    fn next_id(&mut self) -> u64 {
+        let id = self.next_task_id;
+        self.next_task_id += 1;
+        id
+    }
+
+    /// Store a new async task in the runtime.
+    ///
+    /// First, a linear search is performed to locate an already existing but currently unoccupied slot in the task buffer. If there is no
+    /// free slot, a new slot is added which may grow the underlying [`Vec`].
+    ///
+    /// The future storage always starts out with a capacity of 10 tasks.
+    fn add_task<F: Future<Output = ()> + 'static>(&mut self, future: F) -> TaskHandle {
+        let id = self.next_id();
+        let index_slot = self
+            .tasks
+            .iter_mut()
+            .enumerate()
+            .find(|(_, slot)| slot.is_empty());
path: godot-core/src/task/async_runtime.rs
user: login: TitanNano
body: We actually mutate the found slot in line 293. I added a comment about that to make it more obvious.
created_at: 2025-03-06T22:55:24Z
author_association: CONTRIBUTOR

PR comments:
user: login: GodotRust
created_at: 2025-02-09T23:43:01Z
body: API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-1043
author_association: NONE
user: login: jrb0001
created_at: 2025-02-10T22:19:51Z
body: I am currently testing it with my project.

- Executor from this PR and signals from my old implementation (based on async_channel) seems to work ingame.
- My old executor (based on async_task running once per frame) and signals from this PR is the next step, hopefully tomorrow.
- Both Executor and signals from this PR will come after that. I expect some issues with recursive signals but let's see.
- I am getting a weird segfault on hotreloading with a completely useless backtrace which didn't happen with my executor implementation. I need to debug this more, but I suspect it is related to having a tool node spawn a future which listens on its signals and/or a signal>drop>signal>drop something else>signal chain.
author_association: CONTRIBUTOR
user: login: lilizoey
created_at: 2025-02-11T19:01:26Z
body: >     * I am getting a weird segfault on hotreloading with a completely useless backtrace which didn't happen with my executor implementation. I need to debug this more, but I suspect it is related to having a tool node spawn a future which listens on its signals and/or a signal>drop>signal>drop something else>signal chain.

i'd guess it's related to using `thread_local` here which we need to do some hacky stuff to support with hot-reloading enabled
author_association: MEMBER
user: login: TitanNano
created_at: 2025-02-11T19:45:06Z
body: > i'd guess it's related to using thread_local here which we need to do some hacky stuff to support with hot-reloading enabled

Shouldn't the hot-reload hack only leak memory? 🤔

@jrb0001 does the segfault occur on every hot-reload?
author_association: CONTRIBUTOR
user: login: jrb0001
created_at: 2025-02-11T22:27:14Z
body: > > i'd guess it's related to using thread_local here which we need to do some hacky stuff to support with hot-reloading enabled
> 
> Shouldn't the hot-reload hack only leak memory? 🤔
> 
> @jrb0001 does the segfault occur on every hot-reload?

I am not completely sure yet. It doesn't happen if there are no open scenes or if none of them contains a node which spawns a Future.

It also doesn't seem to happen every single time if I close all scenes and then open one with a Future before triggering the hot-reload. In this case it panics *with some scenes*:
```text
ERROR: godot-rust function call failed: <Callable>::GodotWaker::wake()
    Reason: [panic]  Future no longer exists when waking it! This is a bug!
  at /home/jrb0001/.cargo/git/checkouts/gdext-3ec94bd991a90eb6/2877010/godot-core/src/builtin/async_runtime.rs:271
```

With another scene it segfaults in this scenario.

Simply reopening the editor (same scene gets opened automatically) and then triggering a hot-reload segfaults for both scenes.

With both executor + Future from this PR, the hot-reload issue doesn't happen at all?!? So the issue could also be in my code, let me debug it properly before you waste more time on it.

I will do some more debugging later this week (probably weekend).

---

I also finished testing the Future part of the PR and it works fine with both my old executor and your executor in my relatively simple usage.

Unfortunately all my complex usages (recursion, dropping, etc.) need a `futures_lite::Stream` which I can't implement on top of your `GuaranteedSignalFuture` without potentially missing (or duplicating?) some signals while reconnecting with a new Future instance.

The `R: Debug` bound on `to_future()`/`to_guaranteed_future()` was a bit annoying and doesn't seem to be used? Or did I miss something?
author_association: CONTRIBUTOR
user: login: TitanNano
created_at: 2025-02-11T23:19:49Z
body: > The R: Debug bound on to_future()/to_guaranteed_future() was a bit annoying and doesn't seem to be used? Or did I miss something?

Yeah, it's completely unnecessary now. Probably an old artifact. I removed the bound.

----

> Unfortunately all my complex usages (recursion, dropping, etc.) need a futures_lite::Stream which I can't implement on top of your GuaranteedSignalFuture without potentially missing (or duplicating?) some signals while reconnecting with a new Future instance.

Can you elaborate what the issue here is?

---- 

I'm also curious what your use-case for the `GuaranteedSignalFuture` is. Currently, I'm still thinking to get rid of it again. I have never come across a future that resolves when the underlying source disappears, and I wonder if it is really that useful for most users. But maybe you can share how it's important for you.
author_association: CONTRIBUTOR
user: login: TitanNano
created_at: 2025-02-12T00:02:50Z
body: ```
ERROR: godot-rust function call failed: <Callable>::GodotWaker::wake()
    Reason: [panic]  Future no longer exists when waking it! This is a bug!
  at /home/jrb0001/.cargo/git/checkouts/gdext-3ec94bd991a90eb6/2877010/godot-core/src/builtin/async_runtime.rs:271
```

@jrb0001 Do you have an idea what could have triggered this? The only thing that I can think of is that a waker got cloned and reused after the future resolved. The panic probably doesn't make any sense, since the waker can technically be called an infinite number of times. 🤔 
author_association: CONTRIBUTOR
user: login: TitanNano
created_at: 2025-02-14T23:52:58Z
body: @Bromeon I now added a way to test async tasks. I still need to deal with panics inside a `Future`, though. Technically, we could unify the test execution of sync and async tasks, but I get the impression that it also would have some downsides. Keeping it separate adds a bit of duplication, but unifying it would force more complexity onto the execution of sync tasks.
author_association: CONTRIBUTOR
user: login: jrb0001
created_at: 2025-02-16T17:30:01Z
body: > I'm also curious what your use-case for the `GuaranteedSignalFuture` is. Currently, I'm still thinking to get rid of it again. I have never come across a future that resolves when the underlying source disappears, and I wonder if it is really that useful for most users. But maybe you can share how it's important for you.

My experience seems to be the exact opposite of yours. Usually things like sockets and channels return `Err`/`None`/panic when the other side disappears. I don't think I have ever encountered a `Future` that gets stuck intentionally.

With Godot this isn't only caused by intentionally disconnecting a signal, but also when a node is freed, which can happen at any time and on a large scale. I don't like the idea of having hundreds or maybe even thousands of stuck tasks after the player changed scenes a few times.

I also think we shouldn't compare it to gdscript, for two reasons:
- gdscript doesn't need to store any additional state so it doesn't have a memory leak. Your runtime "leaks" memory through the thread-local if a task gets stuck.
- Not sure how to explain this, but for me the direction behind them is different. gdscript (and rust with `Callable`) is "Godot should call this method when ..." (Godot is the owner / pushing) while `Future` is "My future should wait until ..." (Future/Runtime is the owner / pulling). The `Callable` approach can detect the disconnect through `NOTIFICATION_PREDELETE` (gdscript) or `drop()` (Rust `Callable`) while the latter completely depends on the behavior of the signal future.

Your `SignalFuture` is usually enough and more ergonomic than the `GuaranteedSignalFuture` but I would make it panic on disconnect and make the Runtime clear the task on panic. The `GuaranteedSignalFuture` is still helpful if you need to wait for some signal and detect when the source disappears at the same time, without combining multiple signals, relying on `catch_unwind()` or a custom `Drop` impl.

I unfortunately didn't get to do my debugging session due to sickness. I will let you know once I have some results, but that will most likely be towards the end of the week or even weekend.
author_association: CONTRIBUTOR
user: login: Bromeon
created_at: 2025-02-16T18:00:54Z
body: Thanks a lot for the detailed insights, @jrb0001 :+1:

I'm trying to see it from a user perspective. A user would then have to make a choice whether the basic future is enough or the guaranteed one is needed, which may be... not a great abstraction? 

How would you advise a library user to choose correctly here, without needing to know all the details? Does the choice even make sense, or should we sacrifice a bit of ergonomics for correctness?
author_association: MEMBER
user: login: TitanNano
created_at: 2025-02-16T18:33:28Z
body: > My experience seems to be the exact opposite of yours. Usually things like sockets and channels return Err/None/panic when the other side disappears. I don't think I have ever encountered a Future that gets stuck intentionally.

I get this point, but I wouldn't say the future gets stuck intentionally. If you create a Godot Object and don't free it, then it leaks memory. That is also not _intentional_. From my point of view, async tasks must be stored and canceled before freeing the Object, this is simply an inherited requirement from the manually managed `Node` / `Object`. We can put this into the documentation of the `TaskHandle`. Maybe we also want to make the `TaskHandle` `#[must_use]`?

I also think making the `SignalFuture` panic if it's Callable gets dropped would be a good compromise. This would highlight that something unexpected is happening.
author_association: CONTRIBUTOR
user: login: Dheatly23
created_at: 2025-02-17T05:37:52Z
body: > I get this point, but I wouldn't say the future gets stuck intentionally. If you create a Godot Object and don't free it, then it leaks memory. That is also not _intentional_. From my point of view, async tasks must be stored and canceled before freeing the Object, this is simply an inherited requirement from the manually managed `Node` / `Object`. We can put this into the documentation of the `TaskHandle`. Maybe we also want to make the `TaskHandle` `#[must_use]`?

But isn't manually cancelling `TaskHandle` is too much of a chore? Consider this simple GDScript example:

```gdscript
extends Button

func _pressed():
    await get_tree().create_timer(1.0).timeout
    print("Pressed one second before!")
```

If the button got freed, the call simply drops without any cleanup code. But with your proposal we need to store all `TaskHandle` in the node and cancel them all on exit tree, am i right?

Small nitpick, but i disagree on naming it `GuaranteedSignalFuture`, it give impression that the future will resolve without errors. I suggest naming it `TrySignalFuture` to emphasize that the signal might never resolve (eg. the node is removed). My potential use case is for asynchronous task cleanup like sending final message or waiting/selecting on multiple signals.
author_association: CONTRIBUTOR
user: login: Bromeon
created_at: 2025-02-17T09:59:50Z
body: From the discussion, it's stated that the "guaranteed" future is less ergonomic to use than the regular one. At the same time, it seems like the regular one needs manual cleanup (thus being less ergonomic in its own way).

To be on the same page, could someone post similar usage examples for each of them? :slightly_smiling_face: 
author_association: MEMBER
user: login: TitanNano
created_at: 2025-02-17T18:48:49Z
body: # Examples

## SignalFuture
```rust
let node = Node::new_gd();

// might never complete and should be manually canceled to avoid memory leak.
godot_task(async move {
    let children = node.get_children();
    
    // might never resolve if the node gets freed before the signal is emitted.
    let _: () = Signal::from_object_signal(&node, "tree_entered").to_future().await;

   children.iter_shared().for_each(|child| ... );
});
```

## GuaranteedSignalFuture
```rust
let node = Node::new_gd();

// will always complete.
godot_task(async move {
    let children = node.get_children();
    
    // should always resolve.
    let Some(_): Option<()> = Signal::from_object_signal(&node, "tree_entered").to_guaranteed_future().await else {
        // The singnal object was freed.
        return;
    };

   children.iter_shared().for_each(|child| ... );
});
```

## SignalFuture + panic (not yet implemented)
```rust
let node = Node::new_gd();

// always completes but might panic if the node is freed before the signal is emitted.
godot_task(async move {
    let children = node.get_children();
    
    // might panic. The async runtime will catch the panic, print it and discard the task.
    let _: () = Signal::from_object_signal(&node, "tree_entered").to_future().await;

   children.iter_shared().for_each(|child| ... );
});
```

----
> But isn't manually cancelling TaskHandle is too much of a chore? Consider this simple GDScript example:

You can't compare GDScript and Rust like this. 

In GDScript has a script runtime that directly integrates with the `GDScriptFunctionState`. 

In Rust, both the `Future` trait is completely opaque to the runtime and the `Waker` is completely opaque to the `Future`. We usually also don't just have a `SignalFuture` directly as the async task, but nested futures or even a tree of `Future`s:
```
Future >
  - Future > SignalFuture
  - Future > Future > SingalFuture
```

In the GDScript runtime, they store the pending function states inside the owning script and cancel them when the script is destroyed. The closest we can get to something like that, is to store `TaskHandle`s inside `GodotClass`es and cancel the tasks on `Drop`. But this would still require some manual effort, similar to the `Base` field.

> Small nitpick, but i disagree on naming it GuaranteedSignalFuture, it give impression that the future will resolve without errors. I suggest naming it TrySignalFuture to emphasize that the signal might never resolve (eg. the node is removed). My potential use case is for asynchronous task cleanup like sending final message or waiting/selecting on multiple signals.

I like the name, but the impression you get from the current name is what it does, it always resolves, but it might resolve to `Option::None`.
author_association: CONTRIBUTOR
user: login: coder137
created_at: 2025-02-17T22:17:56Z
body: @TitanNano @Bromeon 

Was going through the PR since it was posted in the discord channel. 
Just sharing my thoughts

1. Seems like we are creating a custom async runtime, could this be replaced by https://github.com/smol-rs/async-task?
2. Simplified version of  `to_future` API it could be something like:

```rust
fn to_future<R>() -> impl Future<Output = Option<R>> + 'static {
    // Since we have a FnMut requirement we cannot use oneshot channels here
    // tokio channels are just an example here (we can use any channel that gives us sync tx and async rx)
    let (tx, rx) = tokio::sync::mpsc::channel(1);
    let callable = Callable::from_local_fn("SignalFuture::resolve", move |_args| {
        let _ignore = tx.blocking_send(R::from_args(_args));
        Ok(Variant::nil())
    });
    async move {
        rx.recv().await
    }
}
```

I believe this might solve the problem with futures getting resolved or no, since the channels will get cleaned up even if the signal isn't fired. We won't have to worry about leaks as long as the executor/async runtime shuts down gracefully.
We won't need 2 different implementations i.e (`SignalFuture` and `GuaranteedSignalFuture`) 

Since we are using existing channel implementations, we have lesser technical debt in godot_core as well.

3. Lastly since `to_future` and async runtime are 2 seperate things, would it be possible to make 2 different PRs?
The `to_future` API can come first since the implementation would be executor independent.

Please let me know what you'll think
author_association: NONE
user: login: Dheatly23
created_at: 2025-02-18T04:27:17Z
body: After a bit of testing, i found minor bug with `to_guaranteed_future`. I don't exactly know what happens, but it just hangs. Perhaps it's somehow got polled twice?

<details>
<summary>Code</summary>

```rust
use std::future::Future;
use std::panic::AssertUnwindSafe;
use std::pin::Pin;

use futures_util::future::{select_all, FutureExt as _};
use godot::classes::{Control, IControl};
use godot::prelude::*;

struct TestAsync;

#[gdextension]
unsafe impl ExtensionLibrary for TestAsync {}

#[derive(GodotClass)]
#[class(init, base = Control)]
struct NodeTestAsync {
    base: Base<Control>,
}

#[godot_api]
impl IControl for NodeTestAsync {
    fn ready(&mut self) {
        let this = self.to_gd();

        let signals = (0..this.get_child_count())
            .filter_map(|i| Some(Signal::from_object_signal(&this.get_child(i)?, "pressed")))
            .collect::<Vec<_>>();

        // Waits all child buttons and reports if they're being pressed.
        godot_task(AssertUnwindSafe(async move {
            fn wait_for_signal(
                i: usize,
                s: &Signal,
            ) -> Pin<Box<dyn '_ + Future<Output = Option<usize>>>> {
                // Without fuse, to_guaranteed_future hangs
                Box::pin(
                    async move {
                        println!("Wait: {i}");
                        s.to_guaranteed_future::<()>().await;
                        println!("Done: {i}");
                        Some(i)
                    }
                    .fuse(),
                )
            }

            let mut futs = signals
                .iter()
                .enumerate()
                .map(|(i, s)| wait_for_signal(i, s))
                .collect::<Vec<_>>();

            println!("Start");
            while !futs.is_empty() {
                let i;
                (i, _, futs) = select_all(futs).await;
                if let Some(i) = i {
                    println!("{i}");
                    futs.push(wait_for_signal(i, &signals[i]));
                }
            }
        }));
    }
}
```
</details>

-----

> You can't compare GDScript and Rust like this.
> 
> In GDScript has a script runtime that directly integrates with the `GDScriptFunctionState`.
> 
> In Rust, both the `Future` trait is completely opaque to the runtime and the `Waker` is completely opaque to the `Future`. We usually also don't just have a `SignalFuture` directly as the async task, but nested futures or even a tree of `Future`s:
> 
> ```
> Future >
>   - Future > SignalFuture
>   - Future > Future > SingalFuture
> ```
> 
> In the GDScript runtime, they store the pending function states inside the owning script and cancel them when the script is destroyed. The closest we can get to something like that, is to store `TaskHandle`s inside `GodotClass`es and cancel the tasks on `Drop`. But this would still require some manual effort, similar to the `Base` field.

For ergonomic reason, we should reflect GDScript's convention as much as possible. Manually managing handles is unusual even for other async runtimes like Tokio.

We should be able to spawn a new task and forget about it, similiar to daemon thread. Shutdown sequence can be done like what [Tokio](https://tokio.rs/tokio/topics/shutdown) did, using cancellation token to signal every outstanding tasks that we need to do cleanup. The runtime can then loops until all tasks finished.

> I like the name, but the impression you get from the current name is what it does, it always resolves, but it might resolve to `Option::None`.

My idea is to return `Err(NeverResolve)` to indicate that the signal will never resolve. Being an error type also makes it easier to `?` it.
author_association: CONTRIBUTOR
user: login: TitanNano
created_at: 2025-02-18T08:40:43Z
body: @coder137 to address your comment:
>1. Seems like we are creating a custom async runtime, could this be replaced by https://github.com/smol-rs/async-task?

We could, but that would be more overhead than using the engine. It would also require an additional dependency, while Godot already provides all the necessary components. 

> 2. Simplified version of to_future API it could be something like:
> I believe this might solve the problem with futures getting resolved or no, since the channels will get cleaned up even if the signal isn't fired

This does exactly the same thing as the `GuaranteedSignalFuture` but requires an external dependency.

> We won't have to worry about leaks as long as the executor/async runtime shuts down gracefully.

And the same applies to the current state of this PR. The _problem_ is with the `SignalFuture` that has the potential to get stuck indefinitely. If we get rid of it and only provide the `GuaranteedSignalFuture` then we won't have stuck futures anymore but will have to handle disappearing objects inside the future.

> Lastly since to_future and async runtime are 2 seperate things, would it be possible to make 2 different PRs?
The to_future API can come first since the implementation would be executor-independent.

We can do that, but I would like to provide a way to execute futures without requiring users to include an external dependency. I also don't see what it would solve right now.

---- 
> After a bit of testing, i found minor bug with to_guaranteed_future. I don't exactly know what happens, but it just hangs. Perhaps it's somehow got polled twice?

Thanks for the report, I will see what is going on there.

> For ergonomic reason, we should reflect GDScript's convention as much as possible. 

Yes, and I'm all for that, as long as it's technically possible.

> Manually managing handles is unusual even for other async runtimes like Tokio.

The issue we are discussing has nothing to do with the runtime. If you use the `SignalFuture` on the tokio runtime, you end up with the same issue. The reason there is no problem in GDScript is that the script runtime and async runtime are the same thing, and they can counteract the oddities of their `SingalFuture` / `GDScriptFunctionState` from inside the script runtime. 

Side note: since GDScript only cleans up pending function states when a `GDScript` gets destroyed, you can also end up with a large number of pending states, should your script simply never (or rarely) get destroyed. 

> My idea is to return Err(NeverResolve) to indicate that the signal will never resolve. Being an error type also makes it easier to ? it.

I see, yes that is an alternative that would be more descriptive.
author_association: CONTRIBUTOR
user: login: TitanNano
created_at: 2025-02-18T10:36:02Z
body: @Dheatly23 After some recent refactoring, the `GuaranteedSignalFuture` was holding the `Mutex` lock too long in its `Drop` implementation. After fixing that, your example works now. I also removed the `UnwindSafe` bound again, so you don't have to assert it in your user code.

```rust
async move {
    println!("Wait: {i}");
    s.to_guaranteed_future::<()>().await;
    println!("Done: {i}");
    Some(i)
}
```
This kind of code is quite reckless, as it treats "resolved because signal fired" and "resolved because signal object was freed" as one and the same thing.
author_association: CONTRIBUTOR
user: login: Dheatly23
created_at: 2025-02-18T12:20:18Z
body: > This kind of code is quite reckless, as it treats "resolved because signal fired" and "resolved because signal object was freed" as one and the same thing.

Oh, i forgot to add `?` to the await. In debugging the root cause, i might have substituted `to_guaranteed_future` with `to_future`, then forgot to fully revert it.

----

With regards to my original complaint, i think it should be resolved by making `SignalFuture` panics on signal never resolving. Then there is no need for manual cleanup since the task will be cleaned on next event loop (?) or engine shutdown.

I disagree on making `TaskHandle` a `must_use` though, since spawned async task should live independently. If user wants to access object, they can do something like this:

```rust
let this = self.to_gd();

godot_task(async move {
    // Do other tasks, wait for signal, etc.

    // Access object, if object does not exist it should panics.
    this.bind();
})
```

I quite like the @coder137 suggestion of decoupling signal future and async runtime. So user can essentially _bring-your-own-async-runtime_ (Tokio, `async-std`, etc) with a default runtime if they so choose. For example: HTTP server with Axum/Tokio stack.
author_association: CONTRIBUTOR
user: login: TitanNano
created_at: 2025-02-18T13:21:53Z
body: > With regards to my original complaint, i think it should be resolved by making SignalFuture panics on signal never resolving. Then there is no need for manual cleanup since the task will be cleaned on next event loop (?) or engine shutdown.

As I wrote in an earlier comment, I do agree with this and I think it could make sense to panic the future if the signal object is freed. The panic would be printed to `stderr` though (like all other panics), so while it shouldn't cause any problems, users probably would want to avoid having a lot of panicking tasks. But if you want to avoid the panics, you need to cancel the task before the signal object is freed. This could be hard to coordinate and people might prefer to have the signal future get stuck and then clean up whenever they are ready... What do you think?

> I disagree on making TaskHandle a must_use though, since spawned async task should live independently. If user wants to access object, they can do something like this:

the `#[must_use]` has nothing to do with the class instance and would come down to this:
```rust
let this = self.to_gd();

let _ = godot_task(async move {
    // Do other tasks, wait for signal, etc.

    // Access object, if object does not exist it should panics.
    this.bind();
});
```
The intention was that it would highlight that the task handle can be of importance. And hopefully, people would make an informed decision on what they want to do with it. But making it `#[must_use]` might be too annoying.

> I quite like the @coder137 suggestion of decoupling signal future and async runtime.

They are already decoupled. You should be able to use the futures with any runtime you like.
author_association: CONTRIBUTOR
user: login: TitanNano
created_at: 2025-02-18T22:27:17Z
body: After all the discussions about the futures in this PR, I have now made the following refactoring:
- the `GuaranteedSignalFuture` is now called `TrySignalFuture`
- the `TrySignalFuture` resolves now to a `Result<T, TrySignalFutureError>` (the error type itself should still be refined a bit).
- the `SignalFuture` is now a wrapper around the `TrySignalFuture` that turns the `Result::Err` into a `panic`.
author_association: CONTRIBUTOR
user: login: coder137
created_at: 2025-02-19T07:19:28Z
body: > the TrySignalFuture resolves now to a Result<T, TrySignalFutureError> (the error type itself should still be refined a bit).

To understand this better, under what conditions would we receive `Err(TrySignalFutureError)`?

In the PR I see it says if the Signal object is freed before the signal was emitted. Wouldn't the signal future also get freed in that case?
author_association: NONE
user: login: TitanNano
created_at: 2025-02-19T07:31:53Z
body: @coder137 See the `GuaranteedSignalFuture` example in this comment https://github.com/godot-rust/gdext/pull/1043#issuecomment-2663881844, but now it resolves to a `Result::Err` instead of `Option::None`. 
author_association: CONTRIBUTOR
user: login: coder137
created_at: 2025-02-20T17:32:05Z
body: Since `SignalFuture` can only get stuck in the event that the event is not fired.
What if we remove the `SignalFuture` and only keep the `TrySignalFuture` and give users a way to gracefully shutdown via `CancellationToken` (or something similar)? In the example it seems as though `TrySignalFuture` returns err once the game shuts down.

The reason I say this is because, user's might accidentally use `SignalFuture`'s in their code without completely understanding its usecase or their own logic as the game changes.
In the event that the game hangs, they would need to go through some effort in order to debug the problem. If there are multiple `SignalFuture`'s in the code, debugging the halts/freezes becomes harder.
author_association: NONE
user: login: TitanNano
created_at: 2025-02-20T19:01:56Z
body: @coder137 `SignalFuture` can no longer get stuck. It will produce a panic. So users who run into these panics can choose to either switch from the `TrySignalFuture` or work around the cause for the panic.

> In the example it seems as though TrySignalFuture returns err once the game shuts down.

No, it happens as soon as the signal object is freed. Of course, if you wait for a signal until the game shutdown, this could also happen during shutdown. But during shutdown, all pending tasks are also being canceled and dropped before the engine cleans up the scene tree, so it's unlikely to happen.
author_association: CONTRIBUTOR
user: login: jrb0001
created_at: 2025-02-22T14:21:06Z
body: > I am not completely sure yet. It doesn't happen if there are no open scenes or if none of them contains a node which spawns a Future.
> 
> It also doesn't seem to happen every single time if I close all scenes and then open one with a Future before triggering the hot-reload. In this case it panics _with some scenes_:
> 
> ```
> ERROR: godot-rust function call failed: <Callable>::GodotWaker::wake()
>     Reason: [panic]  Future no longer exists when waking it! This is a bug!
>   at /home/jrb0001/.cargo/git/checkouts/gdext-3ec94bd991a90eb6/2877010/godot-core/src/builtin/async_runtime.rs:271
> ```
> 
> With another scene it segfaults in this scenario.
> 
> Simply reopening the editor (same scene gets opened automatically) and then triggering a hot-reload segfaults for both scenes.
> 
> With both executor + Future from this PR, the hot-reload issue doesn't happen at all?!? So the issue could also be in my code, let me debug it properly before you waste more time on it.
> 
> I will do some more debugging later this week (probably weekend).

This is still the same behavior with the current commit, both the printed panic and the segfault.

```text
(lldb) bt
* thread #1, name = 'godot.linuxbsd.', stop reason = signal SIGSEGV: invalid permissions for mapped object (fault address: 0x7213a9b64980)
  * frame #0: 0x00007213a9b64980
    frame #1: 0x000057bde516208f godot.linuxbsd.editor.dev.x86_64`CallableCustomExtension::call(this=0x000057be33a2d150, p_arguments=0x0000000000000000, p_argcount=0, r_return_value=0x00007ffdeb3d9050, r_call_error=0x00007ffdeb3d9044) const at gdextension_interface.cpp:170:12
    frame #2: 0x000057bde4ddfb88 godot.linuxbsd.editor.dev.x86_64`Callable::callp(this=0x00007213a008a410, p_arguments=0x0000000000000000, p_argcount=0, r_return_value=0x00007ffdeb3d9050, r_call_error=0x00007ffdeb3d9044) const at callable.cpp:57:15
    frame #3: 0x000057bde51860d2 godot.linuxbsd.editor.dev.x86_64`CallQueue::_call_function(this=0x000057be174450f0, p_callable=0x00007213a008a410, p_args=0x00007213a008a428, p_argcount=0, p_show_error=true) at message_queue.cpp:220:18
    frame #4: 0x000057bde51864d5 godot.linuxbsd.editor.dev.x86_64`CallQueue::flush(this=0x000057be174450f0) at message_queue.cpp:268:20
    frame #5: 0x000057bde2aa6c73 godot.linuxbsd.editor.dev.x86_64`SceneTree::physics_process(this=0x000057be19f32eb0, p_time=0.016666666666666666) at scene_tree.cpp:492:38
    frame #6: 0x000057bde034fac7 godot.linuxbsd.editor.dev.x86_64`Main::iteration() at main.cpp:4070:60
    frame #7: 0x000057bde02866da godot.linuxbsd.editor.dev.x86_64`OS_LinuxBSD::run(this=0x00007ffdeb3d92e0) at os_linuxbsd.cpp:962:22
    frame #8: 0x000057bde027e555 godot.linuxbsd.editor.dev.x86_64`main(argc=5, argv=0x00007ffdeb3d9928) at godot_linuxbsd.cpp:85:9
    frame #9: 0x00007213bc0dbe08 libc.so.6`___lldb_unnamed_symbol3261 + 120
    frame #10: 0x00007213bc0dbecc libc.so.6`__libc_start_main + 140
    frame #11: 0x000057bde027e2f5 godot.linuxbsd.editor.dev.x86_64`_start + 37
```

`image list` before the reload, sorted by address:
```text
[ 68] DED83DF7-4521-915A-7BD6-E8990F4F802E-13B43F04 0x00007213a7db3000 /usr/lib/libasyncns.so.0 
[ 17] 7826DD70-4046-27B0-AEBE-4E42CA605707-322A0608 0x00007213a8e00000 /home/jrb0001/GodotProjects/vn-test/rust/target/x86_64-unknown-linux-gnu/debug/libgame.so 
[ 42] DE7A5D36-BC86-167E-D7DE-538A6F63CDC5-26014ACE 0x00007213a9e68000 /usr/lib/libwayland-client.so.0 
[ 41] 81E599CC-EF35-6DCD-56EE-E45E615603EA-2D605B22 0x00007213a9e77000 /usr/lib/libxcb-dri3.so.0 
```

So I guess there is a `Callable` still alive during the reload? Is there a way to figure out which one?

---

Another issue I am currently running into is that `godot_task()` polls the future immediately. Is this intentional? All other wake-ups go through a `call_deferred()`, only the first poll is different. It also didn't happen in an older iteration of this PR.
author_association: CONTRIBUTOR
user: login: TitanNano
created_at: 2025-02-22T15:10:03Z
body: > So I guess there is a Callable still alive during the reload? Is there a way to figure out which one?

Gut feeling for the callable is that Godot is not reloading the custom callable and tries to access the invalid pointer after unloading the old library.

I can look into this more.

----
> godot_task() polls the future immediately

Yes, this is intentional at the moment. There was the issue that signal futures inside an async block or function would only get created after the deferred initial poll from the `godot_task` and any signal that emitted right after calling `godot_task` would be ignored. 

What exact issue is it causing? 


author_association: CONTRIBUTOR
user: login: jrb0001
created_at: 2025-02-22T15:52:19Z
body: I am calling  `godot_task()` from `IButton::pressed()`  and the future ends up calling `Gd<MyButton>::set_focusmode()` which triggers a notification and it can't create a second mut borrow to call `IButton::on_notification`. The first await comes somewhere after that. I need to defer somewhere, but I can't defer the call to `godot_task()` itself because I need to store the `TaskHandle`. So I guess awaiting a future which simply wakes itself is the only solution.

With my old executor, I also abused async blocks without any awaits as a generic "call deferred" solution. Simply because it was easier than messing around with `Callable` and its `Send` bound. We can ignore this usecase in my opinion.
author_association: CONTRIBUTOR
user: login: TitanNano
created_at: 2025-02-23T19:10:27Z
body: > I am calling godot_task() from IButton::pressed() and the future ends up calling Gd<MyButton>::set_focusmode() which triggers a notification and it can't create a second mut borrow to call IButton::on_notification. The first await comes somewhere after that. I need to defer somewhere, but I can't defer the call to godot_task() itself because I need to store the TaskHandle. So I guess awaiting a future which simply wakes itself is the only solution.

Can't you do something like this?

```rust
let base = self.base_mut();
let handle = godot_task(...); 
drop(base);
self.task_handles.push(handle);
```

--- 

Unfortunately, I wasn't able to replicate the hot-reload issue on macOS yet. It might be Linux-specific. 

EDIT: can't replicate it in CI either.
author_association: CONTRIBUTOR
user: login: jrb0001
created_at: 2025-02-24T18:54:05Z
body: > Can't you do something like this?
> 
> ```rust
> let base = self.base_mut();
> let handle = godot_task(...); 
> drop(base);
> self.task_handles.push(handle);
> ```

Yes, that worked. Thanks for the idea!

> Unfortunately, I wasn't able to replicate the hot-reload issue on macOS yet. It might be Linux-specific.
> 
> EDIT: can't replicate it in CI either.

I think it is more likely something specific to my setup. I can't reproduce it with all my scenes, some just print panics without segfaulting. But for the affected scenes, it is 100% reproducible. I will try it in a clean project.

author_association: CONTRIBUTOR
user: login: jrb0001
created_at: 2025-02-25T20:35:05Z
body: Minimalistic repro project: [executor-segfault.zip](https://github.com/user-attachments/files/18971902/executor-segfault.zip)

```sh
cd rust
cargo build --target x86_64-unknown-linux-gnu
$GODOT4_BIN --editor --path ../project/ &
# Wait until project is fully loaded.
touch game/src/lib.rs
cargo build --target x86_64-unknown-linux-gnu
# Focus editor window --> segfault.
```

If you are not on linux, then just change the `x86_64-unknown-linux-gnu` to your target. The `.gdextension` file is configured for anything supported by the official export templates.

So it looks like this is caused by calling the `Waker` inside `drop()` of a (tool) `Node`.

author_association: CONTRIBUTOR
user: login: TitanNano
created_at: 2025-02-25T22:51:39Z
body: @jrb0001 it looks like your issue comes down to calling `call_deferred` on a callable during GDExtension deinitialization (The rust struct of the Node is dropped during deinitialization of the extension when hot-reloading it). Replicating your setup causes a panic for me on macOS:

```
unsafe precondition(s) violated: hint::unreachable_unchecked must never be reached
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread caused non-unwinding panic. aborting.
```

But on linux in CI it works perfectly fine (perhaps the timing is slightly different?). I think you can create a new issue for this edge-case of deferred calling callables during deinitialization.
author_association: CONTRIBUTOR
user: login: TitanNano
created_at: 2025-03-02T21:42:23Z
body: >     1. This PR adds a ton of functionality (1.2 kLOC and counting). I think at this point it makes sense to have a dedicated module instead of spilling all into `godot::builtin`.
>        What about `godot::task`, similar to the previous [`gdnative::tasks`](https://docs.rs/gdnative/latest/gdnative/tasks/index.html)? We can always move it around if we find it fits better into another module, but that's much easier once it is its own module already.

Yes, I think this is a great idea. `builtin` didn't feel like a great fit, but I didn't know where else to place it.

>     2. `godot_task` should probably be called something that contains "spawn", just like with all other async runtimes and `std::thread`. Maybe even just `spawn`. Another reason for the module, it could then be qualified as `godot::task::spawn(...)`.

I like this in combination with 1!

>     3. `TrySignalFuture` -- would it make sense to name it `FallibleSignalFuture` or so? A bit longer, but the type isn't named very often, and this seems to be the not-so-common case if I understood correctly.

I was thinking about `FallibleSignalFuture` as well, but it seemed a bit long. I'm fine with this name too.
author_association: CONTRIBUTOR
user: login: AsbjornOlling
created_at: 2025-03-10T13:57:34Z
body: A caveat of this solution (that I didn't see coming), is that it seems to not work with message passing from an OS thread.

E.g. with code like this:

```rust
let (tx, mut rx) = tokio::sync::mpsc::channel(1024);
tx.blocking_send("Hello from the main thread".to_string()).unwrap();

std::thread::spawn(move || {
    tx.blocking_send("Hello from a worker thread!".to_string()).unwrap();
});

godot::task::spawn(async move {
    while let Some(text) = rx.recv().await {
        debug!("Received: {text:?}");
    }
});
```

The first message comes though perfectly, but the second message (the one sent from a thread) produces this error:

```
ERROR: godot-rust function call failed: <Callable>::GodotWaker::wake()
    Reason: [panic]
  Callable 'GodotWaker::wake' created with from_local_fn() must be called from the same thread it was created in.
  If you need to call it from any thread, use from_sync_fn() instead (requires `experimental-threads` feature).
  at /home/asbjorn/.cargo/git/checkouts/gdext-c10fa258906ea04a/e5b215b/godot-core/src/builtin/callable.rs:585
   at: godot_core::private::report_call_error (/home/asbjorn/.cargo/git/checkouts/gdext-c10fa258906ea04a/e5b215b/godot-core/src/private.rs:335)
ERROR: Error calling deferred method: '': .
   at: _call_function (core/object/message_queue.cpp:222)
```

It might be obvious to someone more familiar with async rust than me; but I at least found it surprising.

It seems like it's currently not possible to communicate between OS threads and async tasks in the godot runtime?

<details>
<summary>
Foldable: Some more context about my use-case for an async runtime in godot
</summary>

I thought that an async runtime in godot would be perfect for my usecase, since my gdextension relies on waiting for some cpu-heavy work that happens on a separate work thread. It takes way longer to process one item than I can tolerate blocking rendering for, so the heavy to happen on a dedicated thread (and not a godot task).

My currently working solution regularly polls the output queues with `try_recv()` on `_physics_process`, and that seems to work fine. But once stuff gets a bit more stateful, it can quickly turn into some kind-of-complicated state machines.

Being able to write a long-running async task that waits for results from the cpu-bound thread, and then does the interactions with godot that need to happen on the main thread, would be really useful for me.
</details>
author_association: NONE
user: login: TitanNano
created_at: 2025-03-10T14:17:01Z
body: @AsbjornOlling this should be possible, but you have to enable the `experimental-threads` feature. This feature is off by default. 
author_association: CONTRIBUTOR
user: login: AsbjornOlling
created_at: 2025-03-10T14:21:50Z
body: > @AsbjornOlling this should be possible, but you have to enable the `experimental-threads` feature. This feature is off by default.

I have `experimental-threads` enabled.

If I run the same code from my example above *without* `experimental-threads`, I get this error instead:

```
attempted to access binding from different thread than main thread; this is UB - use the "experimental-threads" feature.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```
author_association: NONE
user: login: AsbjornOlling
created_at: 2025-03-10T15:05:24Z
body: Nice! This seems to work :heart: 
author_association: NONE
user: login: TitanNano
created_at: 2025-03-10T15:07:43Z
body: @AsbjornOlling thanks for pointing this out. It was indeed not working as intended.
author_association: CONTRIBUTOR
user: login: ColinWttt
created_at: 2025-03-11T17:41:48Z
body: It seems that `Gd<T>` cannot be passed as a parameter in type-safe signals, due to `Gd` not being Send + Sync.

```rust
        let task_handle = task::spawn(async move {
            let (ret,) = self.signals().active_card_ability().deref().to_future().await;
        });
```
```md
error[E0599]: the method `to_future` exists for reference `&TypedSignal<'_, CardManager, (Gd<AbilityContext>,)>`, but its trait bounds were not satisfied
   --> src\class\card_manager.rs:211:71
    |
211 |             let (ret,) = self.signals().active_card_ability().deref().to_future().await;
    |                                                                       ^^^^^^^^^ method cannot be called due to unsatisfied trait bounds
    |
   ::: ..\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib/rustlib/src/rust\library\core\src\cell.rs:311:1
    |
311 | pub struct Cell<T: ?Sized> {
    | -------------------------- doesn't satisfy `Cell<*mut __GdextClassInstance>: Sync`
    |
    = note: the following trait bounds were not satisfied:
            `*mut card_ability_context::AbilityContext: Sync`
            which is required by `(godot::prelude::Gd<card_ability_context::AbilityContext>,): Sync`
            `Cell<*mut __GdextClassInstance>: Sync`
            which is required by `(godot::prelude::Gd<card_ability_context::AbilityContext>,): Sync`
            `*mut card_ability_context::AbilityContext: Send`
            which is required by `(godot::prelude::Gd<card_ability_context::AbilityContext>,): Send`
            `*mut __GdextClassInstance: Send`
            which is required by `(godot::prelude::Gd<card_ability_context::AbilityContext>,): Send`

```
author_association: CONTRIBUTOR
user: login: Bromeon
created_at: 2025-03-11T18:02:26Z
body: > It seems that `Gd<T>` cannot be passed as a parameter in type-safe signals, due to `Gd` not being Send + Sync.

This comes from the `Send + Sync` bounds here:
```rs
pub struct SignalFuture<R: ParamTuple + Sync + Send>(FallibleSignalFuture<R>);
```

Those bounds are unnecessary for signals that are emitted on the main thread. (Awaiting must anyway happen on the main thread).

Was the intention here to support also signals emitted on other threads, as a cross-thread communication mechanism? If yes, we should probably add this later -- might need more thought regarding thread safety, and probably some version of #18.
author_association: MEMBER
user: login: TitanNano
created_at: 2025-03-11T19:54:33Z
body: > Those bounds are unnecessary for signals that are emitted on the main thread. (Awaiting must anyway happen on the main thread).

> Was the intention here to support also signals emitted on other threads, as a cross-thread communication mechanism? If yes, we should probably add this later -- might need more thought regarding thread safety, and probably some version of https://github.com/godot-rust/gdext/issues/18.

Yes, it's basically impossible to tell where a signal will be emitted, since any signal _can_ be emitted from any thread. We also use a `RustCallable` inside the future, and it has a `Send` + `Sync` bound. Making the future `Send` + `Sync` is the most reliable solution. 

For `Gd<T>` I was thinking about a follow-up PR where I would like to work out something like the `ThreadCrosser` in `itest` but with a runtime check if the value actually was moved between threads. I haven't worked on this yet, though.
author_association: CONTRIBUTOR
user: login: TitanNano
created_at: 2025-03-11T22:00:54Z
body: Thanks, everyone, for the great and productive feedback. 
author_association: CONTRIBUTOR
user: login: curtispuetz
created_at: 2025-11-22T23:48:22Z
body: Do we have any examples of how to use this? I tried using it, but I get an error saying there is no tokio runtime and that a tokio runtime is needed.
author_association: NONE
user: login: Bromeon
created_at: 2025-11-23T09:00:38Z
body: > Do we have any examples of how to use this?

The integration tests added in this PR should give you an idea :slightly_smiling_face: 
author_association: MEMBER
user: login: TitanNano
created_at: 2025-11-23T14:49:05Z
body: > I get an error saying there is no tokio runtime and that a tokio runtime is needed

This means you are using a crate that depends on Tokio. Godot-rust does not use Tokio. You can either look for a crate that does not require Tokio or use the `async-compat` crate to include a Tokio runtime when needed. Using `async-compat` is the easiest solution but probably also the least efficient one. 
author_association: CONTRIBUTOR