WEBVTT

00:00.000 --> 00:11.000
All right, could we give a warm welcome to Joven who's going to be talking about

00:11.000 --> 00:21.000
Async Rust in Gido 4?

00:21.000 --> 00:24.000
Hello everyone, can you hear me?

00:24.000 --> 00:29.000
Yeah, I will talk to you about Async Rust in Gido 4, which I implemented for the Gido Rust

00:29.000 --> 00:31.000
Bindings.

00:31.000 --> 00:36.000
Briefly about myself, I have been triggering with Rust since 2018.

00:36.000 --> 00:41.000
Then started doing professionally Rust in 2021.

00:41.000 --> 00:49.000
And started contributing to the Gido Rust Bindings around the Gido for re-write

00:49.000 --> 00:57.000
of the Bindings and then joined as a Rust Maintainer in 2025.

00:57.000 --> 01:05.000
The goals of this project were to enable Async Rust code in Gido because people were asking

01:05.000 --> 01:07.000
about that a lot.

01:07.000 --> 01:14.000
We wanted to keep the overhead to an absolute minimum and not include a lot of additional dependencies.

01:14.000 --> 01:20.000
And we wanted very similar behavior to what people were used to in Gido script.

01:20.000 --> 01:24.000
First, we need to look at Async in Gido script.

01:24.000 --> 01:32.000
So how Async works in Gido script is that you define a signal somewhere.

01:32.000 --> 01:39.000
And when you await that signal, the function implicitly gets turned into an Async function.

01:39.000 --> 01:43.000
And then it returns something that's called a function state.

01:43.000 --> 01:45.000
It's internal to the engine.

01:45.000 --> 01:48.000
But from that, the engine knows that it's an Async function.

01:48.000 --> 01:50.000
And you can await that function again.

01:50.000 --> 01:56.000
Or you just not await it and it will just run in the background.

01:56.000 --> 01:58.000
The signals drive the await points.

01:58.000 --> 02:07.000
So as soon as the signal fires, the Async function gets started again and continues.

02:07.000 --> 02:12.000
Yeah, here you can see how it works from a call as perspective.

02:12.000 --> 02:17.000
You await the Async function and at some point you get a result.

02:17.000 --> 02:20.000
Signals in Gido are defined like this.

02:20.000 --> 02:27.000
You just have a signal name and a number of arguments with their types if you want.

02:27.000 --> 02:36.000
And when you want to emit a signal, you just call self and a signal and emit.

02:37.000 --> 02:41.000
And in the engine, this gets unrolled basically into a for loop.

02:41.000 --> 02:49.000
And it just goes all overall subscribers to that signal and calls them just in that loop.

02:49.000 --> 02:54.000
In Gido, the execution order is a bit important for later.

02:54.000 --> 02:56.000
So you have your main loop.

02:56.000 --> 02:58.000
The main loop updates your game.

02:58.000 --> 03:03.000
It does the physics update and in the end it drains all the deferred tasks.

03:03.000 --> 03:13.000
The deferred tasks are basically functions or closures in rough that get called and stored somewhere in the engine.

03:13.000 --> 03:20.000
And then, yeah, it just runs them to completion until they're no deferred tasks left anymore.

03:20.000 --> 03:23.000
Okay, let's look at Async in Rust.

03:23.000 --> 03:28.000
In Rust, the important part for Async is the future trade.

03:28.000 --> 03:37.000
It's defined like this. You have an output associated type and you have one function to pull the future.

03:37.000 --> 03:43.000
The pull function receives a context and it returns a return value of pull.

03:43.000 --> 03:48.000
The tells whoever pull it if it's ready or not.

03:48.000 --> 03:51.000
We usually define Async functions like this.

03:51.000 --> 03:57.000
So you don't really have to implement the future trade yourself, but you could if you want to.

03:58.000 --> 04:04.000
Just a little bit of an overview of how the Async flow is then executed.

04:04.000 --> 04:06.000
Someone pulls the future.

04:06.000 --> 04:09.000
The future receives the context.

04:09.000 --> 04:14.000
Has to call a function waker on the context, which gives the future a waker.

04:14.000 --> 04:18.000
And the waker can then be called with waker wake.

04:18.000 --> 04:23.000
And tell whoever created this waker to wake up the future.

04:23.000 --> 04:28.000
And that should in the end then trigger a future pull again.

04:28.000 --> 04:34.000
So initially I wrote a proof of concept that we can do this in the engine.

04:34.000 --> 04:41.000
And the idea was that you have some Rust function and you call a function of the bindings.

04:41.000 --> 04:44.000
In this case, I call it good old task.

04:44.000 --> 04:49.000
And it gets some Async code and executes that to completion.

04:49.000 --> 04:52.000
Asyncrenously, obviously.

04:53.000 --> 04:55.000
For that, we need a waker.

04:55.000 --> 05:01.000
As such as outline, we need a waker that we can pass to the future.

05:01.000 --> 05:10.000
In this case, we put a runtime index there because we need to store the future somewhere.

05:10.000 --> 05:17.000
Otherwise, we can't access it when the signal fires or when we get woke up.

05:17.000 --> 05:20.000
So we just stored the Asyncrun time in a static.

05:20.000 --> 05:27.000
It's basically just the vector of these futures.

05:27.000 --> 05:40.000
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,

05:40.000 --> 05:46.000
and just wake it up, just start pulling the future.

05:46.000 --> 05:49.000
The waker itself looks like this.

05:49.000 --> 05:56.000
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.

05:56.000 --> 06:01.000
We get the task from the runtime.

06:01.000 --> 06:05.000
Yeah, handle it if it shouldn't be there for some reason.

06:05.000 --> 06:08.000
Then we pull the future.

06:08.000 --> 06:12.000
And in the end, we check if the future is ready.

06:13.000 --> 06:21.000
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.

06:21.000 --> 06:28.000
But the second part, as I outlined in the beginning, is we also want the same behavior as in good old.

06:28.000 --> 06:33.000
So we want to be able to await signals.

06:33.000 --> 06:36.000
So we need a signal future.

06:37.000 --> 06:50.000
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.

06:50.000 --> 06:57.000
The pull function then just gets the state checks if we have a result.

06:57.000 --> 07:00.000
If we got a result, we return already.

07:00.000 --> 07:13.000
And if we got no result, we just store the waker for later so we can wake up once we receive the signal.

07:13.000 --> 07:18.000
So when we create this future, we pass in a signal.

07:18.000 --> 07:29.000
The signal also creates the state initializes everything and creates a callback function that can be passed to the engine.

07:29.000 --> 07:35.000
As soon as the signal fires, this will only be executed once.

07:35.000 --> 07:42.000
So we don't have to worry about being called over and over again because signals can fire more than once.

07:42.000 --> 07:48.000
And then we can just return that to whoever created the future.

07:48.000 --> 07:52.000
Internally when the signal fires, we just have to lock the state.

07:52.000 --> 08:04.000
We wake up or we get the waker and we replace our return value from the signal into our state.

08:04.000 --> 08:10.000
If we have a waker, we wake it up and that's it.

08:10.000 --> 08:13.000
So basically we are done now.

08:13.000 --> 08:15.000
We can call it a task.

08:15.000 --> 08:21.000
We can pass some asynchronous code to it and we can run our asynchronous code.

08:21.000 --> 08:23.000
It will be executed by the engine.

08:23.000 --> 08:25.000
Are we really done?

08:25.000 --> 08:26.000
No, obviously not.

08:26.000 --> 08:29.000
There were a bunch of challenges along the way.

08:29.000 --> 08:35.000
So it worked, but there were a lot of things that people complained about and that we discovered didn't work quite so well.

08:35.000 --> 08:42.000
So the first challenge that we faced was there are some asynchronous libraries that don't like to be pulled right away.

08:42.000 --> 08:46.000
As I showed, we just create a future and we pull it right away.

08:46.000 --> 08:52.000
And some futures want to do some setup before they get pulled for the first time.

08:52.000 --> 09:00.000
So we shouldn't just pull on the same call stack as we created the future.

09:00.000 --> 09:03.000
So the question is how can we pull later?

09:03.000 --> 09:06.000
In good old, that's actually quite easy.

09:06.000 --> 09:11.000
We just start pulling the future in a deferred good old callable.

09:11.000 --> 09:20.000
The second challenge was that signals can be emitted on any thread they want to.

09:20.000 --> 09:27.000
So as I showed before, it's synchronous code, but the signal image function can be called on any thread you want.

09:27.000 --> 09:32.000
And it will call all your signals subscribers on that thread as well.

09:33.000 --> 09:42.000
Signal arguments are not necessarily thread safe, so they also will move across threads, which is not ideal.

09:42.000 --> 09:51.000
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.

09:51.000 --> 09:55.000
And they will always be moved back to the main thread.

09:55.000 --> 10:04.000
To make this a little bit less error prone because we would move the futures between the threads.

10:04.000 --> 10:10.000
If you start a good old task on a different thread, we restricted the good old task function to the main thread.

10:10.000 --> 10:15.000
So you only can spawn new tasks on the main thread.

10:15.000 --> 10:18.000
And then we solve both of these problems.

10:18.000 --> 10:24.000
This is how it looks like when we make some changes to the proof of concept.

10:24.000 --> 10:41.000
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.

10:41.000 --> 10:49.000
And we just tell the engine to call this deferred, so it will do it in the end of the main loop.

10:49.000 --> 10:57.000
And we assert that the main function, the good old task function is always called on the main thread.

10:57.000 --> 10:59.000
And if that's not the case, we panic.

10:59.000 --> 11:06.000
This is also part of the documentation, so it should be careful how you call the good old task function.

11:06.000 --> 11:09.000
Yeah, challenge 3.

11:09.000 --> 11:12.000
Objects from good old are neither send or think.

11:12.000 --> 11:16.000
I already mentioned many of the types are not thread safe.

11:16.000 --> 11:31.000
And since you can emit the signal on any thread, we may be move this signal argument over the thread, which is not ideal.

11:31.000 --> 11:39.000
Most of the why they are not thread safe is because most of the objects are managed or ref counted.

11:39.000 --> 11:46.000
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.

11:46.000 --> 11:52.000
So it's a bit of a headache, and it's not really something that we have solved yet.

11:52.000 --> 11:56.000
So thread safety is still a bit of a pain.

11:56.000 --> 12:03.000
So we solve this by introducing this trade, dynamic send, and it's basically an unsafe trade.

12:03.000 --> 12:11.000
The implementer has to guarantee that this function extractive safe is returns none.

12:11.000 --> 12:17.000
If the value that it's wrapping was not sent when it was created.

12:17.000 --> 12:24.000
Otherwise, we can return some and give out the value.

12:24.000 --> 12:29.000
So we created this type of thread confined.

12:29.000 --> 12:35.000
It accepts a generic value inside.

12:35.000 --> 12:48.000
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.

12:48.000 --> 12:51.000
And if not, we do not return it.

12:51.000 --> 12:54.000
This causes a drop, and unfortunately a leak.

12:54.000 --> 13:04.000
But we decided that this is the best option to handle it, because we cannot drop the value if it's not thread safe.

13:04.000 --> 13:06.000
And then challenge four.

13:06.000 --> 13:10.000
Single objects objects can be freed at any time.

13:10.000 --> 13:17.000
So since most of the objects that emit signals are manually managed, they can be just freed whenever you want.

13:17.000 --> 13:20.000
And your future can still be hanging around.

13:20.000 --> 13:22.000
And then you have a dangling future.

13:22.000 --> 13:23.000
It's not really a problem.

13:23.000 --> 13:27.000
You just leak some memory because the future will never complete.

13:27.000 --> 13:31.000
Some people didn't like this, which is understandable.

13:31.000 --> 13:34.000
So we had to come up with a solution.

13:34.000 --> 13:40.000
And the solution for that was tracking when signal closures are dropped.

13:40.000 --> 13:48.000
So we marked the future when the callback closure is dropped.

13:48.000 --> 13:58.000
And resolve the future as an era when it's getting pulled the next time.

13:58.000 --> 14:01.000
Around that, we added a signal future a wrapper.

14:01.000 --> 14:03.000
So we have two types.

14:03.000 --> 14:06.000
Two futures now.

14:06.000 --> 14:09.000
One type of signal future, which will return an era.

14:09.000 --> 14:11.000
And the signal future, which just panics.

14:11.000 --> 14:14.000
So you can choose whatever you want to use.

14:14.000 --> 14:17.000
You have this trade-off.

14:17.000 --> 14:24.000
There are a couple of things that we also did that I couldn't really fit into here.

14:24.000 --> 14:26.000
We catch panics.

14:26.000 --> 14:33.000
Obviously in the, I think, code otherwise it would just break your synchronous code at any given point.

14:33.000 --> 14:44.000
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.

14:44.000 --> 14:50.000
It's not that important, but it wasn't really in the proof of concept.

14:50.000 --> 14:53.000
Then we also have support for nested tasks.

14:53.000 --> 15:00.000
So in the proof of concept, you couldn't really create tasks inside of tasks that's now possible.

15:00.000 --> 15:02.000
And some naming changed.

15:02.000 --> 15:09.000
So it's not what you have seen in the proof of concept anymore and some of the naming changed.

15:09.000 --> 15:13.000
Yeah, this is an overview of the project.

15:13.000 --> 15:18.000
If you want to see more, you can see the full implementation in our project.

15:18.000 --> 15:23.000
On GitHub, you can also just check out the repository of the project page.

15:23.000 --> 15:25.000
And that's it.

15:25.000 --> 15:27.000
Thank you very much.

15:28.000 --> 15:43.000
So we have a question up front.

15:43.000 --> 15:48.000
First of all, thank you because I gave up on Rust for Go Do because I couldn't connect to signals.

15:48.000 --> 15:51.000
So thanks for fixing that.

15:51.000 --> 15:57.000
And second question does Go Do, like the engine with Go Do script also leak memory.

15:57.000 --> 16:03.000
If nothing calls my Async function a model today, I have a solution for that with that garbage collector.

16:03.000 --> 16:06.000
No, I can't I check it at the time.

16:06.000 --> 16:15.000
And as far as I remember, they have basically the same problem.

16:15.000 --> 16:17.000
Any other questions?

16:21.000 --> 16:29.000
Thank you.

16:29.000 --> 16:36.000
Other performance implication of running every pull on the main thread that you've seen.

16:36.000 --> 16:46.000
So yeah, if you are planning on like having your futures running concurrently,

16:46.000 --> 16:49.000
it's having a performance implication.

16:49.000 --> 16:55.000
I would say in Go Do, most of the time Async code runs on the main thread anyway,

16:55.000 --> 16:59.000
because people are using threads very rarely.

16:59.000 --> 17:02.000
And if they do, they don't use Async code in them.

17:02.000 --> 17:07.000
But yeah.

17:07.000 --> 17:12.000
Any more questions?

17:12.000 --> 17:19.000
Okay, thank you very, very much.

