|
| 1 | +# Tokio Runtime |
| 2 | + |
| 3 | +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. |
| 4 | + |
| 5 | +## Requirements |
| 6 | +This recipe requires the following entries in your Cargo.toml file |
| 7 | + |
| 8 | +```toml |
| 9 | +tokio = { version = "1.10", features = ["rt"] } |
| 10 | +gdnative = { git = "https://github.com/godot-rust/godot-rust.git", features = ["async"]} |
| 11 | +``` |
| 12 | + |
| 13 | +## Defining the Executor |
| 14 | +The executor itself can be defined the same way. |
| 15 | + |
| 16 | +```rust |
| 17 | +thread_local! { |
| 18 | + static EXECUTOR: &'static SharedLocalPool = { |
| 19 | + Box::leak(Box::new(SharedLocalPool::default())) |
| 20 | + }; |
| 21 | +} |
| 22 | +``` |
| 23 | + |
| 24 | +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. |
| 25 | + |
| 26 | +```rust |
| 27 | +use tokio::task::LocalSet; |
| 28 | + |
| 29 | +#[derive(Default)] |
| 30 | +struct SharedLocalPool { |
| 31 | + local_set: LocalSet, |
| 32 | +} |
| 33 | + |
| 34 | +impl futures::task::LocalSpawn for SharedLocalPool { |
| 35 | + fn spawn_local_obj( |
| 36 | + &self, |
| 37 | + future: futures::task::LocalFutureObj<'static, ()>, |
| 38 | + ) -> Result<(), futures::task::SpawnError> { |
| 39 | + self.local_set.spawn_local(future); |
| 40 | + |
| 41 | + Ok(()) |
| 42 | + } |
| 43 | +} |
| 44 | +``` |
| 45 | + |
| 46 | +## The Executor Driver |
| 47 | + |
| 48 | +Finally, we need to create a `NativeClass` which will act as the driver for our executor. This will store the tokio `Runtime`. |
| 49 | + |
| 50 | +```rust |
| 51 | +use tokio::runtime::{Builder, Runtime}; |
| 52 | + |
| 53 | +#[derive(NativeClass)] |
| 54 | +#[inherit(Node)] |
| 55 | +struct AsyncExecutorDriver { |
| 56 | + runtime: Runtime, |
| 57 | +} |
| 58 | + |
| 59 | +impl AsyncExecutorDriver { |
| 60 | + fn new(_owner: &Node) -> Self { |
| 61 | + AsyncExecutorDriver { |
| 62 | + runtime: Builder::new_current_thread() |
| 63 | + .enable_io() // optional, depending on your needs |
| 64 | + .enable_time() // optional, depending on your needs |
| 65 | + .build() |
| 66 | + .unwrap(), |
| 67 | + } |
| 68 | + } |
| 69 | +} |
| 70 | +``` |
| 71 | + |
| 72 | +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. |
| 73 | + |
| 74 | +```rust |
| 75 | +#[methods] |
| 76 | +impl AsyncExecutorDriver { |
| 77 | + #[export] |
| 78 | + fn _process(&self, _owner: &Node, _delta: f64) { |
| 79 | + EXECUTOR.with(|e| { |
| 80 | + self.runtime |
| 81 | + .block_on(async { |
| 82 | + e.local_set |
| 83 | + .run_until(async { |
| 84 | + tokio::task::spawn_local(async {}).await |
| 85 | + }) |
| 86 | + .await |
| 87 | + }) |
| 88 | + .unwrap() |
| 89 | + }) |
| 90 | + } |
| 91 | +} |
| 92 | +``` |
| 93 | + |
| 94 | +From there, initializing is just the same as it is in the tests. |
| 95 | + |
| 96 | +```rust |
| 97 | +fn init(handle: InitHandle) { |
| 98 | + gdnative::tasks::register_runtime(&handle); |
| 99 | + gdnative::tasks::set_executor(EXECUTOR.with(|e| *e)); |
| 100 | + |
| 101 | + ... |
| 102 | + handle.add_class::<AsyncExecutorDriver>(); |
| 103 | +} |
| 104 | +``` |
| 105 | + |
0 commit comments