I have always been passionate and curious about systems programming. At a first glance, this area of knowledge might seem very complicated and even impenetrable for a lot of people. Thankfully, with Rust, the tide has started to turn: the Rust community has demonstrated that sometimes the perceived complexity is rooted in the lack of support, tools, and educational content.
In this blog post, I want to introduce you to ideas behind Low-Level Academy, a course with the goal of democratizing Rust and systems programming education further. Bret Victor’s post on learnable programming was written back in 2012 but it’s still just as relevant in 2020: with the advent of technologies like WebAssembly, now is the best time to expand these ideas to new areas. With WebAssembly, we can create complex simulations and playgrounds right in our browsers, all while giving people insight into what is happening under the hood of a typical program.
This is the main idea that underlies our courses: we don’t just want to describe how a given system works, we want to show you how to create it, all while demonstrating what happens inside. Right now, we're applying this approach to network programming, but we will keep expanding it to more topics like process management on Linux and memory allocation.
Repurposing the Rust playground
It started with a hack based on the Rust playground. The Rust playground is one of the best learning environments for Rust because of its ease of use: you don’t have to install the Rust compiler on your system; all you need to do is to open it in your web browser and start coding. It even works on mobile devices.
It’s interesting and worthwhile to look at how the Rust playground works: it runs your code on the playground server and returns the output to your browser.
I also thought it would have been fun to try compiling more complex Rust libraries into WebAssembly.
This is how Low-Level Academy was born. It combines the ease of use and minimal setup of the Rust playground with ideas behind Bret Victor’s learnable programming to teach low-level concepts.
How it works
When you click ‘Run’, the code is transferred to our server which executes the Rust compiler, setting the target output to WebAssembly. The resulting module is downloaded and executed in your browser—no surprises here. When it gets more complicated, though, is when we start adding libraries into the equation: a typical Rust WebAssembly module that includes dependencies has a size of around 1-2 megabytes, which is a bit too heavy if we need to transfer it for each request—it would introduce perceivable delays and would not be too polite to users with data caps or mobile devices.
To solve this problem, we devised a different approach.
The code that a user writes in the playground is compiled without using any libraries. This allows us to generate minimal modules of only 35-40 KB in size. We also have a separate module that does all the heavy lifting, such as running the simulation (e.g., the virtual network and the TCP/IP stack), and it has no size restrictions because it needs to be downloaded only once. It then exposes an API that is used by the playground code.
The lightweight user modules are “linked” with the heavy module. The problem with WebAssembly is that dynamic linking is not defined formally in the specification (there’s only a draft), and there is no simple way to call functions exported by one individual module from another because they don’t have shared memory and state. To solve this problem, we copy memory between modules when we need to call the API exposed by the library module:
Another problem with this approach is that exposed WebAssembly APIs are low-level and aren’t suitable for consumption by human beings. Besides, users expect the availability of APIs they are familiar with—for example, for network programming, that’s the std::net module from the Rust standard library.
To address these needs, we reimplement a part of the standard library: the user’s playground code is wrapped into a package that contains a UdpSocket API implementation that is compatible with the standard library API and uses functions exposed by the simulation module:
But this is only a sneak peak of what we can do with this approach. There is much more in the realm of possibilities.
When all the pieces are put together, we get an interactive programming environment that is tailored for our educational needs. This framework opens up a lot of interesting possibilities: we can visualize not only networks, but also memory, OS processes, file systems internals, async tasks, and a lot more.
While we are still far away from the ideals presented in Bret Victor’s article, it’s only a start. WebAssembly is versatile and treats all code equally, so we could even go one step further and compile parts of the Rust compiler itself into WebAssembly modules, and use them for education. For example, this way we can implement IDE-like context hints in the code editor or step-by-step execution, which can be instrumental for demonstrating concepts like asynchronous I/O or multithreading in a clear way.
Give it a try
If you would like to follow our updates, you can follow us on Twitter or subscribe to our email list: