Actually Portable Executables with Rust and Cosmopolitan Libc

aka “Rust is Actually Portable”, after Lua and Python

I just built a Rust executable that runs on six operating systems (Linux, Windows, MacOS, FreeBSD, NetBSD, OpenBSD). If you’d like to build it yourself, clone this repo and follow the README – you’ll need a recent gcc, ld.bfd, objcopy, bash, and the latest (nightly) versions of cargo, rustc, and friends.

I’ve been recently getting into Rust, and it seems pretty cool! I’ve also gotten a bunch of software to run on Cosmopolitan Libc over the last year1, so in June I thought combining Rust with Cosmopolitan Libc would be interesting. Here’s how I got to a hello world! Actually Portable Executable with Rust.

A minimal executable

A good thing about Rust was, unlike Python or Lua, no messing around with C headers. Just find a way to tell cargo to link with cosmopolitan.a at the very end, and you get an APE. I looked up the Rust Embedonomicon and built a no_std example, but it wasn’t that useful – the executable just crashed and I didn’t know if it was on purpose. But the Embedonomicon also described how I could create a custom target for Rust, based on the available targets. Jackpot!

I created a custom target called cosmo.json, based on the existing x86_64-linux-unknown-gnu and x86_64-linux-unknown-musl. It took some trial and error (I set up my own panic handler, but then it clashed with panic_abort, but then it didn’t when I changed the flags to cargo), but eventually I got this simple example below to compile (I took it from some online Rust discussion about the libc crate, I think the code was written by steveklabnik).

#![no_main]
#![no_std]
#![feature(rustc_private)]
extern crate libc;

#[no_mangle]
pub extern "C" fn main(_argc: isize, _argv: *const *const u8) -> isize {
    const HELLO: &'static str = "Hello, world! %d + %d = %d\n\0";
    let x: i32 = 1;
    let y: i32 = 2;
    unsafe {
        libc::printf(HELLO.as_ptr() as *const _, x, y, x+y);
    }
    0
}

#[panic_handler]
fn my_panic(_info: &core::panic::PanicInfo) -> ! {
    loop {}
}

Once I got the libc crate to work, I could do a lot of things. I took a bunch of simple C programs, rewrote them in unsafe Rust with the libc crate, and generally checked if the build process was okay.

Building the std crate

Now I had some simple Rust APEs, but I felt that saying “Rust is Actually Portable” and then showing a bunch of C-like programs with unsafe wouldn’t cut it, so I went on another round of debugging to get the std crate to build.

The std crate pulls in a lot of different crates! There’s core, libc, and alloc, which I used for the above example, panic_abort, panic_unwind, backtrace, and proc_macro, and subcomponents of std itself. I got a tour of the code in the std crate by writing an incomplete target cosmo.json: I’d change a configuration flag, some part of std would break because my flag was wrong, and I’d learn something new about Rust and how std worked.

Once I figured out the right flags in the target cosmo.json, most of the bugs with std went away. The few that remained deserve special mention because it took me a while to fix them.

Weird bugs and their workarounds

One of the weird bugs was internet-related code of std which called the libc crate (like struct hostent or something). I went through Cosmopolitan Libc source code, and everything seemed fine, so the mistake was elsewhere. I modified the std crate trying to figure out what was going on. After a bunch of different compiler errors and searching on the internet, I found the issue was with the filename of my target JSON. So some part of the code or build process uses the filename of the JSON, which was cosmo.json at that time, but it specifically needs a target name like x86_64-linux-unknown-cosmo, otherwise some ifdef cfg_if falls through and breaks a bunch of things. Why does the name of the JSON matter? I don’t know, but I’m happy that this bug doesn’t bother me anymore.

The next “bug” is more a comment about cargo, and is probably because I’m still new to Rust. cargo is a wonderful package manager, and building projects is pretty smooth. But once it comes to building the std crate, some of the convenience disappears, and I’d like a bit more flexibility in specifying what I want cargo to do. I’m building a static executable, and I’d like to say, “okay cargo forget about linking -lunwind or -lm or whatever, just listen to me, cosmopolitan.a has everything you need for this”, but I couldn’t find any combination of flags to communicate this. Eventually I gave up and wrote a bash script which just filtered out all the linker arguments I didn’t want before calling gcc.

The last set of bugs were at the link stage – I had every crate compiling without error, but when linking the executable, I found that I was missing a few symbols. Some were easy to add, like stat64 and __xpg_strerror_r, but the std crate required the pthreads key API (pthread_key_create etc.) to be implemented, which was weird because I thought I had specified single-threaded in cosmo.json. Another dependency of the std crate was backtrace, which requires libunwind in the default linux builds even though there is a noop crate that can be used for std. I couldn’t figure out the right flags to avoid these linker errors, so I wrote some dumb stubs to just get by the linker. A couple of days later, Justine Tunney implemented the pthreads key API in Cosmopolitan Libc, and I asked Justine to add the libunwind stubs as well so backtrace wouldn’t complain. I think there should be a cargo flag or something to choose using the noop crate in backtrace when compiling std.

Anyway, once I got through these linker errors, I downloaded the latest cosmopolitan.zip amalgamation from here, and now I could build some Actually Portable Executables with Rust.

Hello World! and a few examples

Rust is Actually Portable: here’s the hello world program that uses Rust and Cosmopolitan Libc:

fn main() {
    println!("Hello World! This is an APE built with Rust.");
}

That’s it. No unsafe, no changes to the Rust std source code. You just need to provide the right flags to cargo, and done. I also picked the first few examples from Rust By Example to build and try, and they all worked as expected. I’ve not tried a lot of things, so there’s room for experimentation and submitting PRs with Rust code (like with backtrace) and to Cosmopolitan Libc (implementing the libunwind stubs or filling out the pthreads API).

Closing Notes

I feel the package manager and documentation of Rust are big reasons why it is so popular. Being new to Rust, I was able to learn about safe vs unsafe, the libc crate, cargo, the std crate, backtrace, panic, rustc, and what Rust-generated assembly looked like, all over maybe a couple of weekend afternoons. Cosmopolitan Libc provided a unique angle to tour Rust, and it was a lot of fun to discover all of this.

Last March, Lua was the first language to be ported to Cosmopolitan Libc. At that time, parts of the libc API were still missing; when porting Python I submitted PRs for getnameinfo, struct servent etc. so Python’s socket library would work. But since that time a lot of work has been put into Cosmopolitan Libc, so that software built on it can be fun to develop and fast. The almost-complete libc API has made it so much easier to port software – Rust just needed a JSON file and some flags to cargo! I’m pretty sure a lot of software can be built with Cosmopolitan Libc, it’s just a matter of convincing the build system. If someone figures out a way to even partially automate this (say like FreeBSD’s ports), that would be amazing.


  1. As of this writing (2022-07-27), I’ve ported the following software to Cosmopolitan Libc: Lua and SQLite, both of which are now vendored in the Cosmopolitan monorepo and part of redbean, Python3.6, and GNU Make, both of which are part of the Cosmopolitan monorepo, Janet, Python2.7, PHP7.3, tcl8.6, LuaJIT, QuickJS, OpenSSL1.1.1k, zip/unzip, gzip, bzip2, the BLIS acceleration library because I’m trying to get numpy, and a bunch of CPython extensions like greenlet and markupsafe. I’m sure I’ve forgotten a couple, and I’m not noting all the libraries that I haven’t successfully built yet. ↩︎