Cross-compiling Rust code to Minix
TLDR: Scroll down for a pretty screenshot and a GitHub link.
I’ve decided to take a look at Minix, which is an interesting microkernel OS.
Naturally after building Minix from git, the first thing I decided to try was porting Rust’s
std to Minix so I could cross-compile Rust programs from Linux to run under Minix.
Okay, I suppose I could have started with something else, but porting Rust software and modifying the platform-depending part of
std is something I have experience with from working on Redox OS. And Rust really isn’t that hard to port.
In theory, this should be almost trivial. Minix provides a Unix-like libc, in particular a fork of NetBSD’s libc. And
std has a generic back-end for Unix systems that makes calls to the system’s libc to implement the platform specific functionality. So all we need to do is compile it against Minix’s libraries and everything should work.
Of course, it’s not quite that simple…
The libc crate
Now, most languages (including Rust) don’t read C headers. We need the
libc crate to provide bindings so we can use the C library in Rust. This needs to be ported for each new OS/libc, but it’s not too bad, since a lot of things are fairly standard and can be shared.
Minix 3 uses a fork of netbsd’s libc, so I placed the code for Minix at
libc/src/unix/bsd/netbsdlike/minix, which seemed like a good place to put it. Then I needed to add or modifiy
#[cfg] lines in various places for Minix.
We could run into a lot of very subtle issues if the bindings don’t actually match libc. Luckily, the libc crate has tests based on the ctest crate, which compares the bindings against the C headers. It likely won’t catch every issue, but it’s quite helpful.
I didn’t save the first set of errors I got (which was longer), but here’s the output at one stage:
minix# ./main RUNNING ALL TESTS bad siginfo_t size: rust: 120 (0x78) != c 128 (0x80) bad if_msghdr size: rust: 148 (0x94) != c 152 (0x98) bad if_msghdr align: rust: 4 (0x4) != c 8 (0x8) bad NET_RT_MAXID value at byte 0: rust: 5 (0x5) != c 6 (0x6) bad SCM_CREDS value at byte 0: rust: 16 (0x10) != c 4 (0x4) bad FD_SETSIZE value at byte 0: rust: 0 (0x0) != c 255 (0xff) bad FD_SETSIZE value at byte 1: rust: 1 (0x1) != c 0 (0x0) bad FIONREAD value at byte 0: rust: 127 (0x7f) != c 1 (0x1) bad PIPE_BUF value at byte 1: rust: 2 (0x2) != c 128 (0x80) thread 'main' panicked at 'some tests failed', /home/ian/Devel/Code/rust-minix/libc/target/i586-unknown-minix/debug/build/libc-test-433a43a7e12a3191/out/main.rs:11:21 minix#
And then when I fixed those issues:
minix# ./main RUNNING ALL TESTS PASSED 6323 tests minix#
Great! Now I’m sure there are still some subtler issues, but that’s 6323 things verified as correct, and much more easily than if I had to do that manually.
A compiler target, and the standard library
Adding a new target to
rustc can be done by modifying the crate
librustc_target and recompiling
rustc. But compiling compilers can take rather a long time. Luckily, Rust also allows
.json target specification files, so I did that.
The standard library doesn’t need very much modification for Minix; add a
#[cfg] to tell it to use the Unix backend. Most of the work here is then redirected to
libc, and should be portable to most Unix-like systems. Then various other
#[cfg] lines, and a couple temporary hacks.
The main problem: threads
It turns out various common Unix functions are not available on Minix. The most notable limitation is that Minix does not have a complete pthreads implementation.
Minix includes a library called
mthread which includes a basic implementation of user threads, with optional pthread emulation. But it does not support everything in pthreads. Initially I based my port on this, and even sent a small patch fixing a small issue with
const specifiers on arguments). But Rust’s
std uses some pthreads functions that it did not provide. I got a “hello world” working this way, with various hacks in
It’s not part of the base system, but Minix has a package for GNU pth, which is a much more complete user threads library, including support for more pthreads functions. Switching to pth allowed reducing the hacks, and fixed some errors I was getting. So it’s a clear improvement, other than adding a dependency.
pth library is different from the threading provided by most modern operating systems, since it provides non-preemptive user threads, rather than preemptive kernel threads. Most obviously, this could potentially result in a performance hit. What I’m not sure about, however, is what the lack of preemption might break. It’s easy enough to contrive an example of multi-threaded code that will break without preemption, but I don’t know if that is (much of) an issue in practice.
I haven’t tested very much, but I got ripgrep to compile, and it seems to work perfectly:
Now that this much is working, any Rust software is a potential subject for porting attempts, including
cargo. But some will be harder than others, and with these sorts of things, it can be hard to estimate if it will be trivial or quite difficult.
There are one or two remaining issues that I’m aware of, and probably many I haven’t found. In particular, I do wonder what impact having a non-preemptive user threads implementation will have on typical Rust software.
Upstreaming Minix support into Rust is likely possible. But first I would want cleaner patches with fewer hacks and fewer overall changes, as well a some more testing overall.
I suppose writing a driver, filesystem, or other system component for Minix using Rust could be an interesting exercise. I might try this at some point.
I’ve created a GitHub repository with my
libc forks, and a build system that should make it relatively easy to use. I spent way too long messing with Makefiles and scripts to set that up. Using Rust with a fork of the standard library isn’t very convenient currently, but there are recent plans to provide integrated support in cargo for building