uvm32: a RISC-V sandbox in a single C file for microcontrollers
Minimalist, dependency-free virtual machine sandbox for microcontrollers and other resource-constrained devices. Single C file, no dynamic memory allocations, asynchronous design, pure C99
At a glance
- What is it?
- uvm32 is a dependency-free virtual machine that emulates RISC-V, with no dynamic memory allocation, a non-blocking design and a footprint under 3KB of flash and 1KB of RAM on an STM32L0. It suits a firmware team that needs to run untrusted or unreliable logic safely, and it is the wrong tool for anything that expects a scripting language.
- Who is it for?
- uvm32 fits a firmware team that has logic it does not fully trust, or that wants a plugin mechanism where bad code cannot take the device down, since the whole VM is one C file with no dependencies, no heap and an instruction budget that stops a runaway program. It is the wrong choice if you want a scripting language, if you want the host and the guest to call each other freely, or if you need raw speed, since the project explicitly does not aim for maximum efficiency.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 99 days ago.
- What is it written in?
- Mainly C, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A virtual machine sized for a coin cell
uvm32 is described as a minimalist, dependency-free virtual machine sandbox designed for microcontrollers and other resource-constrained devices, in a single C file, with no dynamic memory allocations, an asynchronous design and pure C99. The footprint figure is the one to hold on to: on an STM32L0, an ARM Cortex-M0+ part, the required footprint is under 3KB of flash and 1KB of RAM.
That number explains the whole design. A conventional embedded scripting engine such as Lua, Duktape or MicroPython brings a heap, a garbage collector, a parser and a standard library, and on a part with 32KB of RAM that is a large fraction of the device. uvm32 takes the opposite approach. It is a RISC-V emulator, wrapped in a management interface and provided with tools to build efficient code to run in it, so the guest is a small RISC-V program with a fixed memory budget rather than a language runtime.
The stated purpose of the sandbox is to isolate untrusted or unreliable elements of a system, and the stated design bias is explicit: the project aims for safety over speed, and bad code running in the VM should never be able to crash the host.
The comparison with the alternatives is honest rather than promotional. The readme lists what uvm32 aims for: small footprint suitable for embedded devices, games and apps, support for well-known programming languages for VM code with high quality dev tools, ease of integration into existing software, flexibility of paradigm across event driven, polling and multi-processor use, and robustness against misbehaving VM code.
And it lists what it does not aim for: frictionless FFI, since there are no direct function calls between host and VM code, maximum possible efficiency, the simplest scripting experience for VM code, because a develop-compile-run cycle is expected, and batteries-included libraries for stdio or networking.
The event loop, the instruction budget and the syscall seam
The host interface is small enough to read in one sitting, and the example host shows all of it.
The guest program is loaded as a byte array, and the host initialises a state, loads the image and then loops:
#include "uvm32.h"
#include "uvm32_common_custom.h"
uint8_t rom[] = { // mandel.bin
0x23, 0x26, 0x11, 0x00, 0xef, 0x00, 0xc0, 0x00, 0xb7, 0x08, 0x00, 0x01,
...
};The run call takes an instruction budget, and the comment on that line explains the parameter as the number of instructions before the VM is considered hung:
while(isrunning) {
uvm32_run(&vmst, &evt, 100); // num instructions before vm considered hung
switch(evt.typ) {
case UVM32_EVT_END:
isrunning = false;
break;That budget is the non-blocking design in practice. A guest that enters an infinite loop does not hang the host, it simply stops making progress, and the host is free to decide what to do about it. Combined with the design goal of preventing misbehaving bytecode from stalling the host, it means a plugin cannot take the device down by spinning.
Communication with the host happens through syscalls, which is the seam the FFI story runs through. The host switches on the event type and then on the syscall code, handling a character output, a line output and a yield, with a default branch that prints an unhandled syscall code. The FFI is described as safe and minimally typed, and the readme is clear that it is not frictionless: there are no direct function calls between host and VM code.
The VM also makes no assumptions about host IO capabilities, with no stdio assumed, which is what allows the same guest to run on a microcontroller with no console at all.
Four languages, one instruction set
The guest side is not restricted to one language, and the sample applications are the best evidence of how much is possible.
There are C sample apps, including a hello world, a demonstration of malloc on extram, a console IO demo, a lissajous curve that shows softfp and floating point, a recursive ASCII art maze generator, iterative and recursive fibonacci, and a program written in setup and loop style for Arduino, Wiring and Processing.
There are Rust sample apps, with a hello world, and the readme notes that the Rust version installed by brew on macOS has issues, so the official installer should be used. There are Zig apps: an ASCII mandelbrot generator, a Tetris, a demonstration of the Zig allocator working with extram, a port of PureDOOM that uses Zig to provide an allocator and libc-like functions, TinyGL gears as a softfp stress test, and a Nintendo Entertainment System emulator that the readme marks as currently very slow. There is a minimal hello world in assembly.
The most telling sample is apps/self, a host-mini with an embedded mandelbrot generation program compiled as an app, which is a VM running a VM. It is the clearest statement of the design: the host is itself just a program that can be compiled for the same target.
The apps list also includes a heap sample demonstrating malloc on extram, and the Zig allocator sample doing the same thing with Zig, which tells you extram is a real concept in this project, some form of memory available to the guest outside the VM's own bounds.
Because the guest is RISC-V, compiling to it means targeting riscv32, and the project supports that toolchain path through its Docker image, which installs a RISC-V cross compiler and adds the riscv32im-unknown-none-elf Rust target. So the promise of running in modern systems languages on a microcontroller is real, with the cost of a cross-compilation step for each guest program.
Hosts for every shape of device, including a debugger
The hosts directory shows how many different roles the VM can play, and one of them is unusual enough to call out on its own.
There is a plain host that loads a binary and runs it to completion while handling several syscall types, a minimal host with baked in bytecode, and a parallel host that runs multiple VM instances concurrently, also with baked in bytecode. There is a host as an Arduino sketch, where make test runs AVR code in qemu, and an SDL host providing framebuffer and audio support, which is what the zigdoom sample uses.
Then there is the gdbstub host, which supports the GDB remote serial protocol connection and acts as a server, allowing gdb debugging of running code. That is a significant piece of engineering for a project of this size. Debugging a guest program in a VM on a microcontroller, where you have no debugger and often no console, is exactly the situation where a gdb stub earns its keep, and it means the developer experience does not have to degrade to printf debugging.
The build system reflects the same breadth. The top-level Makefile descends into three subdirectories, test, hosts and apps, and offers the ci target, the test target, a distrib target that copies the built application binaries into precompiled/, and a set of Docker targets. The Docker image is built on ubuntu:25.04 and installs a RISC-V cross compiler, Zig through zvm, Rust with the riscv32im-unknown-none-elf target, the Arduino CLI with the AVR core, qemu for the Arduino tests, SDL3 for the SDL host, gcovr for coverage and ruby and xxd for the unity tests.
The docker target mounts the working directory and runs make inside the container, and dockershell gives an interactive shell in the same image, so the whole toolchain, from cross compilers to emulators to coverage reporting, is reproducible from one Dockerfile.
Where uvm32 is the wrong choice
The first limitation is stated by the project itself and it is the one to internalise: it does not aim for the simplest scripting experience for VM code. A develop-compile-run cycle is expected. There is no Lua-style REPL where a device engineer types a line and sees it happen, and there is no batteries-included library for stdio or networking. Every capability the guest has is one the host chose to expose as a syscall.
The second is that the FFI is deliberately not frictionless. There are no direct function calls between host and VM code, and the FFI is described as minimally typed. If your plugin needs to call back into host logic with rich data structures, the cost is marshalling through a narrow, typed seam rather than a normal call, and the ergonomics of that are yours to build.
The third is the performance position. The project does not aim for maximum possible efficiency, and it aims for safety over speed. For a configuration script or a plugin format that runs occasionally, that is the right trade. For something on a hot path, it is not, and a purpose-built native component would serve better.
The fourth is that the emulation base is a real CPU emulator, which is a source of both power and overhead. The readme is candid that although it is based on the fully fledged mini-rv32ima CPU emulator, uvm32 is intended for executing custom script-like logic and not for simulating hardware. An instruction-level emulator gives you a very well defined target for a compiler to aim at, and it also means the guest executes an emulated RISC-V machine rather than native code, which is exactly the price you pay for the isolation.
The fifth is the ecosystem and the release story. The repository publishes no GitHub releases, the last push was on 2026-07-16, and the sample applications are described with an honesty that ranges from impressive to sobering, since the NES emulator is currently very slow and the SDL host and the parallel host are described as more advanced examples. There is a Discord badge and a project site, and that is the extent of the visible support infrastructure.
The sixth is the language mix. The C, Rust, Zig and assembly examples are compelling, but each guest language needs a toolchain that can target riscv32im-unknown-none-elf, and the Rust example already carries a warning about the brew-installed compiler on macOS. Cross-compilation setup is part of the cost of using this, not an aside.
Editorial conclusion
uvm32 fits a firmware team that has logic it does not fully trust, or that wants a plugin mechanism where bad code cannot take the device down, since the whole VM is one C file with no dependencies, no heap and an instruction budget that stops a runaway program. It is the wrong choice if you want a scripting language, if you want the host and the guest to call each other freely, or if you need raw speed, since the project explicitly does not aim for maximum efficiency. Before adopting it, read the aims and the does-not-aim-for lists in the readme, and try the host-mini example first, since the last push was on 2026-07-16 and the repository publishes no GitHub releases.
Frequently asked questions
What is uvm32?
It is a minimalist, dependency-free virtual machine sandbox for microcontrollers and other resource-constrained devices, in a single C file with no dynamic memory allocations, an asynchronous design and pure C99. On an STM32L0 the required footprint is under 3KB of flash and 1KB of RAM.
How does uvm32 keep a misbehaving program from crashing the host?
The run call takes an instruction budget, the number of instructions before the VM is considered hung, and the design is non-blocking so bad bytecode cannot stall the host. The project states that it aims for safety over speed and that bad code running in the VM should never be able to crash the host.
Which languages can I use to write uvm32 bytecode?
The sample applications are written in C, Rust, Zig and assembly, and the guest runs RISC-V, so a program is compiled for a riscv32 target. The Rust example notes that the brew-installed Rust on macOS has issues and the official installer should be used instead.
How do I build and test uvm32?
The top-level Makefile descends into the test, hosts and apps directories, with a test target, a ci target and Docker targets that run the same commands in a container. The Docker image is based on ubuntu:25.04 and installs the RISC-V cross compiler, Zig, Rust with the riscv32im-unknown-none-elf target, the Arduino CLI, qemu, SDL3 and gcovr.
Is uvm32 a scripting language I can use instead of Lua?
Not in the way you would use Lua. The project states that it does not aim for the simplest scripting experience for VM code and that a develop-compile-run cycle is expected, so guest programs are compiled to RISC-V bytecode rather than interpreted from a script. The FFI is also deliberately minimal, with no direct function calls between host and VM code.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/ringtailsoftware-uvm32)Was this useful?