Skip to content

Commit ffe0248

Browse files
toasteaterchitoyuu
authored andcommitted
Minimal async-await foundations
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.
1 parent afc16aa commit ffe0248

File tree

18 files changed

+1001
-75
lines changed

18 files changed

+1001
-75
lines changed

.github/workflows/release-version.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,6 @@ jobs:
4444
sleep 1m;
4545
(cd gdnative-bindings && cargo publish);
4646
sleep 1m;
47+
(cd gdnative-async && cargo publish);
48+
sleep 1m;
4749
(cd gdnative && cargo publish);

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: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
atomic-waker = "1.0.0"
21+
once_cell = "1.7.2"
22+
thiserror = "1.0"
23+
parking_lot = "0.11.0"
24+
crossbeam-channel = "0.5.0"
25+
26+
[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: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
use std::future::Future;
2+
use std::pin::Pin;
3+
use std::sync::Arc;
4+
use std::task::{Context, Poll};
5+
6+
use atomic_waker::AtomicWaker;
7+
use crossbeam_channel::{Receiver, Sender};
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<AtomicWaker>,
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+
self.waker.register(cx.waker());
36+
Poll::Pending
37+
}
38+
}
39+
}
40+
}
41+
42+
pub(crate) struct Resume<T> {
43+
waker: Arc<AtomicWaker>,
44+
arg_send: Sender<T>,
45+
}
46+
47+
impl<T: Send> Resume<T> {
48+
/// Resume the task with a given argument from GDScript.
49+
pub fn resume(self, arg: T) {
50+
self.arg_send
51+
.send(arg)
52+
.expect("sender should not become disconnected");
53+
54+
self.waker.wake();
55+
}
56+
}

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: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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::export::{Method, Varargs};
10+
use gdnative_core::nativescript::{NativeClass, RefInstance};
11+
use gdnative_core::object::ownership::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+
/// Remove Send and Sync
47+
_marker: PhantomData<*const ()>,
48+
}
49+
50+
impl<'a, C: NativeClass> Spawner<'a, C> {
51+
/// Consumes this `Spawner` and spawns a future returned by the closure. This indirection
52+
/// is necessary so that implementors of the `AsyncMethod` trait do not have to name their
53+
/// future types.
54+
pub fn spawn<F, R>(self, f: F)
55+
where
56+
F: FnOnce(Arc<Context>, RefInstance<'_, C, Shared>, Varargs<'_>) -> R,
57+
R: Future<Output = Variant> + 'static,
58+
{
59+
let ctx = Arc::new(self.ctx);
60+
let future = f(Arc::clone(&ctx), self.this, self.args);
61+
*self.result = Some(
62+
self.sp
63+
.spawn_local_obj(LocalFutureObj::new(Box::new(async move {
64+
let value = future.await;
65+
ctx.resolve(value);
66+
}))),
67+
);
68+
}
69+
}
70+
71+
/// Adapter for async methods that implements `Method` and can be registered.
72+
#[derive(Clone, Copy, Default, Debug)]
73+
pub struct Async<F> {
74+
f: F,
75+
}
76+
77+
impl<F> Async<F> {
78+
/// Wrap `f` in an adapter that implements `Method`.
79+
#[inline]
80+
pub fn new(f: F) -> Self {
81+
Async { f }
82+
}
83+
}
84+
85+
impl<C: NativeClass, F: AsyncMethod<C>> Method<C> for Async<F> {
86+
fn call(&self, this: RefInstance<'_, C, Shared>, args: Varargs<'_>) -> Variant {
87+
if let Some(sp) = crate::executor::local_spawn() {
88+
let ctx = Context::new();
89+
let func_state = ctx.func_state();
90+
91+
let mut result = None;
92+
self.f.spawn_with(Spawner {
93+
sp,
94+
ctx,
95+
this,
96+
args,
97+
result: &mut result,
98+
_marker: PhantomData,
99+
});
100+
101+
match result {
102+
Some(Ok(())) => func_state.to_variant(),
103+
Some(Err(err)) => {
104+
log::error(
105+
Self::site().unwrap_or_default(),
106+
format_args!("unable to spawn future: {}", err),
107+
);
108+
Variant::new()
109+
}
110+
None => {
111+
log::error(
112+
Self::site().unwrap_or_default(),
113+
format_args!("implementation did not spawn a future"),
114+
);
115+
Variant::new()
116+
}
117+
}
118+
} else {
119+
log::error(
120+
Self::site().unwrap_or_default(),
121+
"a global executor must be set before any async methods can be called on this thread",
122+
);
123+
Variant::new()
124+
}
125+
}
126+
}

0 commit comments

Comments
 (0)