Thank you everyone for waking up this early on the Sunday.
I'm sure you wanted to sleep in, but you came to the first talk and it's a really good one.
We have Fedor who's going to talk about WebAssembly on constrained devices.
So please give him a warm welcome.
Hey, good morning everyone and thank you for the introduction.
I'm Fedor and I'm very excited to be giving this talk in the rest of the room of Foster and again, thanks for getting up early.
So what I'm going to be talking about today is about running WebAssembly on really microcontrollers.
So think something like an ESP32, like C3, C5 or something from Nordic, like an NRF53.
And those are basically the learnings that period the company I work at have drawn from just that kind of trying to do that.
So how the talk is going to be organized is like very straightforward.
We're going to start with motivation.
So kind of answering the question, why would you want to run WebAssembly on constrained devices in the first place?
And then looking at it from like these two directions, like on the one hand, what does it mean for your code?
What does it mean for how the functionality you want to bring on your device looks like in the Rust code you can pile for WebAssembly?
And the other direction we're going to look at it from is, what does it mean for what your firmware looks like and specifically?
Which runtime will you pick and what does it mean for your footprint, but also for the integration of this runtime with your functionality?
So let's start with the motivation.
For that I'm going to tell you a little bit about what we do at period and also like about modernic.
So we work in the IoT domain.
So you can imagine systems which are these large distributed systems consisting of a large number of heterogeneous devices.
So think everything from cloud servers to Linux, Edge nodes and at least for us more reason they also micro control this.
And what we do there is we work in Mermek and Mermek is a middleware.
And you can imagine it as this thing where you have an agent, which is like a thin soft firmware layer you put on each of your nodes.
And what it does for you is it manages your applications.
So you just handed out your application which you want to run and Mermek will do the rest.
It will find the devices where they should be put.
It will transfer the executable there, run them, manage the communication, manage the configuration, monitor the thing at runtime.
That's all cool and interesting.
The only thing we care about there in this talk is this point where you take code from the user at runtime and put them on nodes,
which you manage.
On nodes where you potentially have a code running from other users.
And in that context I think that at least for like NOS, great devices.
I won't have to do too much work to convince you that whatever somebody is a pretty cool idea there.
Because like we said, we want to run anywhere.
So already having something we just talked at independent as a big win because you don't have to do anything to switch from one target hardware to the other.
But the other thing and from my perspective it's way more important is the strong sandboxing that you get.
Whether it gives you this very explicit way of defining the capabilities of what this piece of software that you deploy there can do.
And the sandbox around it is very strong so you can be very confident deploying unknown code, which is like, but it has potentially arrows.
It could even have like security problems.
Now, if we look at this on micro controllers, all these advantages hold, in fact you have more advantages.
Because as soon as you take a large part of your functionality and put it from firmware into a web assembly module, you get a part of functionality which is way easier to update.
Because you don't need reflection, you don't need an OTA, you just load another module.
And the great thing about it, the great thing about this update that you get there, it cannot break your device.
The worst thing that can happen is the module runs, it has some arrow, it would just trap to the host.
So your host can just tell you it's still there.
It didn't crash, it can tell you hey give me another module.
And especially if you think about the fact that these micro controllers are often deployed in remote settings, like in settings where specifically don't want to go there at refresh, this could be a big advantage.
Now, this advantage is also obvious.
We are micro controllers, we have very little memory as it is.
And you won't get around introducing a higher memory footprint, you won't get around having a certain performance overhead.
So that's the trade-off we are talking about here.
And just to mention it, I'm definitely not saying that web assembly is like the silver bullet, and that this should be the only way how you deploy a code.
There are definitely many other ways how to do that.
Or interesting, they all have their own use case where this is useful.
The takeaway here is just web assembly is an interesting point to do that.
And we're going to be talking a little bit more about like two of the things that you could consider.
One is tooling ecosystem and the other one is the footprint, which depends very strongly on the chosen runtime.
So first, the practical consequences regarding your code.
So what we're talking about here is just the question, like assume your curious now.
Assume you want to try out to run web assembly on micro controllers.
What does that mean for the code that you write?
And I think there are like two perspectives here.
On the one hand, you could be someone like me coming more from the Linux site, where you maybe have worked with like,
like, present time for a while, with web assembly for a while, probably with present time.
Or the other perspective is you may be actually someone who develops embedded firmware.
And now wants to try to web assembly.
I'm kind of trying to cater to both the perspective a little bit.
So with the first one, say you come from Linux, you may likely be using a varsity target.
So the first thing you will realize, you won't be able to do that on embedded.
Although some of the embedded runtime's due support varsity, you will never get it for free.
It always comes with additional memory cost because you have to implement these whole functions additionally.
The other thing is varsity as it is.
It in general assumes certain like OS support with things like timers, randomness and IO.
So like many of these things are just not written for the constraint environment of microcontrollers.
The other thing, and that is something that kind of surprised me was that as soon as you switch to unknown unknown,
you have to step away from the comfortable situation of just taking any rust code using STD and just putting it to web assembly.
That doesn't work anymore.
That was new for me, but actually the fact that you can do it normally.
It doesn't come from like Western as this, it actually comes from varsity.
Like many standard library things can only be mapped to like a web assembly.
If you have like those varsity host functions available.
So with that, it means your code will become more embeddy.
It will be an OSDD code, for example, your allocation will be explicit.
And since you also don't have the varsity host functions, you will have to implement all of your host functions yourself.
And that's also kind of the segue to the more like embedded perspective.
If you are used to write embedded software, then you will have to get used to in your way of accessing your peripherals.
So you firmware will look something like that.
So you have on the one side your device peripherals, they are kind of the same as they were before.
And you have this big new block of the Western runtime containing the web assembly modules.
And the important thing here is that the web assembly modules, they are extremely sandboxed.
That means the way they can access anything outside the modules have it's very restricted.
In fact, the only thing you can do is you can, there only exchange between hosts and guests through export and import functions.
They are explicitly defined and you are also very restricted about how you define them.
So their signature has to follow the CABI.
And the biggest restriction here is also that the arguments in return very use of those functions.
Those are essentially just integers and flows.
Now, there would of course suck if that was always, if that was the only thing you had, you want to exchange more complex information.
And how you will normally do that is you will make use of the fact that the host can access the guest memory.
And so how these exchanges typically go is like when the module wants to communicate something complex to the host.
It would give it arguments just describing a region in its memory.
And then the host knows how to interpret it.
So these are basically the things.
Now, how does it look in code?
This is just I hope it kind of works out from the font size also the slides are just available.
You could just look and direct the up in the like a font in the page of the talk.
But this is how one of the all the modules looks, which we used to have in Mernick.
And I mean you see two things here like on the one hand, as promised, we have no STD, we have exclusive allocation.
But like with macros, you can make that look fairly nice.
The other thing is down there, you have no unsafe code.
You have no weapons specifics.
You do nice idiomatic error handling also with the try operator.
And this is actually only domain logic.
I mean this is obviously like a demo module is like very simplistic.
But you can imagine it would be more complex.
It was something real.
But the complexity would not come from the fact that we operate with resume.
The complexity would come from the domain, which is exactly what you want.
And this is done like with with our SDK for resume development.
And just to like show some of the things for example, this receive.
This is actually how you access host functionality under the hood.
It is a thing with a safe rust stripper around a C import.
And that's exactly the thing I was talking about.
The thing where you talk to the host through the CABI, which looks like this.
But nevertheless, in your code describing functionality, this can look nice.
This can look like a normal rust function.
And similar, why do we have nice error handling here?
Well, because of this macro, because you know, macro size essentially magic.
So you can expand it to whatever you want.
And this is what it expands to.
This is like something that looks like like the web assembly.
Export.
You export up there and you kind of wrap around a valuable rust function.
And there you can do whatever you want.
So basically, essentially, this just looks where it was an error or success.
If it's an error, it's going to give the status code back.
And this one actually also uses host logging to tell you the error message.
So that you kind of have more experience you expect from rust.
Okay.
So to summarize, there are some constraints.
You will have to do things slightly differently.
But due to the fact, the trust is awesome.
You can actually never let us live with it quite well.
Okay.
Now to the other part.
And the other part, since we now kind of know what it means for the code you write.
The other question is, what does it mean for how you firmware looks?
And specifically, and this is a decision you will have to make,
which where some runtime do you want to pick to run on your microcontrollers?
And I'm just going to walk you through kind of our decision process that we had on that.
So first, we were thinking about what are the things that actually matter for us with respect to the Western runtime.
And the very obvious and actually like the main point is memory footprint.
Because all else, it's kind of nice, but if it doesn't fit, you cannot do anything with it.
So that is definitely the main thing you will be looking at.
The other points are it has of course, it needs to run on bare metal.
And for us, our use case is running it on an ESP, like something like C3, C5.
So there we want to use embassy to access the peripherals and also to be somewhat concurrent.
So it was very important for us that the runtime will pick integrates well with that.
And for this reason, and also for the reason that we generally have a complete rust stack,
we were very biased towards like rust times written in rust.
Yeah, okay, then the question, well, what do we do? How do we find out about memory footprint?
We did like a very straightforward thing to say, hey, let's set up a benchmark.
So the benchmark is very simple, it has like one module which uses one import from the host to just lock.
And then we implement it for the different runtime that you have.
And this is something we actually publish.
So like if you have like an NRF53 board, you can just like stick it to your computer.
And you can run this repository like and each of the features gives you a different runtime.
And then you can, on the one hand, basically see the numbers that I'm going to show you.
And hopefully see the same numbers.
You can tell me how I can do better because maybe I can get it smaller.
That would be awesome.
And the other thing is also like you see the code that you see basically the integration with where
you have the momentum and with each of the runtime.
So if you want to start playing with that, I think this would be kind of a cool place to start.
So these are the runtime to be evaluated.
We have like the three rest ones.
Like where's me? Where's the time?
Which I was very happy is going more embedded direction now.
I think they also need help with like for some of the architecture.
So like if you want to contribute somewhere, that would be a cool place because we would be awesome if you could run
on embedded, tiny ways into my knowledge.
The restaurant time with a smaller footprint.
And of course we had to consider whammer.
Because that's like the standard for running,
WebAssembly and MCU.
And sadly we have seen why.
Because these are numbers.
Like you see, where's me in principle is intended to run on
constraint devices, but it needs like 10 types more memory than whammer.
Tiny weather, which is like the best one which we had written in rough.
Still needs two to three times more.
And also important to highlight.
Like all three.
Where's me? Where's the time in tiny weather?
Running in trouble to mode here.
Where? Whammer runs in AOT.
Like AOT is like 10 times faster.
It performance wise.
Then in trouble at any time you see these numbers when someone says that like
Where's him introduces only like twice and overhead,
compared to native, they always talk about AOT.
So I'm not talking very much about performance here,
but whammer also outperforms significantly like those three.
So that was bad.
Because you know, it means we cannot use the restaurant time.
And the other thing is we still want to integrate with embassy.
We still want to integrate with synchronous embassy,
which is kind of a problem because whammer is a synchronous sea library.
So that's kind of the last thing I want to talk about,
like how we did that.
First just to like kind of reiterate on what we are talking about here.
We're talking about this specific situation.
Well, you can imagine you are running whammer on your MCU.
And just kind of starts up there.
Essentially it's a module.
The module starts running.
And at some point, it now requires something from a peripheral.
For example, like imagine it just wants to know when someone
pushed a button.
So it will call to the host and just kind of say, hey,
tell me when they push a button.
Now what we want to happen is that whammer just triggers this
asynchronous functionality, kind of blocks until the button pushes
there.
And then gives control back to the module.
And I think probably, like for the people coming from the Linux side,
you at this point kind of know what I'm talking about.
It's like not a big deal.
Like everybody has done that.
If we are talking, if we are on Linux,
like anybody using Tokyo has probably have the situation of having
to work with something which hasn't synchronous interface.
And how you solve it is you just say, okay, fine.
We will just separate the asynchronous from the synchronous
functionality.
Whammer gets its own threat.
It's just running there.
They talk through some way of communicating, say, through channels.
So when whammer wants something asynchronous,
it will communicate this intent.
And it will go to sleep a way in the response.
It's fine.
The OS will wake it up as soon as the synchronous functionality is done.
And that's like a sing-async bridge.
Now, the problem, again, we are on an ESPC-3.
You specifically do C-Series.
So we don't have an OS to wake us up.
We don't have threats to separate it.
We can do the embassy thing and just say, okay, let's put them in different tasks.
But as soon as whammer blocks, it blocks the entire context.
So this doesn't solve it.
And this was something that we saw, like as a serious problem for us.
And we were, at this point, it really looked like a tradeoff.
It really looked like a tradeoff between, do we go for the lower footprint
that whammer gives us?
Or do we have to go away from embassy?
But the cool thing was that, like a colleague of mine,
Asunder, he has solved this problem in a very cool way,
which actually gives us kind of the best of both worlds.
And the solution is essentially, we do put both whammer.
And the synchronous services each in their own task.
Like a synchronous, like the whammer task is not really a synchronous,
but that's how it looks on embassy.
And the important thing is a synchronous host service
has a higher priority.
And now these two talk via signals.
And so how this exchange now works is,
when whammer needs something from their synchronous side,
it will signal it.
And then it will try to read the response signal.
And if it doesn't find anything,
it goes to sleep using waiting for interrupts.
Now, what do their synchronous host services do?
They do basically the things that they always do.
When we tell them what we need,
they register an interest in an interrupt,
and go to sleep themselves.
And so at this point, we are at the very cool point
where nobody is busy waiting.
Nobody is burning CPU cycles.
Nobody is using up any power.
But we still are 100% reactive to interrupts
and in a very efficient way.
So if an interrupt comes in that we are not interested in.
Anything doesn't care.
Whammer will briefly wake up.
Check its response signals.
See there's nothing there and go to sleep again.
If we get the interrupt, we are actually waiting for.
Like in this case, the button push.
Then the asing side, it will get to act first
because it is a higher priority.
It will process the response.
It will send the response signal to whammer.
So that when whammer gets to act, it just finds it result.
It can give the control back to the module
and we have exactly the behavior we wanted.
So this was very cool.
I mean, this was honestly something which we saw as a problem
and at the end.
This is the way we are integrating whammer.
This gives us a small memory footprint, high performance
and like the full integration and the embassy ecosystem.
So we wanted to share this because this is cool.
Yeah, and with that, I'm at the end.
Again, everything I was telling about the integration.
I was just doing the talking.
That's all a Sunday.
This is really cool.
That's his GitHub handle.
Check it out.
Also, while working on this,
he has been dealing a lot with integrating
like a sea library into like rust on embedded.
And for that purpose, he has actually published
the sea combat crate.
This really knew.
So like given also like a little bit of time
it's like not perfect yet, but the purpose behind it is
that you can interact with sea libraries without requiring
any Windows specific tooling and also without requiring
like root privileges.
So that is pretty cool.
Decide that.
You can check out the repo with a benchmark.
And also, this is just like one of the many interesting
problems we deal with while working on Mermack.
Mermack will be open source later this year.
We're probably going to be doing a pre-launch test.
So also check out the website.
Yeah.
And that's pretty much it.
I hope it was interesting.
And I hope we have time for some questions.
Thank you.
Yes.
So you talked a bit about the A-Sink Hall
in the very moment of the interesting.
Is it possible for another layer A-Sink Hall
within the last few tasks to open the error?
A-Sink Hall, right?
So, if I, yes.
Yeah.
I repeat the question just to make sure that I understood correctly.
So you are asking whether it's also possible to make it
so that we can write a synchronous code and compile it
to WebAssembly.
Yeah.
So there are two parts to this.
So short answer is yes.
It likely is possible.
But it's like not that simple.
So from the runtime side, the one thing you need
is you need an ability to execute anything A-Sink.
So that would already be solved with this integration we had.
Now from the module side, the problem is a little bit
like when you write WebAssembly.
Everything for the module looks blocking.
So then you have to set up some infrastructure
where I mean you would need to define specific host functions,
which mimic this asynchronous behavior,
where you like kind of block from the inside,
but actually you signal your interest in some event
and also give it like a function to call back.
But also,
I'm not sure if it's that difficult,
but it's also not super simple.
Okay, I think that's all the time we have.
We had a bit of a late start.
Thank you again, Fedor.
Awesome, thank you.