Apple macOS has for many years supported the ability to have binaries for multiple platforms in one executable. Upon execution the correct binary data is loaded into RAM. This has multiple names such as “Universal”, “fat binary” or “Multi-Architecture Binaries”. These were really useful when Apple was transition from PowerPC to Intel CPUs, a single binary would execute on both platforms. It could be useful in the future if, as many predict, Apple move from Intel to ARM CPUs.

In this post I’ll talk about the m4 script I wrote at HP to use this feature for combined 32bit and 64bit Intel binaries (back when that was relevant).

I was working on a high-performance asynchronous C API to connect to MySQL servers around 2014 which was to be used by HP. As part of this I wanted to make it work on as many platforms as possible. Quite a few people in my division used Apple hardware so I wanted to make it work on that. At the time I had access to both 32bit and 64bit Apple hardware and wanted my binaries to work on both. Hence building this solution.

I was using autotools back then as it is my preferred build system, so after figuring out how to do it I created a script which would build a single library that simultaneously worked for x86 and x86_64 platforms. The script is as follows:

# SYNOPSIS
#
#   AX_UNIVERSAL_BINARY()
#
# DESCRIPTION
#
#   --enable-universal-binary
#
# LICENSE
#
#  Copyright 2014 Hewlett-Packard Development Company, L.P.
#  All rights reserved.
#  
# [CUT BSD 3-Clause license, see SPDX-License-Identifier: BSD-3-Clause]

#serial 2

AC_DEFUN([AX_UNIVERSAL_BINARY],
    [AC_ARG_ENABLE([universal-binary],
                   [AC_HELP_STRING([--enable-universal-binary=auto],
                                   [Apple combined x86 & x86_64 binary support])],,
                   [enable_universal_binary=auto])
    have_universal_binary=no
    if test x"enable_universal_binary" != x"no"; then
        AC_CANONICAL_HOST
        AC_MSG_CHECKING([for universal binary support])
        case $host in *-apple-darwin*)
            save_CFLAGS="$CFLAGS"
            save_CXXFLAGS="$CXXFLAGS"
            save_LDFLAGS="$LDFLAGS"
            CFLAGS="$CFLAGS -arch x86_64 -arch i386"
            CXXFLAGS="$CXXFLAGS -arch x86_64 -arch i386"
            LDFLAGS="$LDFLAGS -arch x86_64 -arch i386"
            AC_LINK_IFELSE([AC_LANG_SOURCE([int main() {return 0;}])],
                          [have_universal_binary=yes])
            if test x"$have_universal_binary" = x"no"; then
                CFLAGS="$save_CFLAGS"
                CXXFLAGS="$save_CXXFLAGS"
                LDFLAGS="$save_LDFLAGS"
            fi
            ;;
        esac
        AC_MSG_RESULT($have_universal_binary)
    fi
    case "$have_universal_binary:$enable_universal_binary" in
        no:yes) AC_MSG_ERROR([no universal binary support on this host]) ;;
        yes:*)  AC_MSG_WARN([disabling dependency tracking])
                AM_CONDITIONAL([AMDEP],[false])
                AM_CONDITIONAL([am__fastdepCC],[false])
                AMDEPBACKSLASH=
                ;;
    esac])

You can see here that it first looks “Apple Darwin” which is the macOS kernel and the advertised platform. If we don’t have this then we error saying that we cannot do this.

In the compiler we specify multiple archs, in this case x86_64 and i386. The compiler knows what to do with this and we have a very simple test to build to make sure it works.

If we get this far the script disables dependency tracking. I honestly can’t remember why now (it has been 6 years) but I know dependency tracking didn’t work.

Summary

There have been attempts to do this in Linux too using a project called “FatELF“. This doesn’t appear to have taken off but I think it would be very interesting if more work was put into this. Especially with ARM and Intel both becoming major Linux platforms.