Skip to content

Commit 983c09f

Browse files
authored
Merge pull request #44 from lyonbeckers/tokio-runtime
Tokio runtime recipe
2 parents 6f2faf6 + 9def58c commit 983c09f

File tree

2 files changed

+106
-0
lines changed

2 files changed

+106
-0
lines changed

src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
- [Logging](./recipes/logging.md)
2929
- [Loading external resources](./recipes/loading_external_resources.md)
3030
- [Custom nodes Plugin](./recipes/custom-nodes-plugin.md)
31+
- [Tokio Runtime](./recipes/tokio_runtime.md)
3132
- [Exporting](./exporting.md)
3233
- [Android](./exporting/android.md)
3334
- [(TODO) iOS](./exporting/ios.md)

src/recipes/tokio_runtime.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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

Comments
 (0)