Skip to content

Commit 03c334e

Browse files
bors[bot]chitoyuutoasteater
authored
Merge #804
804: Minimal async-await foundations, updated r=Bromeon a=chitoyuu This is an updated and rebased version of #709, that is no longer blocked on #757. Notable changes outside the original PR include: - `TerminateInfo` and `InitializeInfo` are moved back into the public API. These types are used as arguments to custom init/terminate callbacks. Custom callbacks are necessary for calling the `gdnative_async` `register` and `terminate` functions. Currently, they are just moved into the top level of `gdnative_core`, which I think makes sense given their usage in top-level callbacks. Please let me know if a better place is available. - Renamed the top-level re-export from `asn` to `tasks`, since the original author apparently didn't like it. Supersedes #709 Co-authored-by: Chitose Yuuzaki <chitoyuu@potatoes.gay> Co-authored-by: toasteater <48371905+toasteater@users.noreply.github.com>
2 parents b466273 + 82f928e commit 03c334e

File tree

21 files changed

+1049
-177
lines changed

21 files changed

+1049
-177
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",

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ crate-type = ["cdylib"]
5656

5757
The bindings are currently generated from the API description of Godot 3.2.3-stable by default. To use the bindings with another version or a custom build, see [Using custom builds of Godot](https://godot-rust.github.io/book/advanced-guides/custom-bindings.html) in the user guide.
5858

59+
### Async / "`yield`" support
60+
61+
Async support is a work-in-progress, with a low-level API available in the `gdnative-async` crate. This crate is re-exported as `gdnative::tasks`, if the `async` feature is enabled on `gdnative`.
62+
5963
## Example
6064

6165
The most general use-case of the bindings will be to interact with Godot using the generated wrapper

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.17"
20+
atomic-waker = "1.0.0"
21+
once_cell = "1.8.0"
22+
parking_lot = "0.11.2"
23+
crossbeam-channel = "0.5.1"
24+
crossbeam-utils = "0.8.5"
25+
26+
[build-dependencies]

gdnative-async/src/executor.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
use std::cell::Cell;
2+
3+
use futures_task::LocalSpawn;
4+
5+
thread_local!(
6+
static LOCAL_SPAWN: Cell<Option<&'static dyn LocalSpawn>> = Cell::new(None);
7+
);
8+
9+
pub(crate) fn local_spawn() -> Option<&'static dyn LocalSpawn> {
10+
LOCAL_SPAWN.with(|cell| cell.get())
11+
}
12+
13+
/// Sets the global executor for the current thread to a `Box<dyn LocalSpawn>`. This value is leaked.
14+
pub fn set_boxed_executor(sp: Box<dyn LocalSpawn>) {
15+
set_executor(Box::leak(sp))
16+
}
17+
18+
/// Sets the global executor for the current thread to a `&'static dyn LocalSpawn`.
19+
pub fn set_executor(sp: &'static dyn LocalSpawn) {
20+
LOCAL_SPAWN.with(|cell| cell.set(Some(sp)))
21+
}

gdnative-async/src/future.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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+
/// Future that can be `await`ed for a signal or a `resume` call from Godot. See
24+
/// [`Context`](crate::Context) for methods that return this future.
25+
pub struct Yield<T> {
26+
waker: Arc<AtomicWaker>,
27+
arg_recv: Receiver<T>,
28+
}
29+
30+
impl<T: Send> Future for Yield<T> {
31+
type Output = T;
32+
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
33+
match self.arg_recv.try_recv() {
34+
Ok(arg) => Poll::Ready(arg),
35+
Err(_) => {
36+
self.waker.register(cx.waker());
37+
Poll::Pending
38+
}
39+
}
40+
}
41+
}
42+
43+
pub(crate) struct Resume<T> {
44+
waker: Arc<AtomicWaker>,
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+
self.waker.wake();
56+
}
57+
}

gdnative-async/src/lib.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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::{set_boxed_executor, set_executor};
18+
pub use future::Yield;
19+
pub use method::{Async, AsyncMethod, Spawner};
20+
pub use rt::{register_runtime, terminate_runtime, Context};

gdnative-async/src/method.rs

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

gdnative-async/src/rt.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
use std::marker::PhantomData;
2+
3+
use gdnative_bindings::Object;
4+
use gdnative_core::object::SubClass;
5+
6+
use gdnative_core::core_types::{GodotError, Variant};
7+
use gdnative_core::nativescript::export::InitHandle;
8+
use gdnative_core::nativescript::{Instance, RefInstance};
9+
use gdnative_core::object::ownership::Shared;
10+
use gdnative_core::object::TRef;
11+
12+
use crate::future;
13+
14+
mod bridge;
15+
mod func_state;
16+
17+
use func_state::FuncState;
18+
19+
/// Context for creating `yield`-like futures in async methods.
20+
pub struct Context {
21+
func_state: Instance<FuncState, Shared>,
22+
/// Remove Send and Sync
23+
_marker: PhantomData<*const ()>,
24+
}
25+
26+
impl Context {
27+
pub(crate) fn new() -> Self {
28+
Context {
29+
func_state: FuncState::new().into_shared(),
30+
_marker: PhantomData,
31+
}
32+
}
33+
34+
pub(crate) fn func_state(&self) -> Instance<FuncState, Shared> {
35+
self.func_state.clone()
36+
}
37+
38+
fn safe_func_state(&self) -> RefInstance<'_, FuncState, Shared> {
39+
// SAFETY: FuncState objects are bound to their origin threads in Rust, and
40+
// Context is !Send, so this is safe to call within this type.
41+
// Non-Rust code is expected to be following the official guidelines as per
42+
// the global safety assumptions. Since a reference of `FuncState` is held by
43+
// Rust, it voids the assumption to send the reference to any thread aside from
44+
// the one where it's created.
45+
unsafe { self.func_state.assume_safe() }
46+
}
47+
48+
pub(crate) fn resolve(&self, value: Variant) {
49+
func_state::resolve(self.safe_func_state(), value);
50+
}
51+
52+
/// Returns a future that waits until the corresponding `FunctionState` object
53+
/// is manually resumed from GDScript, and yields the argument to `resume` or `Nil`
54+
/// if nothing is passed.
55+
///
56+
/// Calling this function will put the associated `FunctionState`-like object in
57+
/// resumable state, and will make it emit a `resumable` signal if it isn't in that
58+
/// state already.
59+
///
60+
/// Only the most recent future created from this `Context` is guaranteed to resolve
61+
/// upon a `resume` call. If any previous futures weren't `await`ed to completion, they
62+
/// are no longer guaranteed to resolve, and have unspecified, but safe behavior
63+
/// when polled.
64+
pub fn until_resume(&self) -> future::Yield<Variant> {
65+
let (future, resume) = future::make();
66+
func_state::make_resumable(self.safe_func_state(), resume);
67+
future
68+
}
69+
70+
/// Returns a future that waits until the specified signal is emitted, if connection succeeds.
71+
/// Yields any arguments emitted with the signal.
72+
///
73+
/// Only the most recent future created from this `Context` is guaranteed to resolve
74+
/// when the signal is emitted. If any previous futures weren't `await`ed to completion, they
75+
/// are no longer guaranteed to resolve, and have unspecified, but safe behavior
76+
/// when polled.
77+
///
78+
/// # Errors
79+
///
80+
/// If connection to the signal failed.
81+
pub fn signal<C>(
82+
&self,
83+
obj: TRef<'_, C>,
84+
signal: &str,
85+
) -> Result<future::Yield<Vec<Variant>>, GodotError>
86+
where
87+
C: SubClass<Object>,
88+
{
89+
let (future, resume) = future::make();
90+
bridge::SignalBridge::connect(obj.upcast(), signal, resume)?;
91+
Ok(future)
92+
}
93+
}
94+
95+
/// Adds required supporting NativeScript classes to `handle`. This must be called once and
96+
/// only once per initialization.
97+
pub fn register_runtime(handle: &InitHandle) {
98+
handle.add_class::<bridge::SignalBridge>();
99+
handle.add_class::<func_state::FuncState>();
100+
}
101+
102+
/// Releases all observers still in use. This should be called in the
103+
/// `godot_gdnative_terminate` callback.
104+
pub fn terminate_runtime() {
105+
bridge::terminate();
106+
}

0 commit comments

Comments
 (0)