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.