It's ironic - Rust in Liminix#
Sat May 16 19:40:05 2026
tl;dr I thought there was was going to be some Rust in Liminix, but it turns out there isn't. I'm not opposed to adding it, but the application I thought I had for it turns out to be better done in C
I'd better eludicate. I decided, way back in whenever-it-was, that
Fennel would be the go-to "scripting" language for anything in Liminix
too complicated to want to write it in ash-compatible sh - and as my
criterion for "too complicated" is more than about ten lines long,
that's quite a lot of Fennel. The Fennel is translated to Lua at build
time and interpreted by the lua command at runtime. So we start many
Lua VMs and in most of them we load anoia.lua
and lualinux.so which together provide functionality that pretty
much every such script requires.
I thought it would be a useful optimisation to have a server that spawns prewarmed Lua VMs on demand. I should say upfront that this is premature optimisation in its purest form as I haven't even measured how long these things take to load.
So what it does is start a lua interpreter and listen on a socket. On each incoming conection it reads the name of a lua script and accepts file descriptors for stdin/stdout/stderr as ancillary data. It forks, and run the script in the child so its state does not change the state of the parent. I'm pretty sure I've seen this pattern a bunch of times (something KDE did back in the day, Ruby snailgun gem) but I can't now find a good reference describing it or naming it, so if you haven't seen it before maybe I dreamt it. It's not "preforking" because there's no pool of children - they're forked on connect.
And the reason I wrote it without first finding out of it'd be useful is it seemed like an excuse to try using Rust again.
What I found:
-
embedding a Lua interpreter into Rust is made entirely painless by the
mluacrate -
getting ancillary data on a unix socket requires using the
recv_vectored_with_ancillary_frommethod, which needs Rust nightly. This is kind of understandable as the necessary kernel support has only been present since 1996 -
when it comes to systems programming, there are many ways to do the same thing and it doesn't seem like much consensus. Web searching for "rust fork" led me to the
forkcrate, so I added that. But it doesn't include anywait()orwaitpid()interface so I added that from thenixcrate, and then I also wanted to callpoll()so I did that with thelibccrate. With hindsight I should have usedlibcfor everything - the only value-add of the rust crates (for this application) seems to be that it wraps C's magic 0, n, -1 sentinel values in fancy enums so you can write match clauses likeOk(Fork::Parent(pid))instead ofpid > 0 -
I found a way to do signal handling (I needed to catch SIGCHLD), but only by blindly copying code I didn't understand from the internet. (N.B. this is acceptable in in 2026, it's the same process as LLM-assisted programming except without boiling the ocean). There's
extern "C"andunsafeandlibc::signal(libc::SIGCHLD, handle_sigchld as *const () as libc::sighandler_t)which frankly doesn't (and shouldn't) inspire confidence.
The surprisingly simple bit was incorporating it into Liminix. I added
my package to overlay.nix and ran nix-build and voila, a MIPS
binary. I was expecting to have to swear a bit more than that.
However, I'm not going to do it, because the binary is huge. After
stripping it, replacing the fork and nix crate dependencies with
direct libc calls, and then following this handy guide to reducing
binary size I've got it
from somewhere north of 800k to around 415k, but that's still a significant chunk
of a 16MB flash filesystem. When I wrote the equivalent program in C
for comparison it's about 20k. Yes, both are linked dynamically
against libc and liblua.
So, the outcome of the experiment is I have no current plans to add Rust to Liminix. But if it ever turns out that we do need to, at least we know it's possible.
Maybe I should try Zig.