Recipe: Async with Tokio runtime
This recipe is based off of the test written for gdnative-async
, which uses the futures
crate in the executor. For cases where you may need a tokio
runtime, it is possible to execute spawned tokio tasks in much the same way, with some alterations.
Requirements
This recipe requires the following entries in your Cargo.toml file
tokio = { version = "1.10", features = ["rt"] }
gdnative = { git = "https://github.com/godot-rust/godot-rust.git", features = ["async"]}
Defining the Executor
The executor itself can be defined the same way.
#![allow(unused)] fn main() { thread_local! { static EXECUTOR: &'static SharedLocalPool = { Box::leak(Box::new(SharedLocalPool::default())) }; } }
However, our SharedLocalPool
will store a LocalSet
instead, and the futures::task::LocalSpawn
implementation for the type will simply spawn a local task from that.
#![allow(unused)] fn main() { use tokio::task::LocalSet; #[derive(Default)] struct SharedLocalPool { local_set: LocalSet, } impl futures::task::LocalSpawn for SharedLocalPool { fn spawn_local_obj( &self, future: futures::task::LocalFutureObj<'static, ()>, ) -> Result<(), futures::task::SpawnError> { self.local_set.spawn_local(future); Ok(()) } } }
The Executor Driver
Finally, we need to create a NativeClass
which will act as the driver for our executor. This will store the tokio Runtime
.
#![allow(unused)] fn main() { use tokio::runtime::{Builder, Runtime}; #[derive(NativeClass)] #[inherit(Node)] struct AsyncExecutorDriver { runtime: Runtime, } impl AsyncExecutorDriver { fn new(_base: &Node) -> Self { AsyncExecutorDriver { runtime: Builder::new_current_thread() .enable_io() // optional, depending on your needs .enable_time() // optional, depending on your needs .build() .unwrap(), } } } }
In the _process
call of our AsyncExecutorDriver
, we can block on run_until
on the LocalSet
. run_until
will automatically resume all tasks on the local set until the provided future is completed. Since we don't want to block the frame, and we'd be checking every frame anyway, we just provide an empty task and await it.
#![allow(unused)] fn main() { #[methods] impl AsyncExecutorDriver { #[method] fn _process(&self, #[base] _base: &Node, _delta: f64) { EXECUTOR.with(|e| { self.runtime .block_on(async { e.local_set .run_until(async { tokio::task::spawn_local(async {}).await }) .await }) .unwrap() }) } } }
From there, initializing is just the same as it is in the tests.
#![allow(unused)] fn main() { fn init(handle: InitHandle) { gdnative::tasks::register_runtime(&handle); gdnative::tasks::set_executor(EXECUTOR.with(|e| *e)); ... handle.add_class::<AsyncExecutorDriver>(); } }