All right, could we give a warm welcome to Joven who's going to be talking about
Async Rust in Gido 4?
Hello everyone, can you hear me?
Yeah, I will talk to you about Async Rust in Gido 4, which I implemented for the Gido Rust
Bindings.
Briefly about myself, I have been triggering with Rust since 2018.
Then started doing professionally Rust in 2021.
And started contributing to the Gido Rust Bindings around the Gido for re-write
of the Bindings and then joined as a Rust Maintainer in 2025.
The goals of this project were to enable Async Rust code in Gido because people were asking
about that a lot.
We wanted to keep the overhead to an absolute minimum and not include a lot of additional dependencies.
And we wanted very similar behavior to what people were used to in Gido script.
First, we need to look at Async in Gido script.
So how Async works in Gido script is that you define a signal somewhere.
And when you await that signal, the function implicitly gets turned into an Async function.
And then it returns something that's called a function state.
It's internal to the engine.
But from that, the engine knows that it's an Async function.
And you can await that function again.
Or you just not await it and it will just run in the background.
The signals drive the await points.
So as soon as the signal fires, the Async function gets started again and continues.
Yeah, here you can see how it works from a call as perspective.
You await the Async function and at some point you get a result.
Signals in Gido are defined like this.
You just have a signal name and a number of arguments with their types if you want.
And when you want to emit a signal, you just call self and a signal and emit.
And in the engine, this gets unrolled basically into a for loop.
And it just goes all overall subscribers to that signal and calls them just in that loop.
In Gido, the execution order is a bit important for later.
So you have your main loop.
The main loop updates your game.
It does the physics update and in the end it drains all the deferred tasks.
The deferred tasks are basically functions or closures in rough that get called and stored somewhere in the engine.
And then, yeah, it just runs them to completion until they're no deferred tasks left anymore.
Okay, let's look at Async in Rust.
In Rust, the important part for Async is the future trade.
It's defined like this. You have an output associated type and you have one function to pull the future.
The pull function receives a context and it returns a return value of pull.
The tells whoever pull it if it's ready or not.
We usually define Async functions like this.
So you don't really have to implement the future trade yourself, but you could if you want to.
Just a little bit of an overview of how the Async flow is then executed.
Someone pulls the future.
The future receives the context.
Has to call a function waker on the context, which gives the future a waker.
And the waker can then be called with waker wake.
And tell whoever created this waker to wake up the future.
And that should in the end then trigger a future pull again.
So initially I wrote a proof of concept that we can do this in the engine.
And the idea was that you have some Rust function and you call a function of the bindings.
In this case, I call it good old task.
And it gets some Async code and executes that to completion.
Asyncrenously, obviously.
For that, we need a waker.
As such as outline, we need a waker that we can pass to the future.
In this case, we put a runtime index there because we need to store the future somewhere.
Otherwise, we can't access it when the signal fires or when we get woke up.
So we just stored the Asyncrun time in a static.
It's basically just the vector of these futures.
And we created a go-to task function where we assign or we add the future to the runtime and store the index, create a new waker,
and just wake it up, just start pulling the future.
The waker itself looks like this.
First, we kind of get a clone of the waker and put it in a context that's just so we can pass it to the future.
We get the task from the runtime.
Yeah, handle it if it shouldn't be there for some reason.
Then we pull the future.
And in the end, we check if the future is ready.
If it's depending we do nothing, if it's ready, which has clear the task, the runtime doesn't have to care about it anymore.
But the second part, as I outlined in the beginning, is we also want the same behavior as in good old.
So we want to be able to await signals.
So we need a signal future.
Basically taking the return value and store it and it needs to store the waker so it can wake up as soon as the signal fires.
The pull function then just gets the state checks if we have a result.
If we got a result, we return already.
And if we got no result, we just store the waker for later so we can wake up once we receive the signal.
So when we create this future, we pass in a signal.
The signal also creates the state initializes everything and creates a callback function that can be passed to the engine.
As soon as the signal fires, this will only be executed once.
So we don't have to worry about being called over and over again because signals can fire more than once.
And then we can just return that to whoever created the future.
Internally when the signal fires, we just have to lock the state.
We wake up or we get the waker and we replace our return value from the signal into our state.
If we have a waker, we wake it up and that's it.
So basically we are done now.
We can call it a task.
We can pass some asynchronous code to it and we can run our asynchronous code.
It will be executed by the engine.
Are we really done?
No, obviously not.
There were a bunch of challenges along the way.
So it worked, but there were a lot of things that people complained about and that we discovered didn't work quite so well.
So the first challenge that we faced was there are some asynchronous libraries that don't like to be pulled right away.
As I showed, we just create a future and we pull it right away.
And some futures want to do some setup before they get pulled for the first time.
So we shouldn't just pull on the same call stack as we created the future.
So the question is how can we pull later?
In good old, that's actually quite easy.
We just start pulling the future in a deferred good old callable.
The second challenge was that signals can be emitted on any thread they want to.
So as I showed before, it's synchronous code, but the signal image function can be called on any thread you want.
And it will call all your signals subscribers on that thread as well.
Signal arguments are not necessarily thread safe, so they also will move across threads, which is not ideal.
The solution to that is also good old deferred calls because they always run on the main thread as I showed before they run in the main loop.
And they will always be moved back to the main thread.
To make this a little bit less error prone because we would move the futures between the threads.
If you start a good old task on a different thread, we restricted the good old task function to the main thread.
So you only can spawn new tasks on the main thread.
And then we solve both of these problems.
This is how it looks like when we make some changes to the proof of concept.
So basically the Waker no longer just wakes up the future, but it creates a callable, which then calls a call future function, which does a bit a little bit of additional stuff that I couldn't fit on here.
And we just tell the engine to call this deferred, so it will do it in the end of the main loop.
And we assert that the main function, the good old task function is always called on the main thread.
And if that's not the case, we panic.
This is also part of the documentation, so it should be careful how you call the good old task function.
Yeah, challenge 3.
Objects from good old are neither send or think.
I already mentioned many of the types are not thread safe.
And since you can emit the signal on any thread, we may be move this signal argument over the thread, which is not ideal.
Most of the why they are not thread safe is because most of the objects are managed or ref counted.
Even if they are ref counted, we have no idea how many people are currently having access to them, and they could write and read to them.
So it's a bit of a headache, and it's not really something that we have solved yet.
So thread safety is still a bit of a pain.
So we solve this by introducing this trade, dynamic send, and it's basically an unsafe trade.
The implementer has to guarantee that this function extractive safe is returns none.
If the value that it's wrapping was not sent when it was created.
Otherwise, we can return some and give out the value.
So we created this type of thread confined.
It accepts a generic value inside.
And basically when we want to access the signal argument, we check if we are still on the thread with the signal argument was created on.
And if not, we do not return it.
This causes a drop, and unfortunately a leak.
But we decided that this is the best option to handle it, because we cannot drop the value if it's not thread safe.
And then challenge four.
Single objects objects can be freed at any time.
So since most of the objects that emit signals are manually managed, they can be just freed whenever you want.
And your future can still be hanging around.
And then you have a dangling future.
It's not really a problem.
You just leak some memory because the future will never complete.
Some people didn't like this, which is understandable.
So we had to come up with a solution.
And the solution for that was tracking when signal closures are dropped.
So we marked the future when the callback closure is dropped.
And resolve the future as an era when it's getting pulled the next time.
Around that, we added a signal future a wrapper.
So we have two types.
Two futures now.
One type of signal future, which will return an era.
And the signal future, which just panics.
So you can choose whatever you want to use.
You have this trade-off.
There are a couple of things that we also did that I couldn't really fit into here.
We catch panics.
Obviously in the, I think, code otherwise it would just break your synchronous code at any given point.
We kind of have this future slot state machine that checks if the future is still active or if it's currently pulled and so on.
It's not that important, but it wasn't really in the proof of concept.
Then we also have support for nested tasks.
So in the proof of concept, you couldn't really create tasks inside of tasks that's now possible.
And some naming changed.
So it's not what you have seen in the proof of concept anymore and some of the naming changed.
Yeah, this is an overview of the project.
If you want to see more, you can see the full implementation in our project.
On GitHub, you can also just check out the repository of the project page.
And that's it.
Thank you very much.
So we have a question up front.
First of all, thank you because I gave up on Rust for Go Do because I couldn't connect to signals.
So thanks for fixing that.
And second question does Go Do, like the engine with Go Do script also leak memory.
If nothing calls my Async function a model today, I have a solution for that with that garbage collector.
No, I can't I check it at the time.
And as far as I remember, they have basically the same problem.
Any other questions?
Thank you.
Other performance implication of running every pull on the main thread that you've seen.
So yeah, if you are planning on like having your futures running concurrently,
it's having a performance implication.
I would say in Go Do, most of the time Async code runs on the main thread anyway,
because people are using threads very rarely.
And if they do, they don't use Async code in them.
But yeah.
Any more questions?
Okay, thank you very, very much.