Skip to content

Commit 5891c5b

Browse files
committed
Async Runtime POC
1 parent ced9f71 commit 5891c5b

File tree

4 files changed

+222
-0
lines changed

4 files changed

+222
-0
lines changed

godot-core/src/tools/async_support.rs

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
use std::cell::RefCell;
2+
use std::future::Future;
3+
use std::pin::Pin;
4+
use std::sync::{Arc, Mutex};
5+
use std::task::{Context, Poll, Wake, Waker};
6+
7+
use crate::builtin::{Callable, Signal, Variant};
8+
use crate::classes::object::ConnectFlags;
9+
use crate::godot_error;
10+
use crate::meta::FromGodot;
11+
use crate::obj::EngineEnum;
12+
13+
pub fn godot_task(future: impl Future<Output = ()> + 'static) {
14+
let waker: Waker = ASYNC_RUNTIME.with_borrow_mut(move |rt| {
15+
let task_index = rt.add_task(Box::pin(future));
16+
Arc::new(GodotWaker::new(task_index)).into()
17+
});
18+
19+
waker.wake();
20+
}
21+
22+
thread_local! { static ASYNC_RUNTIME: RefCell<AsyncRuntime> = RefCell::new(AsyncRuntime::new()); }
23+
24+
struct AsyncRuntime {
25+
tasks: Vec<Option<Pin<Box<dyn Future<Output = ()>>>>>,
26+
}
27+
28+
impl AsyncRuntime {
29+
fn new() -> Self {
30+
Self {
31+
tasks: Vec::with_capacity(10),
32+
}
33+
}
34+
35+
fn add_task<F: Future<Output = ()> + 'static>(&mut self, future: F) -> usize {
36+
let slot = self
37+
.tasks
38+
.iter_mut()
39+
.enumerate()
40+
.find(|(_, slot)| slot.is_none());
41+
42+
let boxed = Box::pin(future);
43+
44+
match slot {
45+
Some((index, slot)) => {
46+
*slot = Some(boxed);
47+
index
48+
}
49+
None => {
50+
self.tasks.push(Some(boxed));
51+
self.tasks.len() - 1
52+
}
53+
}
54+
}
55+
56+
fn get_task(&mut self, index: usize) -> Option<Pin<&mut (dyn Future<Output = ()> + 'static)>> {
57+
let slot = self.tasks.get_mut(index);
58+
59+
slot.and_then(|inner| inner.as_mut())
60+
.map(|fut| fut.as_mut())
61+
}
62+
63+
fn clear_task(&mut self, index: usize) {
64+
if index >= self.tasks.len() {
65+
return;
66+
}
67+
68+
self.tasks[0] = None;
69+
}
70+
}
71+
72+
struct GodotWaker {
73+
runtime_index: usize,
74+
}
75+
76+
impl GodotWaker {
77+
fn new(index: usize) -> Self {
78+
Self {
79+
runtime_index: index,
80+
}
81+
}
82+
}
83+
84+
impl Wake for GodotWaker {
85+
fn wake(self: std::sync::Arc<Self>) {
86+
let waker: Waker = self.clone().into();
87+
let mut ctx = Context::from_waker(&waker);
88+
89+
ASYNC_RUNTIME.with_borrow_mut(|rt| {
90+
let Some(future) = rt.get_task(self.runtime_index) else {
91+
godot_error!("Future no longer exists! This is a bug!");
92+
return;
93+
};
94+
95+
// this does currently not support nested tasks.
96+
let result = future.poll(&mut ctx);
97+
match result {
98+
Poll::Pending => (),
99+
Poll::Ready(()) => rt.clear_task(self.runtime_index),
100+
}
101+
});
102+
}
103+
}
104+
105+
pub struct SignalFuture<R: FromSignalArgs> {
106+
state: Arc<Mutex<(Option<R>, Option<Waker>)>>,
107+
}
108+
109+
impl<R: FromSignalArgs> SignalFuture<R> {
110+
fn new(signal: Signal) -> Self {
111+
let state = Arc::new(Mutex::new((None, Option::<Waker>::None)));
112+
let callback_state = state.clone();
113+
114+
// the callable currently requires that the return value is Sync + Send
115+
signal.connect(
116+
Callable::from_fn("async_task", move |args: &[&Variant]| {
117+
let mut lock = callback_state.lock().unwrap();
118+
let waker = lock.1.take();
119+
120+
lock.0.replace(R::from_args(args));
121+
drop(lock);
122+
123+
if let Some(waker) = waker {
124+
waker.wake();
125+
}
126+
127+
Ok(Variant::nil())
128+
}),
129+
ConnectFlags::ONE_SHOT.ord() as i64,
130+
);
131+
132+
Self { state }
133+
}
134+
}
135+
136+
impl<R: FromSignalArgs> Future for SignalFuture<R> {
137+
type Output = R;
138+
139+
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
140+
let mut lock = self.state.lock().unwrap();
141+
142+
if let Some(result) = lock.0.take() {
143+
return Poll::Ready(result);
144+
}
145+
146+
lock.1.replace(cx.waker().clone());
147+
148+
Poll::Pending
149+
}
150+
}
151+
152+
pub trait FromSignalArgs: Sync + Send + 'static {
153+
fn from_args(args: &[&Variant]) -> Self;
154+
}
155+
156+
impl<R: FromGodot + Sync + Send + 'static> FromSignalArgs for R {
157+
fn from_args(args: &[&Variant]) -> Self {
158+
args.first()
159+
.map(|arg| (*arg).to_owned())
160+
.unwrap_or_default()
161+
.to()
162+
}
163+
}
164+
165+
// more of these should be generated via macro to support more than two signal arguments
166+
impl<R1: FromGodot + Sync + Send + 'static, R2: FromGodot + Sync + Send + 'static> FromSignalArgs
167+
for (R1, R2)
168+
{
169+
fn from_args(args: &[&Variant]) -> Self {
170+
(args[0].to(), args[0].to())
171+
}
172+
}
173+
174+
// Signal should implement IntoFuture for convenience. Keeping ToSignalFuture around might still be desirable, though. It allows to reuse i
175+
// the same signal instance multiple times.
176+
pub trait ToSignalFuture<R: FromSignalArgs> {
177+
fn to_future(&self) -> SignalFuture<R>;
178+
}
179+
180+
impl<R: FromSignalArgs> ToSignalFuture<R> for Signal {
181+
fn to_future(&self) -> SignalFuture<R> {
182+
SignalFuture::new(self.clone())
183+
}
184+
}

godot-core/src/tools/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010
//! Contains functionality that extends existing Godot classes and functions, to make them more versatile
1111
//! or better integrated with Rust.
1212
13+
mod async_support;
1314
mod gfile;
1415
mod save_load;
1516
mod translate;
1617

18+
pub use async_support::*;
1719
pub use gfile::*;
1820
pub use save_load::*;
1921
pub use translate::*;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use godot::builtin::Signal;
2+
use godot::classes::{Engine, SceneTree};
3+
use godot::global::godot_print;
4+
use godot::tools::{godot_task, ToSignalFuture};
5+
6+
use crate::framework::itest;
7+
8+
async fn call_async_fn(signal: Signal) -> u8 {
9+
let value = 5;
10+
11+
let _: () = signal.to_future().await;
12+
13+
value + 5
14+
}
15+
16+
#[itest]
17+
fn start_async_task() {
18+
let tree = Engine::singleton()
19+
.get_main_loop()
20+
.unwrap()
21+
.cast::<SceneTree>();
22+
23+
let signal = Signal::from_object_signal(&tree, "process_frame");
24+
25+
godot_print!("starting godot_task...");
26+
godot_task(async move {
27+
godot_print!("running async task...");
28+
let result = call_async_fn(signal).await;
29+
godot_print!("got async result...");
30+
31+
assert_eq!(result, 10);
32+
godot_print!("assertion done, async task complete!");
33+
});
34+
godot_print!("after godot_task...");
35+
}

itest/rust/src/engine_tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
66
*/
77

8+
mod async_test;
89
mod codegen_enums_test;
910
mod codegen_test;
1011
mod gfile_test;

0 commit comments

Comments
 (0)