Rust for billions of MCUs

I am posting on this board because it's more about the roadmap of the Rust language.
I have been working on an IoT platform that brings Apps to the IoT world. Apps are designed to be "write once, run everywhere." There is no need to address the benefit of running Apps that interact with the world locally on devices.
The Typescript API and implementation have already been finalized. But it is hard to make decisions on the Rust implementation.
The Microcontrollers are very resource-limited (a few hundred KB of RAM/ROM). The design needs to aim for maximum code reuse. But Rust ABI has never been stable. Forcing App developers to publish their source code is an option, but I'm afraid it will negatively affect adoption, which will ultimately hurt users.
I would appreciate thoughts and insights from you guys.

The Thing-App design:

Thanks a lot for letting me in. This is Qingjun Wei, creator of Libertas IoT. Rust is the perfect candidate for building Apps for the Internet of Things running locally on billions of devices.

The devices are most likely powered by a tiny MCU with limited resources. I am facing some difficult design decisions. The ideal scenario is letting App developers publish binary libraries. The central controller performs the linking and remotely deploys the executable to the MCU.

Since the ABI is not finalized yet, it appears now the only viable solution is to force developers to publish source code.

It doesn’t matter if an end-user only deploys one App to the device. However, in reality, we need to support multiple deployments with Apps for different versions of SDK. It will be nice to share the common library code.

Of course, making each App an independent binary blob is another option. It will be the easiest though not optimal.

In very general terms, one way to achieve binary compatibility is to create a Rust/C compatibility layer (I.e. foreign function interface API to C, which has a stable ABI). So, all Rust libraries that you want to share as binaries need to implement one. They will be called from other C code (or Rust code, with a compatible API).

For further details see Chapter 10 of the embedded Rust book.

That’s our current plan. Our system will craft a linker descriptor to expose API entry points for each compiled firmware. The linker descriptor will be published with each firmware for proper linkage. Still, we need to make sure interoperability among different versions of SDK. Our design will be very affected by Rust’s ABI roadmap.

Huh, how are you running typescript on such constrained devices? That doesn't seem to make sense?

Apart from a hand made C API there are also two crates that help with a higher level stable Rust-Rust API/ABI:

I believe stabby seems to be more maintained out of those, but the idea is the same. And only stabby is no-std anyway.

Are you using any MPU as a security barrier between the OS and the "apps"? You might want to look at for example https://hubris.oxide.computer/ (though that doesn't support loading programs, all tasks must be compiled in statically). It seems protection domains like that would be necessary for your project to offer any safety against buggy or malicious third party code. I suspect memory isolation will be key if you don't want a security nightmare on your hands if this thing gets popular.

Actually, it’s quite doable. Typescript is transpiled into Lua. I built a Lua VM into firmware on MCU. It takes a few dozen KB of flash. 128KB of RAM can run quite a sizeable code.

It appears we don’t need Stabby. My IDE can automatically generate a C API “stub“ for Thing-App functions. The C API will have a fixed signature.

I know Rust’s memory safety is not perfect, and can be easily exploited. But I will rely on it for now. At least new ARM cortex M has hardware stack protection.

I don't really understand what did you want to inquire/tell with your post? Stable abi imo is more or less irrelevant for mcus. We are just… Updating the whole firmware.

“Write once, run everywhere” - yeah, we just have no_stdand “no_alloc” crates that rely on embedded_io_async etc and it pretty much “write once, run everywhere” - we are just writing different “main” for our crates with application logic, with Rusts type system and lifetimes it becomes like a lego.

I wondered about that as well. Thing is nobody runs typescript anywhere, it gets transpiled to Javascript for delivery. Then there are very small Javascript run times for microcontrollers, like the Espruino https://www.espruino.com/ which I have played with. It works very well if you are not needing much in terms of performance or code size. Quick and simple to create all kind of functionality. The brilliant thing about Espruino is that you can play with JS in a REPL as it runs on your MCU. Also the event driven model of JS makes it dead easy to juggle all kind of MCU peripherals simultaneously without having to worry about threads and such. Magic.

That is possibly the weirdest thing I have heard all year :slight_smile:

Why do smartphones need Apps? Why don’t smartphones recompile firmware every time users change Apps?

You are welcome to give my stuff a try. I chose Lua VM because it’s easier to run multiple Apps as isolated virtual processes. My implementation also works very well.

Sounds very interesting. Though next time I have an MCU thing to do it will be written in Rust if at all possible. Hope you cook up a nice Rust solution.

Espruino is indeed pretty cool. I will look into how much effort it takes to port. The cool thing is that the source code of Thing-App shall not change. My Typescript IDE will perform necessary transpiling.

Smartphones have gigabytes of RAM and multicore CPU. They typically runs on desktop-grade 64bit OS.

Rust embedded is quite immature so the only way to upload some code to an MCU is to overwrite the whole firmware. Other more mature projects (Zephyr for example) allow kernel vs userland with memory protection and deferred loading of the application code even on smaller MCUs. Rust just isn't there yet.

True.

Us old embedded systems builders think of micro-controllers and firmware. “Firmware” is called such because although it is written like software it is hard to change, maybe blown permanently into ROM or less permanently into EPROM or now a days FLASH. The firmware would include the application code and the OS if there is one all welded together into a single binary blob. Updating was difficult.

Now a days MCU’s can have enough RAM and FLASH, even if tiny compared to a phone) to run a tiny boatload/OS that is firmware but then also download and run application code and even have tiny VMs to run interpreted code like JS or Python (See Espruino and MircoPython). That is even MCU’s with no process memory isolation and such. So basically its MCU + OS + App(s).

I thought this kind of thing was crazy when I first saw it years ago, but it turns out to be great if you want to quickly grab a tiny MCU board and make it do some simple task with an “app” you have written in minutes.

I am not thinking about “simple“ tasks. I am thinking about replacing the “entire firmware“, well, sort of.

Let’s leave OS to Zephyr or FreeRTOS, and leave the protocol stack to something like Matter. Exposing local devices and sensors is quite standard, just like peripherals on PC or phones.

The rest can be Apps, which is the only thing that needs to be customized.

Let’s use the “Smart Sprinkler Controller“ as an example. The App is a function that controls the on/off of multiple water valves that water the lawn.

Instead of paying $200 for a sprinkler controller with fixed functions, end-users will have a choice to buy a blank box with a bunch of switches at $20. Then shop for App software from multiple App providers.

The App developer only needs to write a function with arbitrary arguments. My IDE will parse the source code. We can automatically generate UI for end-users to use the App. So never ever worry about UI.

I would expect a few dozen KB of flash/ROM overhead with the architecture, virtually no RAM overhead.

Performance-wise, even hard real-time guarantee comparable to C can be offered. So really, the downside is minimal, mainly the flash/ROM overhead.

This crops up frequently in various forums. Honestly, an unstable ABI is not the world-ending problem people make it out to be.

Look at the Commodore-Amiga, back in its hey-day. Lattice C and Manx C were two very popular C compilers for that platform. However, shockingly, they did not share a common ABI. In fact, until AmigaOS 4.0 came out of Hyperion, AmigaOS simply didn’t HAVE any concept of a “standard C ABI.” It. Just. Didn’t. Exist.

What AmigaOS did do is, by way of examples with the system libraries, define a completely unique, language independent ABI, where function parameters are loaded into registers prior to a library call. Speaking of libraries, these libraries were shared, but still statically linked. A concept utterly foreign on Windows and POSIX-based systems today.

Basically each language (interpreted or compiled) used a set of “thunks”, sometimes built dynamically on-demand (e.g., AmigaBASIC) and sometimes provided via linker libraries (e.g., C or Modula-2) to talk to each other using this standard ABI. It’s not hard. It’s not even inconvenient, if you make use of code generators. It worked flawlessly. And still does to this day.

Ultimately, each language (if not individual compilers for said language) ends up having its own ABI anyway. Might as well embrace it; the alternative is that every language gets shoehorned into a common interface which nobody likes (vis-a-vis the IBM System/360 ABI where subroutines call other subroutines by manually and statically allocating register frames that are then manually linked into a singly-linked list to emulate stack semantics).

Just my (rather unpopular despite existence proofs) opinion. :slight_smile:

Thank you for your insight. I agree with you. Rust is innovation. A new ABI for Rust is understandable.

Based on my requirement, I would envision a Rust Thing-App is published as a static library with binary code only. My platform will perform the final “linking“ when an end-user deploys the App code. The final binary must be properly linked to the desired address space on the flash memory (and RAM) of the MCU.

So I guess if I don’t expect LTO (link time optimization) to minimize binary size, it should be easy. As for shared library code, if the end-user only deploys one Thing-App, it doesn’t matter. It only matters if the end-user run multiple Thing-Apps on the MCU.