-
-
Notifications
You must be signed in to change notification settings - Fork 236
Introduce a generic rust async runtime integration #1228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
…strant Wasm registration fn names now based on crate name + index
- Allow clippy::uninlined_format_args.
…d-format-args Allow `clippy::uninlined_format_args` (Rust 1.88)
Sync from upstream
I scrolled through it and ehhhhhhhhhh. I haven't been looking to hard (5 mins total), but it looks like something written on stroke.
All of it can be done 100% in user space, with macro and fn on_level_init(level: InitLevel) {
// Show which initialization level is being processed
println!("📍 gdext initialization: level = {level:?}");
// Register the async runtime early in the initialization process
// This is the proper way to integrate async runtimes with gdext
if level == InitLevel::Scene {
register_runtime::<TokioIntegration>().expect("Failed to register tokio runtime");
}
} In general – I don't see why it would be a part of the core and it doesn't live up to standards in the core. Additionally limiting it only to static functions only is silly – like, what if I have something that implements Send+Sync on my struct and I want to use it with async? In such cases (which is, I argue, most of the cases) this feature is next to useless. |
@Yarwin Well written! Did you enjoy venting your anger? As someone who is contributing to gdext with my own ideas, I'm honestly shocked. What exactly made you so angry? You completely dismissed this PR as worthless—how exactly did you come to the conclusion that this PR has no value at all? I clearly stated at the beginning that my intention was simply to expose Tokio-based async functions to GDScript, allowing the use of GDScript's await syntax. It's as straightforward as that. Even though currently only static methods are supported, this already meets our practical needs. Our game project frequently interacts with servers via HTTP requests, and this PR allows us to write this logic in Rust. It works well for us, and it's certainly not meaningless or useless as you described. I'm currently working on supporting instance methods as well, and I've already completed testing. This will further enhance async support. If the PR quality isn't good enough, I'm open to criticism and willing to improve it. But is it really appropriate to treat PR contributors with such an attitude? |
@AllenDang Thank you for your contribution, and I apologize for the heated response here. The use of AI-generated code can make it look like no effort went into the PR. However, our responsibility as maintainers is to provide constructive feedback, so that things can be improved. In particular, it was me who suggested to come up with a runtime-agnostic async integration, so "why should it be part of core" is on me, not the author. I'll have a deeper look today evening, let's please keep a nice tone. |
@Bromeon Deep thank, I'm keep improving it, and really open to any criticism and willing to improve it. |
Yes, I will try to make some time to take a look at this PR. |
API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-1228 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, I had a high-level look at the code.
Here's some feedback 🙂
-
The diff is currently at +1,851 -221 LoC.
This is too much code to review at once, especially for a feature of this complexity. I know async runtimes are complex by their nature, but I wonder if we can start with a minimal foundation and add more features over time. Maybe parts can be offloaded to concrete runtimes like
tokio
(outside godot-rust), maybe some parts we don't need at all. Or maybe there are lightweight crates (notokio
,syn
etc.) that could support us here? We can then consider feature-gating this. -
It's a bit unclear how everything works together. You show some examples that highlight the
AsyncRuntimeIntegration
trait, which is nice. But the code adds a lot of other public symbols. While they are individually documented, I'm missing the architecture connecting them (and justifying their existence). Maybe related to 1, some can be omitted in a first version? -
Regarding CI, you can test locally by running
check.sh
, see also book. This script isn't perfect -- if you have a fork of the repo on GitHub, you can also enable GitHub Actions and run the whole suite there (maybe separate branch -- and update this one once ready). That would speed up review time, as you don't have to wait for us manually approving CI. -
Do we need thread-local storage? Can we not start by only allowing async on the main thread? Or how does Godot interact with async on other threads? A disadvantage is also that the whole runtime needs to be re-instantiated for each thread (with CPU + memory implications) -- something that's not obvious to the user.
-
@TitanNano already implemented async functionality based on the "Godot runtime". This is a very nice and lightweight option for people who want to reuse Godot's idle frame time to run async functionality. It currently supports signals, and may or may not be extended. But the PR seems to overwrite all existing functionality in a breaking way, and expect an external async runtime instead. I believe it should be possible to stay with the Godot runtime, even at limited functionality.
-
There are some code style rules in the book. Not everything is specified, it's usually best to look at existing code. Some parts:
- Comment line length 120-145 characters.
- Full sentences for comments, capitalized first word and ending in period.
- ...
I hope that helps! We can also gladly discuss the approach a bit more before jumping too much into implementation. Especially ideas regarding a simpler foundation would be very welcome 😊
TLDR: massively simplify (much smaller diff, minimal API) + reuse existing ASYNC_RUNTIME
👍
godot-core/src/task/async_runtime.rs
Outdated
/// Errors that can occur during async runtime operations | ||
#[derive(Debug, Clone)] | ||
pub enum AsyncRuntimeError { | ||
/// Runtime has been deinitialized (during engine shutdown) | ||
RuntimeDeinitialized, | ||
/// Task was canceled while being polled | ||
TaskCanceled { task_id: u64 }, | ||
/// Task panicked during polling | ||
TaskPanicked { task_id: u64, message: String }, | ||
/// Task slot is in an invalid state | ||
InvalidTaskState { | ||
task_id: u64, | ||
expected_state: String, | ||
}, | ||
/// No async runtime has been registered | ||
NoRuntimeRegistered, | ||
/// Task spawning failed | ||
TaskSpawningFailed { reason: String }, | ||
/// Signal emission failed | ||
SignalEmissionFailed { task_id: u64, reason: String }, | ||
/// Thread safety violation | ||
ThreadSafetyViolation { | ||
expected_thread: ThreadId, | ||
actual_thread: ThreadId, | ||
}, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a very detailed error API. I would probably keep this private in the beginning -- and wrapped by a public-facing struct:
pub struct AsyncRuntimeError {
inner: AsyncInnerError, // your enum
}
Then all the traits (Display
etc.) can appear on the public-facing type. This reduces API complexity quite a bit. If people need to differentiate those programmatically, we can always gradually expose the details, but then we start simple.
godot-core/src/task/async_runtime.rs
Outdated
// Check if runtime is registered | ||
if !is_runtime_registered() { | ||
panic!("No async runtime has been registered. Call gdext::task::register_runtime() before using async functions."); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a breaking change.
So far, could use the "Godot runtime" as a default. That one supports some features (signal awaiting) but not all (async fn
).
Do you think it's possible to still provide that as a default, and later let the user explicitly select which one to use?
enum FutureSlotState<T> { | ||
/// Slot is currently empty. | ||
Empty, | ||
/// Slot was previously occupied but the future has been canceled or the slot reused. | ||
Gone, | ||
/// Slot contains a pending future. | ||
Pending(T), | ||
/// Slot contains a future which is currently being polled. | ||
Polling, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we keep supporting the Godot runtime, we can probably not just remove these 2 states, can we?
/// Separated concerns for better architecture | ||
/// | ||
/// Task limits and backpressure configuration | ||
#[derive(Debug, Clone)] | ||
pub struct TaskLimits { | ||
/// Maximum number of concurrent tasks allowed | ||
pub max_concurrent_tasks: usize, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is task management something that needs to be done in godot-rust?
#[async_func] | ||
async fn async_vector2_multiply(input: Vector2) -> Vector2 { | ||
// Use real tokio sleep to test tokio runtime integration | ||
time::sleep(Duration::from_millis(10)).await; | ||
Vector2::new(input.x * 2.0, input.y * 2.0) | ||
} | ||
|
||
#[async_func] | ||
async fn async_compute_sum(a: i32, b: i32) -> i32 { | ||
// Use real tokio sleep to test tokio runtime integration | ||
time::sleep(Duration::from_millis(12)).await; | ||
a + b | ||
} | ||
|
||
#[async_func] | ||
async fn async_get_magic_number() -> i32 { | ||
// Test with a short tokio sleep | ||
time::sleep(Duration::from_millis(15)).await; | ||
42 | ||
} | ||
|
||
#[async_func] | ||
async fn async_get_message() -> StringName { | ||
// Test async with string return | ||
time::sleep(Duration::from_millis(20)).await; | ||
StringName::from("async message") | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These tests do the same thing. The sum is enough and the other 3 can be removed, without losing coverage.
I agree with @Bromeon s last comment and would highly recommend following a three-step plan like this:
|
Just noting here as someone who needs to support web builds as a first-class citizen, I highly support and encourage this choice. Standing on those giants' shoulders is very helpful indeed, but I've yet to encounter one that properly plays nicely with the |
@Bromeon here is the current design diagram, might be help to understand how the system works. And thanks for you guys @TitanNano @KenAKAFrosty , I will carefully read your comments and rethink. |
Thanks for the diagram! As mentioned earlier, I believe some parts could be simpler. Maybe look how the existing async runtime works and try to understand it. Then instead of replacing the entire thing, let's slowly and carefully extend it, where every extension has a clear justification and use case. As mentioned by Yarwin, it might be worthwhile to feature-gate this behind |
@Bromeon Got it, I will work on it. |
Hi, all contributors of gdext
This PR introduces a new macro
#[async_func]
which could expose rust async function with return value to gdscript, and allow gdscript couldawait
the execution, like below demonstrated.Note: this PR only works with experimental-multithread feature is enabled.
I raised a PR before, that at that time, I coupled tokio into gdext, after discuss with you guys, I decided to create a generic async runtime interface which could let user plug any async runtime like async-std or smol.
So it comes with it this time, user could plug async runtime like this
And in lib.rs, do
This PR is kind of huge, according to the complexity nature of async future management, but it comes out finally.
Hope contributors could spent sometime to help reviewing the code, and hopefully could merge this into gdext.
Thanks!