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>();
}
}