-
Notifications
You must be signed in to change notification settings - Fork 210

Description
Goal
The Godot API is rich in asynchronousness with its many signals. The interface is easy for GDScript to consume, in coroutines with the yield
function, but not very wieldy in Rust. Async-await may help close this gap. Wel want to be able to write something like this in Rust:
// Provisional API
#[export(spawn = "local")] // Indicates that the future will be spawned on a local executor, explained below
async fn my_coroutine(&self, owner: Node, anim: AnimationPlayer) -> String {
use gdnative::async_yield::Yield;
// - snip -
Yield::new(anim, "animation_finished").await;
// - snip -
"foo".into()
}
And be able to consume it in GDScript like this:
func foo(node):
# - snip -
var result = yield(node.my_coroutine(get_node("anim")), "completed")
print(result) # outputs "foo"
Proposed implementation
To accomplish this, there are four parts that need to be implemented, at minimum:
- The
Yield
Future
itself. This should just be a thin wrapper around aWaker
. - A
NativeClass
singleton that acts as a bridge between signals andWaker
s (onlyNativeClass
es can connect to signals, and giving eachYield
its own instance would be too expensive). - A
NativeClass
that wraps around boxedFuture
s and exposesresume
and acompleted
signal, analogous toGDScriptFunctionState
. - Support for
async
methods in the proc-macro.
For this feature to work, two supportive NativeClass
types need to be registered. It should be easiest for the users that we put the types behind a (optionally optional) async_yield
feature, and automatically register them on init if the feature enabled. If we give sufficiently long and specific names to these types, I think we can safely pray that collisions won't happen. The proc-macro can emit errors when async methods are encountered, if the feature flag is not enabled.
Executor to use
Futures are lazy and require an executor to actually progress. The futures
crate implements both a multi-thread executor and a single-thread one that we can use. However, both of them have some problems:
ThreadPool
can be used without manual intervention, but don't play well with Godot's generally thread-unsafe API. LocalPool
should enable more ergonomic async
functions, but needs to be polled from a Node
, which have to be manually put into the scene tree by the user, and has serious performance implications. Both options are not very ideal as a default.
As such, we have to require the users to provide their own executors in their init functions, and not provide a default:
// Provisional API
fn init(handle: init::InitHandle) {
// - snip -
gdnative::async_yield::set_spawner(Box::new(ThreadPool::new().unwrap());
gdnative::async_yield::set_local_spawner(Box::new(MY_LOCAL_POOL.spawner()));
}
// in a Node far, far away
#[export]
fn _process(&self, _owner: Node) {
// this certainly won't just compile, but essentially do something in the spirit of:
MY_LOCAL_POOL.run_until_stalled();
}
Unresolved questions
The feature should be reasonably useful, and the implementation itself should be fairly straightforward, but there are several problems I have with the current provisional API:
- The proposed module name and feature flag being
async_yield
. I don't think it's very good, butasync
andyield
are keywords and can't be used.futures
sound a bit too broad, on the other hand. - The spawners being global. Alternatively spawners can be specified for each method individually using the attribute, but it doesn't feel very useful.
- The
spawn = "local"
attribute argument. I feel it can be a bit confusing, but something like it is necessary since the methods inSpawn
andLocaSpawn
are named differently, and a macro has no way to tell whether the resultingFuture
will beSend
.
Opinions are very appreciated!