This article is a guest post written by our interns at RIoT Secure, as part of a multi-part series exploring the evolution of WebAssembly (WASM) and the WebAssembly System Interface (WASI). Each post in this series takes a closer look at the technology’s journey, challenges, and opportunities - with a special focus on what these developments mean for IoT and secure edge computing.
Zero-Bloat Starter Kits for BRAWL (WASM) - Demos in WAT, C, Rust and Go
In our previous post, we evaluated the feasibility of compiling high-level languages into WASM with a specific focus on the size of the binaries being produced. It was clear that not every language would be feasible, due to bringing in a complete runtime environment - specifically JavaScript, Go and Python. There definitely was some promise, especially for C, Rust and TinyGo. While writing WebAssembly (WASM) modules directly in WAT gives us complete control, it’s not exactly the most ergonomic way to develop real-world applications.
In this post, we explore what happens when the same minimal demo - our bouncing ball demo that we previously highlighted - is compiled from higher-level languages like C, Rust, and TinyGo. The goal: see how close we can stay to the hand-written WAT baseline without introducing unnecessary runtime baggage. The results show that with the right compiler and linker flags, it’s entirely possible to build zero-bloat WASM modules in modern languages, staying true to the WebAssembly MVP specification and keeping binaries small enough to run even on constrained devices.
Bouncing Ball Demo
In an earlier blog post: RIoT Secure: WebAssembly on an Arduino UNO R4 WiFi we demonstrated, complete with a video, an Arduino UNO R4 WiFi micro-controller that integrates a 12x8 LED Matrix running the BRAWL WASM runtime with a small demo written using WebAssembly Text (WAT).
Building on the original bouncing ball demo designed for the Arduino UNO R4 WiFi, we extended the WASI interface to be more generic - so the same code runs on an ESP32 micro-controller paired with a larger LED matrix. With the help of our mentor, this is the resulting demo setup:
This serves two purposes: it demonstrates the portability of our BRAWL-based WASM modules across different hardware targets, and it allows us to push the boundaries further by observing how each toolchain behaves when the typical microcontroller constraints are relaxed. The ESP32 setup offers a more generous environment for debugging, profiling, and visualizing performance - while still retaining the embedded context that keeps our optimizations honest.
Porting WebAssembly Text (WAT) To C, Rust and Go
With the baseline bouncing ball demo already defined in WebAssembly Text (WAT), the next step was to port the same logic to higher-level languages - C, Rust, and Go - while preserving the exact behavior and function interfaces. Each port directly maps to the original WebAssembly Text (WAT) module’s structure and imports, ensuring a one-to-one functional comparison when compiled back to WASM. This approach lets us evaluate how much additional overhead each toolchain introduces, not in features or abstraction, but purely in binary size and runtime footprint, under identical logic and execution flow.
- source: "WebAssembly Text (WAT)"
- source: "C"
- source: "Rust"
- source: "Go"
;; ------------------------------------------------------------ ;; ;; ball_demo.wat ;; (c) 2025 RIoT Secure AB ;; ;; https://www.hackster.io/Ripred/bouncing-balls-sketch-on-uno-r4-wifi-led-matrix-d0657e ;; ;; This module implements the LED Bounce example on hackster.io ;; for the Arduino UNO R4. The sketch uses WebAssembly host ;; functions. It generates 5 random balls and movement, then ;; proceeds to animate them with a small delay between. ;; ;; F32 - float math (optimal for MCU with FPU) ;; ;; Host functions expected: ;; (import "brawl:device/matrix" "dimensions" (func ...)) ;; (import "brawl:device/matrix" "begin" (func ...)) ;; (import "brawl:device/matrix" "loadPixels" (func ...)) ;; (import "brawl:device/sys" "delay" (func ...)) ;; ;; Entry point: ;; (start $start) -- calls setup() once, then repeatedly loop() ;; (module ;; ---- imports ---- (import "brawl:env/debug" "print_i32" (func $print_i32 (param i32))) (import "brawl:device/matrix" "dimensions" (func $dimensions (result i32))) (import "brawl:device/matrix" "begin" (func $begin)) (import "brawl:device/matrix" "loadPixels" (func $loadPixels (param i32 i32 i32))) (import "brawl:device/sys" "delay" (func $delay (param i32))) ;; ---- memory ---- (memory (export "mem") 1) ;; 4KB of memory ;; ---- constants ---- (global $POINTS i32 (i32.const 5)) ;; number of points to display ;; ---- constants :: memory locations ---- (global $RNG i32 (i32.const 0x0000)) ;; offset: 0x0000 - random seed (4 bytes) (global $FB i32 (i32.const 0x0100)) ;; offset: 0x0100 - framebuffer (512 bytes max) (global $STATE i32 (i32.const 0x0300)) ;; offset: 0x0300 - dot struct (x,y,dx,dy as f32)) ;; ---- random number generator ---- (func $rand32 (result i32) (local $s i32) (local.set $s (i32.load (global.get $RNG))) (local.set $s (i32.add (i32.mul (local.get $s) (i32.const 1664525)) (i32.const 1013904223))) (i32.store (global.get $RNG) (local.get $s)) (local.get $s) ) ;; ---- start function which includes the setup and loop ---- (func $start (local $dims i32) (local $dim_x i32) (local $dim_y i32) ;; dimensions (local $i i32) (local $base i32) (local $tmpi i32) ;; counters, temp integer (local $xi i32) (local $yi i32) ;; integer coords for framebuffer (local $random i32) (local $area i32) ;; used for randomization (local $x f32) (local $y f32) (local $dx f32) (local $dy f32) ;; current dot values (floats) (local $nx f32) (local $ny f32) (local $tmpf f32) ;; next positions (floats) (local $MAXX f32) (local $MAXY f32) ;; bounds ;; set the initial random seed (i32.store (global.get $RNG) (i32.const 0x12345678)) ;; device init (call $begin) ;; get the LED dimensions (call $dimensions) (local.set $dims) (local.set $dim_x (i32.shr_u (local.get $dims) (i32.const 16))) (local.set $dim_y (i32.and (local.get $dims) (i32.const 0xFFFF))) (call $print_i32 (local.get $dim_x)) (call $print_i32 (local.get $dim_y)) ;; bounds as f32 (local.set $MAXX (f32.convert_i32_u (local.get $dim_x))) (local.set $MAXY (f32.convert_i32_u (local.get $dim_y))) ;; ---- initialize points ---- (local.set $i (i32.const 0)) (loop $INIT (local.set $random (call $rand32)) (local.set $area (i32.mul (local.get $dim_x) (local.get $dim_y))) ;; area = dim_x * dim_y (local.set $tmpi (i32.rem_u (local.get $random) (local.get $area))) ;; tmpi = random % area ;; x = (tmpi % dim_x), y = (tmpi / dim_x) (local.set $x (f32.convert_i32_u (i32.rem_u (local.get $tmpi) (local.get $dim_x)))) (local.set $y (f32.convert_i32_u (i32.div_u (local.get $tmpi) (local.get $dim_x)))) ;; dx = ± (1 / (2 + ((random >> 16) % 3))) (local.set $tmpi (i32.add (i32.const 2) (i32.rem_u (i32.shr_u (local.get $random) (i32.const 16)) (i32.const 3)))) (local.set $dx (f32.div (f32.const 1) (f32.convert_i32_u (local.get $tmpi)))) (if (i32.and (call $rand32) (i32.const 1)) (then)(else (local.set $dx (f32.neg (local.get $dx))))) ;; dy = ± (1 / (2 + ((random >> 24) % 3))) (local.set $tmpi (i32.add (i32.const 2) (i32.rem_u (i32.shr_u (local.get $random) (i32.const 24)) (i32.const 3)))) (local.set $dy (f32.div (f32.const 1) (f32.convert_i32_u (local.get $tmpi)))) (if (i32.and (call $rand32) (i32.const 1)) (then) (else (local.set $dy (f32.neg (local.get $dy))))) ;; store point (local.set $base (i32.add (global.get $STATE) (i32.mul (local.get $i) (i32.const 16)))) (f32.store (i32.add (local.get $base) (i32.const 0)) (local.get $x)) (f32.store (i32.add (local.get $base) (i32.const 4)) (local.get $y)) (f32.store (i32.add (local.get $base) (i32.const 8)) (local.get $dx)) (f32.store (i32.add (local.get $base) (i32.const 12)) (local.get $dy)) (local.set $i (i32.add (local.get $i) (i32.const 1))) (br_if $INIT (i32.lt_u (local.get $i) (global.get $POINTS))) ) ;; ---- animation loop ---- (loop $FOREVER ;; clear framebuffer (local.set $i (i32.const 0)) (loop $CLR (i32.store8 (i32.add (global.get $FB) (local.get $i)) (i32.const 0)) (local.set $i (i32.add (local.get $i) (i32.const 1))) (br_if $CLR (i32.lt_u (local.get $i) (i32.mul (local.get $dim_x) (local.get $dim_y)))) ) ;; draw points (local.set $i (i32.const 0)) (loop $DRAW (local.set $base (i32.add (global.get $STATE) (i32.mul (local.get $i) (i32.const 16)))) (local.set $x (f32.load (i32.add (local.get $base) (i32.const 0)))) (local.set $y (f32.load (i32.add (local.get $base) (i32.const 4)))) ;; convert to integer and clamp (local.set $xi (i32.trunc_f32_u (local.get $x))) (local.set $yi (i32.trunc_f32_u (local.get $y))) (if (i32.and (i32.lt_u (local.get $xi) (local.get $dim_x)) (i32.lt_u (local.get $yi) (local.get $dim_y))) (then ;; turn on the pixel in the framebuffer (i32.store8 (i32.add (global.get $FB) (i32.add (local.get $xi) (i32.mul (local.get $yi) (local.get $dim_x)))) (i32.const 1)) ) ) ;; move to the next point (local.set $i (i32.add (local.get $i) (i32.const 1))) (br_if $DRAW (i32.lt_u (local.get $i) (global.get $POINTS))) ) ;; render framebuffer to LED display (call $loadPixels (global.get $FB) (local.get $dim_x) (local.get $dim_y)) (call $delay (i32.const 50)) ;; update with bounce physics (local.set $i (i32.const 0)) (loop $UPD (local.set $base (i32.add (global.get $STATE) (i32.mul (local.get $i) (i32.const 16)))) (local.set $x (f32.load (i32.add (local.get $base) (i32.const 0)))) (local.set $y (f32.load (i32.add (local.get $base) (i32.const 4)))) (local.set $dx (f32.load (i32.add (local.get $base) (i32.const 8)))) (local.set $dy (f32.load (i32.add (local.get $base) (i32.const 12)))) ;; check if next position required a bounce to occur (local.set $nx (f32.add (local.get $x) (local.get $dx))) (if (i32.or (f32.lt (local.get $nx) (f32.const 0.0)) (f32.ge (local.get $nx) (local.get $MAXX))) (then (local.set $dx (f32.neg (local.get $dx)))) ) (local.set $ny (f32.add (local.get $y) (local.get $dy))) (if (i32.or (f32.lt (local.get $ny) (f32.const 0.0)) (f32.ge (local.get $ny) (local.get $MAXY))) (then (local.set $dy (f32.neg (local.get $dy)))) ) ;; calculate next position (local.set $x (f32.add (local.get $x) (local.get $dx))) (local.set $y (f32.add (local.get $y) (local.get $dy))) ;; store values (f32.store (i32.add (local.get $base) (i32.const 0)) (local.get $x)) (f32.store (i32.add (local.get $base) (i32.const 4)) (local.get $y)) (f32.store (i32.add (local.get $base) (i32.const 8)) (local.get $dx)) (f32.store (i32.add (local.get $base) (i32.const 12)) (local.get $dy)) ;; increment out counter and move to the next dot (local.set $i (i32.add (local.get $i) (i32.const 1))) (br_if $UPD (i32.lt_u (local.get $i) (global.get $POINTS))) ) (br $FOREVER) ) ) (export "start" (func $start)) (start $start) ) ;; ------------------------------------------------------------
To reveal the associated source code, simply click on the list points above.
As enjoyable as it was to craft the original module directly in WAT, returning to high-level languages is definitely a welcome relief. Working in C, Rust, or Go brings back all the familiar comforts - readable syntax, structured tooling, and a proper build pipeline - without sacrificing the control needed for fine-grained optimization. It’s a reminder that while WAT offers unmatched visibility into the raw mechanics of WebAssembly, modern compilers can get us remarkably close to that same efficiency with far less effort and far more developer sanity.
Initial Results: Default Compilation
Before diving into aggressive optimization and all the special compiler options, it’s important to understand how each toolchain behaves under default conditions - no memory flags, no stack limitations, and minimal linker tuning. These “out-of-the-box” builds represent what most developers would get from a standard compile using WASI or a default target profile. While they all run correctly, the resulting binaries vary significantly in both memory footprint and practical deployability on constrained devices. This baseline helps illustrate how much implicit overhead modern compilers introduce before we start trimming things down.
A key attribute is memory expectaionts - using wasm2wat we can extract these:
$ wasm2wat ball_demo.wasm | grep memory (memory (;0;) 1) (export "memory" (memory 0))
Let's now look at each toolchain and the resulting default memory maps for each environment.
WebAssembly Text (WAT)
Our hand-written WAT version defines (memory 1) - a single 64Kb page - more than enough for the demo logic and framebuffer data. It serves as the clean reference point: no hidden allocations, no runtime bookkeeping, and no unreferenced sections. This is the pure MVP baseline that all other builds are compared against.
C (wasi-sdk)
The WASI build mirrors WAT closely however defines (memory 2) - two 64Kb pages - 128Kb in total. This is already double that of the hand written WAT, but this is due to compiler defaults for stack size (which, looks to be around 64Kb) - something that can definitely be reduced.
C (Emscripten)
Emscripten’s defaults tell a completely different story (memory 256 256) - that is a 16Mb memory requirement upfront, fixed and non-growable. This stems from Emscripten’s default WASI-like environment, which reserves ample space for its internal structures, stack, and potential malloc heap even when unused. On desktop WASM runtimes this is negligible, but on embedded targets it’s a show-stopper. Without overriding the memory flags, Emscripten’s convenience layer trades compatibility for capacity, demonstrating why explicit constraints are essential when targeting constrained devices.
Rust
Rust’s default output defines (memory 17) - 17 distinct 64Kb pages - roughly 1.1Mb of linear memory. This allocation originates from Rust’s internal layout for .data and .bss sections, combined with a generous default stack and heap. While the binary itself remains compact, the memory reservation makes it less suitable for strict environments unless linker arguments explicitly cap it. Fortunately, Rust’s toolchain exposes these knobs directly, allowing developers to bring it down to parity with the WAT and C builds with minimal adjustments.
TinyGo
TinyGo by default defines (memory 2), or 128 Kb - seemingly modest, with also a large stack size - however its initialization sequence uses bulk-memory operations to zero and fill memory pages and enforces entropy at startup. The behavior is safe and consistent across platforms, but it introduces extra startup cost and reliance on post-MVP features. Without additional flags or wasm-opt passes, these binaries won’t execute on strict MVP runtimes, even though they’re functionally correct.
In default mode, each toolchain behaves according to its ecosystem’s assumptions - desktop-class memory, ample stack, and a generous heap. The results underscore why explicit memory tuning is not optional when deploying to real embedded hardware. The next step is to apply tight constraints and re-evaluate how far we can shrink these builds while maintaining full functionality and compliance with the WebAssembly MVP.
Memory Tweaking Each Toolchain
After observing how generous the default toolchain settings can be, the next step is to take control of memory allocation directly. The toolchains gives us the flexibility to define initial memory, maximum memory, and stack size at link time - parameters that have a dramatic effect on both binary size and runtime behavior. By tightening these values, we can align each build more closely with the constraints of embedded devices while still maintaining predictable execution.
- Stack size to 1Kb, to simulate the limitations of small MCUs.
- Initial memory to 64 KB (1 page) - enough for the demo’s data and stack.
- Maximum memory also fixed at 64 KB, disabling dynamic growth to ensure deterministic performance.
The complete set of optimizations are detailed below - in some cases, additional files are needed for the build process.
WebAssembly Text (WAT)
// Makefile wasm.wat: @mkdir -p $@ wat2wasm ball_demo.wat # ensure the binary is MVP compliant! wasm-opt $@/ball_demo.wasm -o $@/ball_demo.wasm --mvp-features
C (wasi-sdk)
// Makefile CLANG_SLIM=-Oz -ffreestanding -fno-builtin -nostdlib CLANG_WASM=-mcpu=mvp -Wl,--export=start -Wl,--entry=start CLANG_XMEM=-Wl,--stack-first -Wl,-z,stack-size=1024 \ -Wl,--initial-memory=65536 -Wl,--max-memory=65536 CLANG_FLAGS=$(CLANG_SLIM) $(CLANG_XMEM) $(CLANG_WASM) wasm.c: @mkdir -p $@ $(WASI_SDK)/bin/clang --target=wasm32-wasi $(CLANG_FLAGS) \ -o $@/ball_demo.wasm ball_demo.c # ensure the binary is MVP compliant! wasm-opt $@/ball_demo.wasm -o $@/ball_demo.wasm --mvp-features
C (Emscripten)
// Makefile EMCC_SLIM=-Oz -sSTRICT=1 -sERROR_ON_UNDEFINED_SYMBOLS=0 EMCC_XMEM=-sMALLOC=none -Wl,--stack-first -sSTACK_SIZE=1024 \ -sINITIAL_MEMORY=65536 -sMAXIMUM_MEMORY=65536 -sALLOW_MEMORY_GROWTH=0 EMCC_WASM=-sSTANDALONE_WASM=1 -mcpu=mvp -Wl,--export=start -Wl,--entry=start EMCC_FLAGS=$(EMCC_SLIM) $(EMCC_XMEM) $(EMCC_WASM) wasm.c.emscripten: @mkdir -p $@ emcc ball_demo.c $(EMCC_FLAGS) \ -o $@/ball_demo.wasm # ensure the binary is MVP compliant! wasm-opt $@/ball_demo.wasm -o $@/ball_demo.wasm --mvp-features
Rust
// Makefile RUST_SLIM=-C opt-level=z -C lto=fat -C codegen-units=1 -C panic=abort RUST_XMEM=-C link-arg=--stack-first -C link-arg=-z -C link-arg=stack-size=1024 \ -C link-arg=--initial-memory=65536 -C link-arg=--max-memory=65536 RUST_WASM=-C link-arg=--entry=start -C link-arg=--export=start \ -C target-feature=-bulk-memory,-nontrapping-fptoint RUST_FLAGS=$(RUST_SLIM) $(RUST_XMEM) $(RUST_WASM) wasm.rs: @mkdir -p $@ rm -rf rust cargo new --lib rust cp ball_demo.rs rust/src/lib.rs cp ball_demo.rs.cargo rust/Cargo.toml (cd rust; cargo rustc --release --lib \ --target wasm32-unknown-unknown -- $(RUST_FLAGS)) mv rust/target/wasm32-unknown-unknown/release/ball_demo.wasm $@/ball_demo.wasm rm -rf rust # ensure the binary is MVP compliant! wasm-opt $@/ball_demo.wasm -o $@/ball_demo.wasm --mvp-features // ball_cargo.rs.cargo [package] name = "ball_demo" version = "0.1.0" edition = "2021" [lib] crate-type = ["cdylib"] [dependencies] [profile.release] opt-level = "z" lto = "fat" codegen-units = 1 panic = "abort" strip = "symbols"
TinyGo
// Makefile TIGO_SLIM=-opt=z -no-debug -panic=trap -scheduler=none -gc=leaking TIGO_XMEM=-stack-size=1KB TIGO_FLAGS=$(TIGO_SLIM) $(TIGO_XMEM) wasm.go.tiny: @mkdir -p $@ mkdir -p golang.tmp cp ball_demo.go golang.tmp/ cp ball_demo.go.target golang.tmp/target.json docker run --rm --volume "$(PWD)/golang.tmp:/work:rw" \ -w /work tinygo/tinygo:0.39.0 \ tinygo build -target=/work/target.json $(TIGO_FLAGS) \ -o ball_demo.wasm ball_demo.go mv golang.tmp/*.wasm $@ rm -rf golang.tmp # ensure the binary is MVP compliant! wasm-opt $@/ball_demo.wasm -o $@/ball_demo.wasm \ --llvm-memory-copy-fill-lowering \ --llvm-nontrapping-fptoint-lowering \ --mvp-features // ball_cargo.go.target { "inherits": ["wasm"], "llvm-target": "wasm32-unknown-unknown", "libc": "wasmbuiltins", "linker": "wasm-ld", "goos": "js", "goarch": "wasm", "wasm-abi": "generic", "cpu": "mvp", "ldflags": [ "-z", "stack-size=1024", "--initial-memory=65536", "--max-memory=65536", "--export-memory" ] }
These parameters let us measure how well each compiler and runtime tolerates a highly constrained environment. Some toolchains, like wasi-sdk, adapt cleanly to these limits; others, such as Emscripten or TinyGo, require more aggressive flags or post-processing to fully comply. The goal isn’t just to make the binaries smaller - it is to ensure that every generated module can run predictably on devices where memory expansion isn’t an option.
Results: Constrained Memory Builds
With the new limits in place - 1Kb stack, 64Kb initial memory, and no memory growth - each demo was rebuilt using its corresponding toolchain optimization and linking flags. The goal was to produce MVP-compliant, predictable binaries suitable for microcontrollers, and to measure the resulting binary size growth compared to the baseline WAT build.
|
||
Language / Toolchain | WASM Binary Size | Relative To WAT |
---|---|---|
|
||
WebAssembly Text (WAT) | 739 bytes | 100.0% |
C (wasi-sdk) | 932 bytes | 126.1% |
C (Emscripten) | 1,100 bytes | 148.8% |
Rust | 1,192 bytes | 161.3% |
TinyGo | 3,492 bytes | 472.5% |
|
Even under aggressive memory constraints, both C and Rust perform exceptionally well, producing binaries that remain close in size and structure to the original WAT baseline. With the right compiler and linker options, these toolchains respect strict memory limits, maintain full MVP compliance, and introduce virtually no unnecessary overhead. This makes them ideal candidates for developers who want the convenience of high-level languages without sacrificing control or predictability in resource-constrained environments.
C (Emscripten), while slightly heavier, still demonstrates that its additional runtime helpers and function table exports come at a very reasonable cost. The convenience it provides through its ecosystem and tooling might justify the minor increase in binary size, especially when targeting environments with a bit more headroom, such as the ESP32 or similar devices.
TinyGo stands apart, delivering a functional and portable runtime at the cost of an additional memory footprint. Its inclusion of heap management and entropy imports makes it less suitable for the tightest devices, but it still performs impressively for what is effectively a high-level language runtime running on the WebAssembly MVP. For projects where Go’s language model and tooling are valuable, TinyGo remains a practical choice - especially once bulk-memory features are lowered for compatibility.
Across all toolchains, the constrained builds validate the broader goal of RIoT Secure's BRAWL’s zero-bloat approach: it’s entirely feasible to develop portable, efficient, and deterministic WASM modules in modern languages, without drifting far from the minimalism of hand-written WAT.
Demo on the ESP32 with 16x16 LED Matrix
To take the concept beyond theory, we deployed each of the WASM binaries on an ESP32 paired with a 16x16 LED matrix.
ball_demo.go compiled to WASM using TinyGo flashed to an ESP32 with 16x16 LED Matrix
This setup highlights the practical side of our work - proving that the same WASM modules can run across vastly different hardware profiles without modification. On the ESP32, the increased memory and processing headroom allow for smoother animation and richer visual output, but the underlying logic remains identical to the constrained Arduino Uno R4 WiFi. It’s a tangible demonstration that portability and predictability aren’t just design goals - they’re deliverables. At RIoT Secure, we don’t stop at benchmarks or specs; we validate everything on real hardware, ensuring our runtime performs consistently from the smallest microcontroller to more capable edge devices.
RIoT Secure Perspective
From our point of view at RIoT Secure, this exercise goes far beyond benchmarking compiler outputs - it’s a validation of our own WASM runtime and its ability to execute real-world modules under genuine IoT constraints. Every demo in this series, except the TinyGo build, runs natively on the Arduino UNO R4 WiFi, where our runtime defines a very constrained memory page size of just 4Kb. That’s an order of magnitude smaller than standard WASM expectations, yet the modules compiled from C, Rust, and even Emscripten execute reliably within those limits.
This result reinforces a key principle of our design philosophy: IoT doesn’t need bigger specifications - it needs smarter implementations. By focusing on predictable execution, fixed memory boundaries, and the elimination of growth dependencies, our runtime achieves what general-purpose WASM engines cannot - deterministic behavior on hardware with strict limits. The ESP32 variant of the demo highlights this portability: the exact same module scales effortlessly to a larger LED matrix with no code changes, proving that efficiency and flexibility can coexist.
TinyGo’s incompatibility on the Arduino Uno R4 WiFi isn’t a failure - it is a useful illustration of the boundary lines. When runtimes assume 64 KB pages and bulk memory zeroing, they quickly outgrow the most constrained hardware. Our approach avoids that by treating memory as a scarce, managed resource, not an abundant one. This philosophy extends beyond demos: it’s the foundation for how RIoT Secure builds its platform - delivering secure, deterministic, and lifecycle-managed execution for connected devices, regardless of how limited their resources may be.
This is the third of our multi-part series on WASM and WASI. In the next post - "WASI: Abstracting the Previews — A Stable Foundation for BRAWL" we shall start working on an abstraction layer across the various WASI preview - defining a micro-controller friendly interface that can remain stable, even as the official WASI specifications continue to evolve.
In parallel, now that we have proven the concept and validated our runtime across real hardware, it’s time to bring these results into production. The next phase is integrating WASM module delivery directly into RIoT Secure’s lifecycle management platform, allowing us to handle WASM binaries with the same reliability and security as our existing hardware sandbox firmware updates. This integration will complete the loop - moving from standalone proof-of-concept demos to a unified system capable of managing and updating both firmware and WebAssembly modules seamlessly across our supported microcontroller environments.