Skip to content

Commit 671a2f1

Browse files
bors[bot]toasteater
andauthored
Merge #709
709: Minimal async-await foundations r=toasteater a=toasteater This sets the foundations for async-await support in godot-rust, based on the original idea in #284. However, although the tests work, this is not a full implementation: - Async methods can only be registered manually using `build_method`. Macro syntax and implementation are out of the scope of this PR. - The runtime types aren't registered automatically yet. Users need to manually call `register_runtime` and `terminate_runtime` functions in their library lifecycle hooks. Improving this is out of the scope of this PR for now. - The crate is currently re-exported as `gdnative::asn`, instead of the much longer `async_yield`. The name is open to discussion -- I don't like it very much. - Only local spawners are supported, due to issues with thread safety. Users may off-load tasks that don't contain `yield`-likes to thread pool spawners using something like `futures::future::Remote`, however. - Panics in async methods don't currently behave very well. Their `FunctionState`-likes simply block forever and any outstanding bridge objects for futures can be leaked. - - - While the feature is not yet complete, the commit is already pretty big, and I feel that it's in a somewhat usable state. As a result, I'm putting this up as a draft PR to gather some feedback. If you have uses for async-await / "yield" from GDScript, please feel free to try it and tell me what you think! Registering an async method currently looks like this (excerpt from the tests): ```rust struct ResumeAddFn; impl AsyncMethod<AsyncMethods> for ResumeAddFn { fn spawn_with(&self, spawner: Spawner<'_, AsyncMethods>) { spawner.spawn(|ctx, _this, mut args| { let a = args.read::<i32>().get().unwrap(); let obj = args.read::<Ref<Object>>().get().unwrap(); let name = args.read::<GodotString>().get().unwrap(); async move { let b = ctx.until_resume().await; let b = i32::from_variant(&b).unwrap(); let c = unsafe { obj.assume_safe().call(name, &[]) }; let c = Ref::<Reference>::from_variant(&c).unwrap(); let c = unsafe { c.assume_safe() }; let c = ctx.signal(c, "completed").unwrap().await; assert_eq!(1, c.len()); let c = i32::from_variant(&c[0]).unwrap(); (a + b + c).to_variant() } }); } } fn register_methods(builder: &ClassBuilder<AsyncMethods>) { builder .build_method("resume_add", Async::new(ResumeAddFn)) .done(); } ``` Using it is almost like any other GDScript coroutine: ```gdscript func _async_call(obj): var fn_state = obj.resume_add(1, self, "_get_async_number") # the "resumable" signal is unique to Rust, since unlike GDScript coroutines, # Rust futures aren't guaranteed to be polled up to a yield right after spawning. yield(fn_state, "resumable") fn_state = fn_state.resume(2) # no "resumable" signal when awaiting signals, though var result = yield(fn_state, "completed") assert(result == 42) func _get_async_number(): yield(get_tree().create_timer(0.1), "timeout") return 39 ``` Co-authored-by: toasteater <[email protected]>
2 parents 37a35c5 + 3e5b9f0 commit 671a2f1

File tree

16 files changed

+986
-74
lines changed

16 files changed

+986
-74
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[workspace]
22
members = [
33
"gdnative",
4+
"gdnative-async",
45
"gdnative-bindings",
56
"gdnative-core",
67
"gdnative-derive",

gdnative-async/Cargo.toml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[package]
2+
name = "gdnative-async"
3+
authors = ["The godot-rust developers"]
4+
description = "Runtime async support for godot-rust."
5+
documentation = "https://docs.rs/crate/gdnative-async"
6+
repository = "https://github.com/godot-rust/godot-rust"
7+
homepage = "https://godot-rust.github.io/"
8+
version = "0.9.3"
9+
license = "MIT"
10+
workspace = ".."
11+
edition = "2018"
12+
13+
[features]
14+
15+
[dependencies]
16+
gdnative-derive = { path = "../gdnative-derive", version = "=0.9.3" }
17+
gdnative-core = { path = "../gdnative-core", version = "=0.9.3" }
18+
gdnative-bindings = { path = "../gdnative-bindings", version = "=0.9.3" }
19+
futures-task = "0.3.13"
20+
once_cell = "1.7.2"
21+
thiserror = "1.0"
22+
parking_lot = "0.11.0"
23+
crossbeam-channel = "0.5.0"
24+
25+
[build-dependencies]

gdnative-async/src/executor.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
use futures_task::LocalSpawn;
2+
use once_cell::unsync::OnceCell as UnsyncCell;
3+
use thiserror::Error;
4+
5+
thread_local!(
6+
static LOCAL_SPAWN: UnsyncCell<&'static dyn LocalSpawn> = UnsyncCell::new();
7+
);
8+
9+
/// Error returned by `set_*_executor` if an executor of the kind has already been set.
10+
#[derive(Error, Debug)]
11+
#[error("an executor is already set")]
12+
pub struct SetExecutorError {
13+
_private: (),
14+
}
15+
16+
impl SetExecutorError {
17+
fn new() -> Self {
18+
SetExecutorError { _private: () }
19+
}
20+
}
21+
22+
pub(crate) fn local_spawn() -> Option<&'static dyn LocalSpawn> {
23+
LOCAL_SPAWN.with(|cell| cell.get().copied())
24+
}
25+
26+
/// Sets the global executor for the current thread to a `Box<dyn LocalSpawn>`. This value is leaked.
27+
pub fn set_boxed_executor(sp: Box<dyn LocalSpawn>) -> Result<(), SetExecutorError> {
28+
set_executor(Box::leak(sp))
29+
}
30+
31+
/// Sets the global executor for the current thread to a `&'static dyn LocalSpawn`.
32+
pub fn set_executor(sp: &'static dyn LocalSpawn) -> Result<(), SetExecutorError> {
33+
LOCAL_SPAWN.with(|cell| cell.set(sp).map_err(|_| SetExecutorError::new()))
34+
}
35+
36+
/// Sets the global executor for the current thread with a function that will only be called
37+
/// if an executor isn't set yet.
38+
pub fn ensure_executor_with<F>(f: F)
39+
where
40+
F: FnOnce() -> &'static dyn LocalSpawn,
41+
{
42+
LOCAL_SPAWN.with(|cell| {
43+
cell.get_or_init(f);
44+
});
45+
}

gdnative-async/src/future.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use std::future::Future;
2+
use std::pin::Pin;
3+
use std::sync::Arc;
4+
use std::task::{Context, Poll, Waker};
5+
6+
use crossbeam_channel::{Receiver, Sender};
7+
use parking_lot::Mutex;
8+
9+
pub(crate) fn make<T>() -> (Yield<T>, Resume<T>) {
10+
let (arg_send, arg_recv) = crossbeam_channel::bounded(1);
11+
let waker = Arc::default();
12+
13+
let future = Yield {
14+
waker: Arc::clone(&waker),
15+
arg_recv,
16+
};
17+
18+
let resume = Resume { waker, arg_send };
19+
20+
(future, resume)
21+
}
22+
23+
/// Signal
24+
pub struct Yield<T> {
25+
waker: Arc<Mutex<Option<Waker>>>,
26+
arg_recv: Receiver<T>,
27+
}
28+
29+
impl<T: Send> Future for Yield<T> {
30+
type Output = T;
31+
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
32+
match self.arg_recv.try_recv() {
33+
Ok(arg) => Poll::Ready(arg),
34+
Err(_) => {
35+
let mut waker = self.waker.lock();
36+
*waker = Some(cx.waker().clone());
37+
Poll::Pending
38+
}
39+
}
40+
}
41+
}
42+
43+
pub(crate) struct Resume<T> {
44+
waker: Arc<Mutex<Option<Waker>>>,
45+
arg_send: Sender<T>,
46+
}
47+
48+
impl<T: Send> Resume<T> {
49+
/// Resume the task with a given argument from GDScript.
50+
pub fn resume(self, arg: T) {
51+
self.arg_send
52+
.send(arg)
53+
.expect("sender should not become disconnected");
54+
55+
if let Some(waker) = self.waker.lock().take() {
56+
waker.wake();
57+
}
58+
}
59+
}

gdnative-async/src/lib.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//! Runtime async support for godot-rust.
2+
//!
3+
//! This crate contains types and functions that enable using async code with godot-rust.
4+
//!
5+
//! # Safety assumptions
6+
//!
7+
//! This crate assumes that all user non-Rust code follow the official threading guidelines.
8+
9+
// Workaround for macros that expect the `gdnative` crate.
10+
extern crate gdnative_core as gdnative;
11+
12+
mod executor;
13+
mod future;
14+
mod method;
15+
mod rt;
16+
17+
pub use executor::{ensure_executor_with, set_boxed_executor, set_executor, SetExecutorError};
18+
pub use method::{Async, AsyncMethod, Spawner};
19+
pub use rt::{register_runtime, terminate_runtime, Context, InitError};

gdnative-async/src/method.rs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
use std::future::Future;
2+
use std::marker::PhantomData;
3+
use std::sync::Arc;
4+
5+
use futures_task::{LocalFutureObj, LocalSpawn, SpawnError};
6+
7+
use gdnative_core::core_types::{ToVariant, Variant};
8+
use gdnative_core::log::{self, Site};
9+
use gdnative_core::nativescript::init::{Method, Varargs};
10+
use gdnative_core::nativescript::{NativeClass, RefInstance};
11+
use gdnative_core::thread_access::Shared;
12+
13+
use crate::rt::Context;
14+
15+
/// Trait for async methods. When exported, such methods return `FunctionState`-like
16+
/// objects that can be manually resumed or yielded to completion.
17+
///
18+
/// Async methods are always spawned locally on the thread where they were created,
19+
/// and never sent to another thread. This is so that we can ensure the safety of
20+
/// emitting signals from the `FunctionState`-like object. If you need to off-load
21+
/// some task to another thread, consider using something like
22+
/// `futures::future::Remote` to spawn it remotely on a thread pool.
23+
pub trait AsyncMethod<C: NativeClass>: Send + Sync + 'static {
24+
/// Spawns the future for result of this method with `spawner`. This is done so
25+
/// that implementors of this trait do not have to name their future types.
26+
///
27+
/// If the `spawner` object is not used, the method call will fail, output an error,
28+
/// and return a `Nil` variant.
29+
fn spawn_with(&self, spawner: Spawner<'_, C>);
30+
31+
/// Returns an optional site where this method is defined. Used for logging errors in FFI wrappers.
32+
///
33+
/// Default implementation returns `None`.
34+
#[inline]
35+
fn site() -> Option<Site<'static>> {
36+
None
37+
}
38+
}
39+
40+
pub struct Spawner<'a, C: NativeClass> {
41+
sp: &'static dyn LocalSpawn,
42+
ctx: Context,
43+
this: RefInstance<'a, C, Shared>,
44+
args: Varargs<'a>,
45+
result: &'a mut Option<Result<(), SpawnError>>,
46+
_marker: PhantomData<*const ()>,
47+
}
48+
49+
impl<'a, C: NativeClass> Spawner<'a, C> {
50+
/// Consumes this `Spawner` and spawns a future returned by the closure. This indirection
51+
/// is necessary so that implementors of the `AsyncMethod` trait do not have to name their
52+
/// future types.
53+
pub fn spawn<F, R>(self, f: F)
54+
where
55+
F: FnOnce(Arc<Context>, RefInstance<'_, C, Shared>, Varargs<'_>) -> R,
56+
R: Future<Output = Variant> + 'static,
57+
{
58+
let ctx = Arc::new(self.ctx);
59+
let future = f(Arc::clone(&ctx), self.this, self.args);
60+
*self.result = Some(self.sp.spawn_local_obj(LocalFutureObj::new(Box::new(async {
61+
let value = future.await;
62+
ctx.resolve(value);
63+
drop(ctx);
64+
}))));
65+
}
66+
}
67+
68+
/// Adapter for async methods that implements `Method` and can be registered.
69+
#[derive(Clone, Copy, Default, Debug)]
70+
pub struct Async<F> {
71+
f: F,
72+
}
73+
74+
impl<F> Async<F> {
75+
/// Wrap `f` in an adapter that implements `Method`.
76+
#[inline]
77+
pub fn new(f: F) -> Self {
78+
Async { f }
79+
}
80+
}
81+
82+
impl<C: NativeClass, F: AsyncMethod<C>> Method<C> for Async<F> {
83+
fn call(&self, this: RefInstance<'_, C, Shared>, args: Varargs<'_>) -> Variant {
84+
if let Some(sp) = crate::executor::local_spawn() {
85+
let ctx = Context::new();
86+
let func_state = ctx.func_state();
87+
88+
let mut result = None;
89+
self.f.spawn_with(Spawner {
90+
sp,
91+
ctx,
92+
this,
93+
args,
94+
result: &mut result,
95+
_marker: PhantomData,
96+
});
97+
98+
match result {
99+
Some(Ok(())) => func_state.to_variant(),
100+
Some(Err(err)) => {
101+
log::error(
102+
Self::site().unwrap_or_default(),
103+
format_args!("unable to spawn future: {}", err),
104+
);
105+
Variant::new()
106+
}
107+
None => {
108+
log::error(
109+
Self::site().unwrap_or_default(),
110+
format_args!("implementation did not spawn a future"),
111+
);
112+
Variant::new()
113+
}
114+
}
115+
} else {
116+
log::error(
117+
Self::site().unwrap_or_default(),
118+
"a global executor must be set before any async methods can be called on this thread",
119+
);
120+
Variant::new()
121+
}
122+
}
123+
}

0 commit comments

Comments
 (0)