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 mlua crate

  • getting ancillary data on a unix socket requires using the recv_vectored_with_ancillary_from method, 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 fork crate, so I added that. But it doesn't include any wait() or waitpid() interface so I added that from the nix crate, and then I also wanted to call poll() so I did that with the libc crate. With hindsight I should have used libc for 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 like Ok(Fork::Parent(pid)) instead of pid > 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" and unsafe and libc::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.