← Back to DevBytes

macOS Rosetta 2: Running x86 Apps on M-Series

Introduction to Rosetta 2 on macOS

When Apple announced its transition from Intel-based x86_64 processors to its own ARM-based Apple Silicon (M1, M2, M3, and beyond), a critical question emerged: what happens to the vast library of existing Intel applications? The answer is Rosetta 2, a translation layer built into macOS that allows apps compiled for Intel architecture to run seamlessly on Apple Silicon Macs. For developers, understanding how Rosetta 2 works, how to test under it, and how to eventually migrate away from it is essential for delivering a native experience.

What Is Rosetta 2?

Rosetta 2 is a just-in-time (JIT) and ahead-of-time (AOT) binary translation system developed by Apple. Unlike the original Rosetta from the PowerPC-to-Intel transition, Rosetta 2 performs much of its translation work at install time, converting x86_64 instructions into ARM64 instructions and caching the result. This dramatically reduces runtime overhead and makes translated applications feel nearly as fast as native ones for many workloads.

Key characteristics of Rosetta 2 include:

Why Rosetta 2 Matters for Developers

For developers, Rosetta 2 is both a safety net and a diagnostic tool. It matters because it provides a bridge period during which you can ship Intel binaries while you work on a native ARM64 build. However, relying on it indefinitely has costs: translated apps consume more memory, cannot take advantage of ARM-specific features like the Neural Engine, and may exhibit subtle bugs in low-level code that touches memory ordering, inline assembly, or custom JIT engines.

Understanding Rosetta 2 helps you in three concrete scenarios:

Checking Architecture and Rosetta Status

Before diving into usage, you should know how to inspect the architecture of binaries on your system. The file command and lipo tool are your primary utilities.

# Inspect a binary's architecture
file /Applications/Safari.app/Contents/MacOS/Safari

# Output for a universal binary:
# /Applications/Safari.app/Contents/MacOS/Safari: Mach-O universal binary with 2 architectures
# /Applications/Safari.app/Contents/MacOS/Safari (for architecture x86_64): Mach-O 64-bit executable x86_64
# /Applications/Safari.app/Contents/MacOS/Safari (for architecture arm64):  Mach-O 64-bit executable arm64

# List architectures in a binary
lipo -archs /Applications/Safari.app/Contents/MacOS/Safari
# Output: x86_64 arm64

You can also check whether your current shell or process is running under Rosetta by inspecting the sysctl.proc_translated value.

# Returns 1 if running under Rosetta, 0 if native
sysctl -n sysctl.proc_translated

This check is invaluable in build scripts and CI pipelines where behavior may need to differ between translated and native environments.

Installing Rosetta 2

On a fresh Apple Silicon Mac, Rosetta 2 is not installed by default. It is installed on demand the first time a user launches an Intel binary, but developers often want to install it explicitly, especially on headless CI machines. Use the following command:

# Install Rosetta 2 silently (useful for automation)
softwareupdate --install-rosetta --agree-to-license

If you omit the --agree-to-license flag, macOS will prompt interactively to accept the license agreement. In CI environments, always include the flag to avoid hanging on user input.

Running Apps Under Rosetta 2

Most of the time, macOS handles translation automatically. However, there are cases where you want to force an app to run under Rosetta even when a native ARM slice exists — for example, when a third-party plugin only supports Intel. You can do this via the Finder "Get Info" dialog by checking "Open using Rosetta," or programmatically with the arch command.

# Force a universal binary to run as x86_64
arch -x86_64 /Applications/Safari.app/Contents/MacOS/Safari

# Force it to run native arm64
arch -arm64 /Applications/Safari.app/Contents/MacOS/Safari

The arch command is also useful in shell scripts to ensure a specific toolchain runs in the correct architecture. For example, if you have an Intel-only homebrew installation at /usr/local and an ARM installation at /opt/homebrew, you may need to invoke Intel tools explicitly:

# Run an Intel-only binary from the x86 Homebrew prefix
arch -x86_64 /usr/local/bin/some-tool --version

Building Universal Binaries

The long-term goal for any developer should be to ship a universal binary containing both x86_64 and arm64 slices. This ensures the app runs natively on both Intel Macs (which cannot run ARM code) and Apple Silicon Macs. With Xcode, this is as simple as setting the architecture to "Standard Architectures (Apple Silicon, Intel)" in your build settings.

From the command line, you can build each architecture separately and combine them with lipo:

# Build for arm64
clang -arch arm64 -o myapp_arm64 main.c

# Build for x86_64
clang -arch x86_64 -o myapp_x86 main.c

# Combine into a universal binary
lipo -create -output myapp myapp_arm64 myapp_x86

# Verify
lipo -archs myapp
# Output: arm64 x86_64

For Xcode projects, a single invocation can produce a universal binary:

xcodebuild -project MyApp.xcodeproj \
  -scheme MyApp \
  -configuration Release \
  -destination 'generic/platform=macOS' \
  -arch arm64 -arch x86_64 \
  build

Detecting Rosetta at Runtime

Sometimes your application needs to know at runtime whether it is being translated. This is useful for displaying warnings, collecting telemetry, or adjusting behavior. The recommended approach is to use the sysctl API from C or call out to the shell from higher-level languages.

#include <sys/sysctl.h>
#include <stdio.h>

int is_running_under_rosetta(void) {
    int ret = 0;
    size_t size = sizeof(ret);
    if (sysctlbyname("sysctl.proc_translated", &ret, &size, NULL, 0) != 0) {
        return 0; // Assume native if the call fails
    }
    return ret == 1;
}

int main(void) {
    if (is_running_under_rosetta()) {
        printf("Running under Rosetta 2 translation.\n");
    } else {
        printf("Running natively.\n");
    }
    return 0;
}

In Swift, you can bridge to the same sysctl call:

import Darwin

func isRunningUnderRosetta() -> Bool {
    var ret = 0
    var size = MemoryLayout.size(ofValue: ret)
    let result = sysctlbyname("sysctl.proc_translated", &ret, &size, nil, 0)
    return result == 0 && ret == 1
}

Performance Considerations

While Rosetta 2 is impressively fast, it is not free. Translated applications typically see a memory overhead of 20-30% because the translated code is less compact than native ARM64. CPU-bound workloads may run at 70-90% of native speed, but workloads involving JIT compilation, virtualization, or heavy floating-point SIMD can degrade more significantly.

To measure the difference, use the time command or Instruments to compare the same binary running under both architectures:

# Time the native arm64 run
time arch -arm64 ./myapp --benchmark

# Time the translated x86_64 run
time arch -x86_64 ./myapp --benchmark

Common performance pitfalls under Rosetta include:

Best Practices

To make the most of Rosetta 2 and plan a smooth migration to native Apple Silicon, follow these best practices:

Conclusion

Rosetta 2 is a remarkable piece of engineering that made Apple's transition to Apple Silicon viable without leaving millions of existing applications behind. For developers, it is a powerful tool for maintaining compatibility during migration, but it should be treated as a temporary bridge rather than a permanent solution. By understanding how to inspect architectures, force translation when needed, build universal binaries, and detect Rosetta at runtime, you can deliver a smooth experience for all users while steadily moving toward fully native ARM64 builds. The sooner your application runs natively on Apple Silicon, the better its performance, memory footprint, and access to platform features will be — and Rosetta 2 is the tool that buys you the time to get there.

— Ad —

Google AdSense will appear here after approval

← Back to all articles