-- Leo's gemini proxy

-- Connecting to gemini.hitchhiker-linux.org:1965...

-- Connected

-- Sending request

-- Meta line: 20 text/gemini;lang=en-US

Cross compilers III: cross compiling Rust

2023-03-15

Since the official Rust compiler, rustc, uses llvm as a code generator, it is technically already capable of cross compilation to any of the architectures that llvm supports. However, we still need a linker for the target. Eventually lld, being a cross linker, might be a suitable drop in for this use. However, I have not really been able to find information on how to set this up or if it is even possible. What definitely is possible is using gcc as a driver for the linker, as this is what rustc does by default already. We're just going to swap out our system gcc for a cross gcc such as that built in part one of this series.

Part I


> NOTE: We're using gcc here as a driver for the linker. The actual linker is `ld` from binutils, or something like `aarch64-linux-musl-ld` depending on the cross target. Since gcc knows the parameters to pass to ld it is often used in this way. In fact, when you use gcc to compile and link a binary it also calls the preprocessor `cpp` before runnings it's own code, and then calls `ld` to do the linking. So it's not only a C compiler but also a frontend for the rest of the toolchain.


We're going to start out with a somewhat common use case, compiling Rust code on an x86_64 machine that will run on a Raspberry Pi 3 or 4 (or similar SBC with the aarch64 architecture) using a musl libc based distribution such as Alpine or Void. Note that while this probably could be done using your distribution's packages, it's much easier if you use rustup. Using rustup, we can add a target which will include the pre-compiled `std` and `core` libraries for the target system (assuming we're compiling for a tier 1 supported system, which aarch64-unknown-linux-musl is).

rustup target add aarch64-unknown-linux-musl

Note that if you try to compile your Rust code for this target now, it will fail. There is a little more setup to be done. We need to configure Cargo next. This can be done globally for your user with the file ~/.cargo/config.toml or per project by creating .cargo/config.toml inside the project directory. The syntax is the same, regardless. For this simple case, all we need to do is define which linker to use in the appropriate table. Edit your config.toml to look like the one below.

[target.aarch64-unknown-linux-musl]
linker = "aarch64-linux-musl-gcc"

Now we can compile for our target just by passing the appropriate command line arguments to Cargo.

cargo build --target=aarch64-unknown-linux-musl --release

What if our target platform is NOT tier 1?

If the target platform is not considered tier 1 by the Rust project, don't despair. Things might be a bit more complicated, but still likely to work provided llvm can generate code for the target architecture. Our example is going to be riscv64, again with musl libc. Now riscv64 is a tier 1 supported platform for Linux if you are using Glibc, but Musl is tier 3. This means that the `core` and `std` crates may not be available for the platform and we can't just add a target via rustup. Since the `build-std` flag for Cargo is behind a feature flag that means a nightly compiler is required. Also, there is an issue with the compiler not being able to find the libc start files crt1.o, crti.o and crtbegin.o, which can be resolved via another addition to .cargo/config.toml.

# add to .cargo/config.toml
[target.riscv64gc-unknown-linux-musl]
rustflags = [
    "-C", "target-feature=-crt-static"
]
linker = "riscv64-linux-musl-gcc"
ar = "riscv64-linux-musl-ar"

With that addition, and assuming that the cross toolchain tools are in our PATH, we can compile our code by adding some extra parameters.

cargo +nightly build -Z build-std=std --target=riscv64gc-unknown-linux-musl --release

In this case, `+nightly` tells cargo to use a nightly compiler. We pass `-Z build-std=std` to tell Cargo that we want to build `std` from source. Now, there is another situation worth mentioning. If you are at all like me, then you likely like small and optimized binaries. I generally add some extra juice to `Cargo.toml` for most of my projects to tell Cargo to use fat LTO, only one codegen unit so that it can optimize the entire binary as a single unit, and often set the panic handler to `abort` rather than Rust's default use of libunwind. It's this last part that might trip you up again.

# in Cargo.toml in the project root
[profile.release]
codegen-units = 1
strip = true
lto = true
panic = "abort"

I figure that having a nice unwind and trace is in case of the need for debugging. That's what debug builds are for. If you need unwinding in a release binary then you're probably not taking full advantage of Rust or it's not fullfilling it's promises for your use case. Anyway, compiling a project with those settings using the earlierr command will fail.

suaron% cargo +nightly build -Z build-std=std --target=riscv64gc-unknown-linux-musl --release
   Compiling jah v0.1.0 (/home/nathan/src/jah)
error[E0463]: can't find crate for `panic_abort`

For more information about this error, try `rustc --explain E0463`.
error: could not compile `jah` due to previous error

The answer is actually pretty simple and once again rustc's great error messages point the way, although it's not as glaringly obvious as I'm sometimes used to. The answer is that we have to build `panic_unwind` along with `std`.

suaron% cargo +nightly build -Z build-std=std,panic_abort --target=riscv64gc-unknown-linux-musl --release
    Finished release [optimized] target(s) in 0.04s

Tags for this page

rust

compilers

cross-compilation


Home

All posts


All content for this site is licensed as CC BY-SA.

© 2023 by JeanG3nie

Finger

Contact

-- Response ended

-- Page fetched on Mon May 20 10:15:42 2024