Adam Israel

Adam Israel

An economy of words.

3-Minute Read

During my adventures learning Rust, I’ve been writing an implementation of coreutils (arch, base64, basename, wc, etc). It’s been an interesting exercise, working to recreate these GNU utilities in a new language, but today I got to learn a whole lot about architectures and Universal Binaries.

So. This weekend, I picked up a 2022 Macbook Pro M2.

And while doing some refactoring over the weekend, to use workspaces with cargo, Rust’s package manager, I realized that my implementation of arch was returning x86_64.

At first, I thought there was a bug with the platform-info crate I was using to get the architecture. I looked deeper, and suspected the bug was in the underlying libc bindings.

Reader, there was no bug.

TIL a bunch about Apple Silicon, M2, and universal binaries.

To test this, I created a simple Rust application, using libc the same way platform-info does:

// arch/src/main.rs
extern crate libc;
use self::libc::{uname, utsname};

use std::ffi::CStr;
use std::mem::MaybeUninit;

macro_rules! cstr2cow {
    ($v:expr) => {
        CStr::from_ptr($v.as_ref().as_ptr()).to_string_lossy()
    };
}

fn main() {
    unsafe {
        let mut uts = MaybeUninit::<utsname>::uninit();
        if uname(uts.as_mut_ptr()) != -1 {
            let uts = uts.assume_init();
            println!("{}", cstr2cow!(uts.machine));
        }
    }
}

The resulting binary still returned x86_64. It turns out that’s because Rust is compiling to x86_64, and the resulting binaries are running through Rosetta (a compatibility layer that allows Intel binaries to run on Apple Silicon).

It is possible, though, to build a native arm64 binary. Using rustup, the Rust toolchain installer, to add new build targets. In this case, we want to add a target for aarch64-apple-darwin:

rustup target add aarch64-apple-darwin

Next, we can tell cargo to build an arm64 binary:

cargo build --target aarch64-apple-darwin

Running this binary yields arm64 “as expected”, and the binary itself is also arm64:

$ file target/aarch64-apple-darwin/release/arch
target/aarch64-apple-darwin/release/arch: Mach-O 64-bit executable arm64

There’s still one more step, though. How do we create a Universal Binary – both x86_64 and arm64? There’s some discussion about if and how to implement this via cargo, but for the time being we can run this additional step:

lipo -create -output arch target/release/arch target/aarch64-apple-darwin/release/arch

This creates a Universal Binary containing both architectures:

$ file arch
arch: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64Mach-O 64-bit executable x86_64] [arm64:Mach-O 64-bit executable arm64Mach-O 64-bit executable arm64]
arch (for architecture x86_64):	Mach-O 64-bit executable x86_64
arch (for architecture arm64):	Mach-O 64-bit executable arm64

Running the Universal Binary results in this, which is the native architecture.

$ arch
arm64

And there we have it. Rust can build x86_64 and arm64 binaries on Apple Silicon, and we can link the two to create a Universal Binary.

It also raises some interesting considerations around architecture. The way I’ve typically used the arch command is to determine what architecture I’m currently running on, but in this case, the answer is both. Cargo and rustup make it easy to add and build for different architectures.

Addendum

As pointed out by @kornel@mastadon.social, the reason my build was defaulting to x86_64 was because I was running the Intel build of Rust, installed on my previous Intel-based Mac and transferred over when I setup the new laptop. After uninstalling and reinstalling Rust,I now have arm64 builds by default (and the reverse is true, I can add an intel toolchain to build x86_64).

comments powered by Disqus

Recent Posts

Categories

About

This website is the digital home of software engineer, author, and genealogist Adam Israel.