Could Hubris and WebAssembly Allow High-Level Hardware Emulation?
WebAssembly is a specification for a portable binary execution format, which has grown far beyond its original intent of simply providing an alternative runtime for running code in web browsers. Notably, it has a segmented memory model that is unlike the usual flat address space most programs are accustomed to running in. Not that programs don’t usually run within some level of memory segmentation, but it’s usually not something tracked alongside your pointers and is handled a bit lower level than that. WebAssembly also makes complicated memory allocation environments where multiple library may have multiple memory allocators a bit of a bear to get working, and this has been the source of much debate hammering out the standard moving forward. Therefore, WebAssembly tends to be at its utmost smoothest when everything is statically linked, memory regions are known statically, and there’s no dynamic allocation outside these regions at runtime. And here’s where things get interesting, because that’s exactly what you get with Hubris.
I wrote about Hubris just a few days ago so I’m not going to rehash the explanation I gave there. Go read the start of that, or read Cliff Biffle’s post about it for the more comprehensive overview if this is new.
Hubris is a kernel designed to run on embedded systems. These systems typically have flat memory layouts, and low amounts of total system memory (think in the kilobytes to megabytes). Hubris has a task model whereby all tasks are given a fixed amount of memory at a fixed address during compile time. This memory allotment does not change over the runtime of the application. Tasks can not share memory between each-other except by passing around Leases. A task with a lease can ask the kernel to access the lease’s memory region on its behalf.
Now, Hubris itself is intended to be portable across multiple CPUs, with all the low-level ARM stuff that’s ARM or Cortex-M specific self contained enough to allow for a potential Risc-V implementation in the future or something like that. So, what if we compiled Hubris for WebAssembly?
WebAssembly (wasm) outside the browser is interesting because there are existing wasm runtimes that already implement the actual execution of the wasm bytecode, and are flexible enough to be molded into doing whatever hairbrained ideas you want just by forking them and messing around with the implmentation. I believe all the platform-specific stuff in the kernel and userlib for Hubris can be implemented by providing a function body in the rust code that just calls out to the wasm runtime. Then, you can implement the logic to make it actually do what it’s supposed to do in the wasm runtime itself.
Why would you want to do this though?
Well, not just for the sake of putting Hubris somewhere weird, although that is neat. No, what’s interesting about this is it could allow for high-level emulation of target embedded systems for the purpose of automated testing.
So like, think about how you’d test something that talks to a database or a web API. Generally you create something that pretends to be that database or API, but is actually just some simple logic that expects the code you’re testing to send over some specific sequence of data, and provides a plausible response to that data in return. This doesn’t guarantee that your code is actually sending a valid SQL query or json object or whatever, but it does let you know that your code is doing what you think it should be doing, and you can automate it such that you’ll know if it ever stops doing that.
Running a Hubris app inside a web-assembly runtime could make it easier to do these sorts of tests. You could implement a virtual GPIO and serial peripheral, and create a test that makes sure your serial transfer task is indeed writing the configurations you expect to the GPIO and serial control registers. You can pretend to be the device that would sit on the other end of that serial connection, and see if you’re getting the datastream you’re expecting. It should be less complicated and potentially more performant to do this in a wasm runtime, rather than trying to hack it into qemu or something (which apparently doesn’t emulate things well enough for hubris to work anyway). At the same time, it gives you a test environment more similar to the real hardware than, for example, testing the task’s code with everything stubbed at the function level.
This wouldn’t be a substitute for testing on hardware, since the emulation is only as good as your knowledge of the hardware you’re emulating. You also may not be able to compile the full app as it would be deployed on hardware, and may have to omit tasks that rely on inline assembly or anything cortex-m specific. But, I think it could complement it. These sorts of tests could catch things well before you even get to the point of testing on hardware, saving you write-cycles on your dev boards, while being easier to integrate into an automated git pipeline.
Barriers to making this work
The first and foremost problem I’ve already mentioned: making a runtime that could run Hubris and emulate the hardware features it needs at a high level. Because of wasm’s memory model it probably doesn’t make sense to implement this with the assumptions the ARM variant of Hubris makes. The microcontrollers have a flat memory model, whereas wasm is segmented even under the hood unless you provide a way to break out of those semantics. This might require some changes to the hardware-independent sections of Hubris, but I don’t know enough about it to say.
That said it might(?) be possible to actually give Hubris and all its tasks a flat memory model for the purposes of being more true to hardware, but it would require some clever tricks during the compilation and linking steps to get rust to actually do that. Even if it is possible, I don’t know of it makes sense to go through the effort unless Hubris really needs it to work. Take this with some salt though; it’s been awhile since I read into the details of wasm’s memory model, so much of what I’m saying about it comes from talking with my friend who created the innative wasm runtime. He’s had far more experience dealing with the toolchain at this level than I have, and it’s possible I misunderstood some of what he was telling me.
With that caveat, there’s another challenge, which is peripheral access. Interacting with peripherals on ARM chips is as simple as writing or reading predefined memory addresses. That’s sort of a problem with web-assembly though because web-assembly really does not want to represent this sort of access. Memory access requires both a memory region and an offset into that region, and you can’t just cast a constant to a pointer and expect it to compile into something sensible as far as I’m aware.
The way peripheral access crates are made actually provides a potential way out of this though. Peripheral access crates are a bunch of fancy wrappers around the raw pointers that make them nicer to work with, and they’re auto-generated from XML descriptions of the chips they’re created for. The same XML could be used to generate a drop-in crate that replaces the reads and writes with accesses into a dedicated memory segment for IO. The runtime could then pick up where the hardware normally would and emulate the memory mapped IO in the same way. Or, if you wanted, you could forego the memory song and dance entirely and make the pac readers/writers call a special wasm function instead.
Then you’d need to get your build system to override the pac crate with your runtime-specific crate, and hopefully that works out the way you want.
Is it a pipedream?
I’m not sure! And that’s part of why I’m writing this, is to find out. This is all just what I can think of right now, having only poked at this very briefly changing a few target values to
wasm32-unknown-unknown in Hubris. It seems plausible to me, but I wonder if I’m missing something that makes it infeasible in practice. Maybe I’m not, and I’m onto something. Either way, now the idea is out of my head and into someone else’s.