Universal binaries
In Hello Static Library I created a binary with the architecture of the machine where it was compiled, in my case ARM. But if we were to distribute it to customers with Intel chips we would need a universal binary, which is an executable that packs executable code for multiple architectures. These are also called fat binaries, and each architecture is called a slice.
The complete script is very similar, except it compiles twice and joins the result.
Static library demo
Creating a fat binary
We see that the compiler is now targeting specific architectures and joining the result.
# Same as before but compile twice to different folders
mkdir -p arm64 x86_64
swiftc -emit-module-path arm64 \
-target arm64-apple-macosx10.9.0 \
-o arm64/Hello.o \
...
swiftc -emit-module-path x86_64
-target x86_64-apple-macosx10.9.0 \
-o x86_64/Hello.o \
...
# join the products compiled into one object file
lipo -create arm64/Hello.o x86_64/Hello.o -output Hello.o
Same trick for the executable, compile twice, join the result. The resulting executable contains two architectures.
% file UseHello
UseHello: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64:Mach-O 64-bit executable arm64]
UseHello (for architecture x86_64): Mach-O 64-bit executable x86_64
UseHello (for architecture arm64): Mach-O 64-bit executable arm64
% lipo -archs UseHello
x86_64 arm64
% dyld_info -platform UseHello
UseHello [x86_64]:
-platform:
platform minOS sdk
macOS 10.9 15.0
UseHello [arm64]:
-platform:
platform minOS sdk
macOS 11.0 15.0
macOS chooses the most suitable version for the current CPU, but I can also run each separatedly:
% arch -arch arm64 ./UseHello
Hello World!
% arch -arch x86_64 ./UseHello
Hello World!
Or even extract one:
% lipo -thin x86_64 ./UseHello -output UseHellox86_64
% ./UseHellox86_64
Hello World!
But how come my Apple Silicon CPU runs x86? This is due to Rosetta 2. Rosetta 2 is Apple’s translation layer that allows x86_64 (Intel) binaries to run on ARM-based Macs. It translates the x86_64 instructions to ARM64 instructions on-the-fly, allowing me to run Intel-compiled software on my Apple Silicon Mac.
Simulator slice
To generate a simulator slice pass the appropriate target and specify a different SDK.
swiftc -parse-as-library \
-emit-object -o iossimulator/Hello.o \
-emit-module -module-name Hello -emit-module-path iossimulator \
-enable-library-evolution \
-emit-module-interface-path Hello.swiftinterface \
-sdk $(xcrun --show-sdk-path --sdk iphonesimulator) \
-target x86_64-apple-ios18.0-simulator \
Hello.swift
...
The simulator slice is x86_64 so adding it to the static library would overwrite the macOS slide. For this I’ll need to create a XCFramework, which I do in Hello XCFramework.
macOS ARM64e slice
ARM64e is an architecture extension for Apple’s ARM64. It is available in several phones and latest M chips and provides enhanced security features. In the script before I replaced arm64 with arm64e and tried to run it:
% ./UseHello
zsh: killed ./UseHello
Not very explanatory. Console.app shows additional messages:
kernel AMFI: 'UseHello' has no CMS blob?
kernel AMFI: 'UseHello': Unrecoverable CT signature issue, bailing out.
exec_mach_imgact: not running binary built against preview arm64e ABI
To get rid of the first messages sign the executable. This, by the way, adds a load command LC_CODE_SIGNATURE.
security find-identity -v -p codesigning | grep Development
codesign -s "YOUR_ID_HERE" UseHello
However, the third would require to disable SIP and boot into ARM64e preview mode using sudo nvram boot-args=-arm64e_preview_abi
. Turns out this technology is still in development. Apple processes are compiled with ARM64e and can easily be replaced with new versions of the operative system, but Apple discourages its use by developers since OS updates can break compatibility. Therefore, the XNU kernel (where that exec_mach_imgact
function belongs) is refusing to run the binary.
Using libHello.a from SPM
We wrap the library in XCFramework.
xcodebuild -create-xcframework \
-library libHello.a \
-output Hello.xcframework
Then you can run with
swift build --arch x86_64
.build/debug/UseHello
Actually, I tried --arch x86_64 --arch arm64
, but something not so cool happened…
A mystery
Unexpectedly, compilation fails for arm64.
% swift build --arch arm64
error: module 'Hello' was created for incompatible target x86_64-apple-macosx10.9.0: /Users/jano/Desktop/Hello/.build/arm64-apple-macosx/debug/ModuleCache/Hello-1UJ689923AKQI.swiftmodule
1 | import Hello
| `- error: module 'Hello' was created for incompatible target x86_64-apple-macosx10.9.0: /Users/jano/Desktop/Hello/.build/arm64-apple-macosx/debug/ModuleCache/Hello-1UJ689923AKQI.swiftmodule
The reason is that SPM targets x86_64 despite the flag.
% grep -A 1 'target' .build/debug/description.json
"-target",
"x86_64-apple-macosx15.0",
(?) I don’t know what to make of this. Sounds like a question for the packagemanager forum.
Conclusion
Generating universal binaries is a basic ability when distributing software for systems that run on multiple architectures. We’ll be using this in the next article to build a fat static framework. Spoiler alert --arch x86_64 --arch arm64
works fine this time.