Skip to content

[discussion] Async-await support #284

@ghost

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 a Waker.
  • A NativeClass singleton that acts as a bridge between signals and Wakers (only NativeClasses can connect to signals, and giving each Yield its own instance would be too expensive).
  • A NativeClass that wraps around boxed Futures and exposes resume and a completed signal, analogous to GDScriptFunctionState.
  • 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, but async and yield 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 in Spawn and LocaSpawn are named differently, and a macro has no way to tell whether the resulting Future will be Send.

Opinions are very appreciated!

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureAdds functionality to the library

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions