What would be required for Rust to produce cross-platform static binaries (more) easily?

I've been using Rust for many years now, since 2015, and I've had a lot of experience with trying to produce static binaries, typically using x86_64-unknown-linux-musl. I've been burned many times by it and lost many hours and days trying to figure out what wasn't working. While I prefer Rust for many, many reasons, the ability for Go to produce static binaries even cross-platform is incredible.

I know that the reason Go can do this is because it essentially bundles its own Assembly shims for syscalls and avoids libc altogether unless using CGo. Rust depends on libc (at least for std, maybe not for core) and uses it to speak to the OS and the world. I know that these things aren't comparable in and of themselves, and Rust supports C FFI as a core feature of the language, making Go's approach impossible for Rust.

I'm also aware of many different hacks for various components of building Rust binaries and libraries such as cargo-zigbuild which uses the zig linker for cross-compiling. I'm also aware that relibc, a pure-Rust implementation of libc, exists though I haven't used it.

What are the major challenges that stand in the gap right now between Rust and a mythical future where building static binaries and cross-compiling becomes much easier and less painful? More specifically, I'm not asking per se to fully reimagine Rust ala Go, but I want to understand what could be done to improve things so that I can better understand the compilation/linking process. I'm sure the compiler team probably already has a roadmap for these features, so I'd like to know more about this.

3 Likes

One thing that makes cross-building harder than it needs to be, is that Cargo does not have any logic for detecting a correct working linker. In this regard it's completely naive and helpless — just runs cc. It requires manual configuration of linkers for every single target on every OS.

It could have been much better if Cargo shipped with a linker detection logic for common platforms, similar to the compiler detection logic that the cc crate has (the cc crate shows it's a messy problem, but it's wonderful that users don't have to manually figure out that mess themselves for every quirky platform and compiler).

However, AFAIK instead of adding detection for the correct OS/cross-linker to use, the plan was to just use a bundled LLVM lld linker, which should be able to handle various targets itself. That switchover is getting delayed for years and years, since lld doesn't seem to be quite production-ready outside of Linux.

2 Likes

Cargo does not have any logic for detecting a correct working linker

Is this something that Cargo could even have? Is there some way of validating the correctness of a linker?

I don't think there could be a universal solution. However, IMHO it could check for well-known toolchains on Rust-supported platforms. For example on macOS there are SDK roots for Apple platforms, and Homebrew packages for a few non-Apple platforms. Cargo could detect these. On Debian it could check the known location of sysroots. It could check for presence of standard gnu cross-build toolchains.


Second problem is that linking usually needs target-specific libraries, at least libc. Zig does a hard work of building and bundling libc for every target it supports, so at least simple executables work out of the box.

Ease of getting the libraries varies greatly between platforms. For example, distros like Debian have first-class support for multi-arch libraries and sysroots. If rustup was taught to install the sysroot when you add a target, it could make cross-compilation work seamlessly on Debian.

OTOH targeting macOS from any other platform raises questions whether it's legal to use Apple's SDK outside of Apple's hardware. Apple broke compatibility with GCC long time ago, so it's not even easy to use GNU toolchain as an alternative, similar to windows-gnu instead of windows-msvc.

targeting macOS from any other platform

Yes, aware of this. I'm not sure why/how Go is able to work here, they must not have a problem with it. I'm also aware that the macOS SDK wants everything to link to it to dynamically look up a syscall table, but obviously Go just circumvents this, it's not like syscall numbers change often as I'm sure this would break a lot of software.

No, Go doesn't use syscalls on macOS. It used to, but they fixed it in 1.12. See Go 1.12 Release Notes - The Go Programming Language.

3 Likes

Go's workaround seems to be to compile and link an executable once on the target OS using a real C compiler and real OS libraries, and then take it apart and reuse it as a template for go executables built by go itself (copy its linker sections).

That's clever, but requires controlling the linker and knowing all the libraries that the executable will ever need. Linking to something that isn't built-in in go requires cgo, which makes all of the Go's magic vanish, and is just another painful C compiler.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.