sunfishcode's blog
A blog by sunfishcode


Rust programs written entirely in Rust

Posted on

mustang is a system for writing Rust programs entirely in Rust, meaning they don't use libc, crt1.o, or any C code. It's experimental, but it's complete enough to run a std-using Hello World and other simple programs on Linux on x86-64, x86, and aarch64.

See mustang's README for information on how to use it. This post walks through the major steps leading to this point.

System calls

The first building block for mustang was rsix, a crate providing a syscall-like API, but which can be configured to use different backends, currently either libc or raw Linux syscalls. rsix has been a place to help prototype the io-lifetimes crate and I/O safety feature, support Wasmtime's WASI implementation, support cap-std's filesystem sandbox implementation, and to push the boundaries of low-level performance while maintaining safe and relatively idiomatic Rust APIs.

These continue to be the main motivators for rsix, and that's an important point: while mustang isn't particularly practical, it isn't all a one-off effort. Some of the major pieces have real-world practical use cases.

The first part of the inspiration for mustang was the realization that rsix could be used in this way.

Process startup

crt1.o and process startup has always had an aura of mystery about it, at least to me. There are parts implemented in assembly, parts that use obscure C compiler features, parts that are heavily conditionalized with ifdefs, and support for dynamic linking requires several particularly tricky features.

The other part of the inspiration for mustang was the realization that simple Rust programs don't need most of what goes into crt1.o and libc startup code The minimum needed is just a little bit of assembly to translate from the initial process state that the OS sets up into the Rust calling convention. Once Rust code is running, it just needs to compute argc, argv, and envp, call any .init_array functions, and then call main.

This code is in mustang's origin crate; it's very small.

To tell the linker to avoid linking in crt1.o so that it uses origin's startup code instead, mustang defines custom targets named *-mustang. These primarily add -nostdlib to the link command so that crt1.o is not linked in.

Cargo's build-std mode allows mustang to easily compile the standard library with its custom targets, so it doesn't need to make any upstream standard library changes (for now...).

C-scape

If we can start a process with Rust code, and make system calls with Rust code, what's left that's not Rust?

As a practical matter, Rust's standard library depends on libc and uses many libc APIs. One option would be to build a runtime library that runs on no_std. However, in the limit, that would essentially require a reimplementation of std, which would be a lot of work, and a huge burden to maintain.

So mustang instead defines C-compatible ABIs providing functions like write, strlen, memcpy, mmap, and other things used by Rust's standard library, using rsix internally to do actual system calls. These implementations are currently very minimal, but they are enough to support Hello World, running with std.

And going forward, it should be relatively straightforward to add additional libc APIs as needed by std.

That said, the goal here is to just implement enough of libc to get std working on top of it. Mustang doesn't need to implement a whole new libc, which would be a lot more work, and much of it wouldn't be needed in Rust programs.

This code is in mustang's c-scape crate.

The global_allocator

Rust allows users to configure their global allocator with the global_allocator attribute. The default global allocator ultimately uses libc malloc. Writing a new malloc is a lot more work that I was looking to do here, so the question here was, is there a nice Rust allocator crate that can be used with #[global_allocator]?

It turns out, there are a few options, but one which turned out to be particularly easy to integrate was wee_alloc. Built for reducing code size on WebAssembly, wee_alloc doesn't do fancy optimizations, but it does plug directly into #[global_alloctor] with no additional hassle, and it is portable, so it was very easy to set up. And it only uses a few things from libc which c-scape is easily able to support.

Putting it all together

The mustang crate pulls all these pieces together to make it easy to use them all at once. But note that since mustang is aiming at supporting regular std-using Rust code, it isn't something you explicitly call yourself. As such, it currently needs an extern crate mustang to link it into the program.

A note about safety (and a lack thereof)

There is often a temptation whenever one is working with C code to Rewrite It In Rust, however it's not always a good idea. In mustang's case, existing libc implementations are mature, robust, and well optimized. Libc interfaces are relatively portable, and on some operating systems, libc interfaces are the only stable interfaces to operating system functionality. There's no urgency to stop using libc or to rewrite it.

mustang is inspired by the realization that some of the major pieces were already available, and the remaining pieces to get basic examples working could be written with a relatively quick effort. It's new code, it's experimental, it has a lot of unsafe code, it lacks important optimizations, it's overall far less safe to use than just using libc, and it would take a lot of work to change this. But, it's fun, and educational.

It's also possible that in the future, mustang could be part of a path to designing new low-level system features like command-line arguments, environment variables, or OS error codes are handled with more safety or other ideas such as first-class I/O. This is also part of why the goal isn't to just build a Rust implementation of libc: many libc APIs consist of a small amount of code behind a function signature that uses raw pointers; just putting Rust behind those kinds of APIs isn't going to make them significantly safer. But that's another post; for now...

Hello, World!

See mustang's README for step-by-step instructions for how to build an run mustang.

If you're interested in following the story, helping out, or even just asking questions, come say hi in the chat channel, or file an issue!