To link a native macOS program, every selected library slice must match its CPU architecture and Apple platform, with a compatible language ABI. This guide shows how to build and inspect a small C library used by a C++ desktop command-line tool.

The concrete task is to produce a macOS executable from local source and diagnose an incompatible native dependency. The example uses synthetic integer arithmetic, requires no downloaded libraries, and keeps architecture-specific outputs separate.

What must match before a native library can link?

Check three things independently: architecture, platform, and the binary interface between languages. Clang’s target specification includes more than the processor; its target and environment influence code generation and library selection. Dependencies must also be built for the intended target. Cross-compilation using Clang.

Check Question for this macOS task What a mismatch means
CPU architecture Does the dependency contain the arm64 or x86_64 slice being linked? Obtain or build the missing architecture.
Apple platform Is that slice built for native macOS? Obtain a macOS build; an iOS or Simulator build is a separate variant.
Language ABI Do declarations, calling conventions, and required runtimes agree? Correct the interface or use the language’s supported interoperability path.

An architecture listing cannot answer the platform question. Apple’s Mach-O format records CPU information separately from platform build information, and defines distinct platform identifiers for macOS, iOS, Mac Catalyst, and iOS Simulator. A dependency described only as arm64 therefore needs further inspection. Apple’s Mach-O format definitions.

For this example, the intended build contract is native macOS, deployment target macOS 13.0, and one build per architecture. The C library exposes a C interface; the executable is C++17. The version is an illustrative choice, not a universal minimum for Mac software.

Build a complete macOS example with explicit targets

Use a Mac with a current Xcode installation selected for command-line development. Apple documents xcrun as the mechanism for finding or running tools in the selected developer installation. Check the selection before investigating source changes. Building from the Command Line with Xcode FAQ.

The commands below are designed for Bash on macOS. They were inspected but not executed here because the research environment is Linux and has no Apple toolchain.

xcode-select --print-path
xcrun --sdk macosx --find clang
xcrun --sdk macosx --find clang++
xcrun --sdk macosx --show-sdk-path

If these checks fail, resolve the developer-tool selection or installation first. The build needs the macOS SDK as well as a compiler. An SDK supplies the platform headers and libraries against which the product is built. Xcode Build Settings Reference: Base SDK.

Create the source files

Run this setup block in Bash. It creates a new temporary directory and prints its location, so existing project files remain untouched. Continue with the later blocks in the same shell.

set -euo pipefail

example_dir="$(mktemp -d "${TMPDIR:-/tmp}/mac-link-demo.XXXXXX")"
cd "$example_dir"
printf 'Example directory: %s\n' "$example_dir"
mkdir -p include src build/arm64 build/x86_64

cat > include/tile.h <<'EOF'
#ifndef TILE_H
#define TILE_H

#ifdef __cplusplus
extern "C" {
#endif

int tile_total(int rows, int columns);

#ifdef __cplusplus
}
#endif

#endif
EOF

cat > src/tile.c <<'EOF'
#include "tile.h"

int tile_total(int rows, int columns)
{
    return rows * columns;
}
EOF

cat > src/main.cpp <<'EOF'
#include <cstdio>
#include "tile.h"

int main()
{
    const int total = tile_total(3, 4);
    std::printf("Tiles: %d\n", total);
    return total == 12 ? 0 : 1;
}
EOF

The header declares C language linkage when a C++ translation unit includes it. This prevents the C++ caller from requesting a differently mangled C++ function name. The C++ FAQ also recommends letting the C++ compiler direct the final link so that its required libraries are included. Standard C++ FAQ: How to mix C and C++.

Compile, archive, and link each architecture

The following block builds both macOS variants. It passes the same target and SDK to the library compilation and executable link for each architecture. Clang documents -c as producing an object file; without a stage-selection option, its driver proceeds through linking. Clang compiler command guide.

mac_sdk="$(xcrun --sdk macosx --show-sdk-path)"

for cpu in arm64 x86_64; do
    mac_target="${cpu}-apple-macos13.0"

    xcrun --sdk macosx clang \
        -target "$mac_target" -isysroot "$mac_sdk" \
        -std=c17 -Wall -Wextra -Werror \
        -I include -c src/tile.c \
        -o "build/$cpu/tile.o"

    xcrun ar rcs "build/$cpu/libtile.a" "build/$cpu/tile.o"

    xcrun --sdk macosx clang++ \
        -target "$mac_target" -isysroot "$mac_sdk" \
        -std=c++17 -stdlib=libc++ -Wall -Wextra -Werror \
        -I include src/main.cpp "build/$cpu/libtile.a" \
        -o "build/$cpu/tile-demo"
done

Here, -target chooses the architecture and macOS deployment target, while -isysroot supplies the SDK root. These are separate inputs. The explicit archive path also makes the selected dependency visible in the command. Clang cross-compilation guidance, Clang command line argument reference.

The ar rcs command packages the compiled object into a static archive and supplies a symbol index. Archiving packages existing objects; it does not compile them for another target. LLVM documents the archive operations and their compatibility with other ar implementations. LLVM archiver documentation.

The sample inputs 3 and 4 should produce Tiles: 12. This tiny function is only a linking example; its interface does not define behavior for arbitrary dimensions whose product exceeds the range of int.

How do you inspect architecture, platform, and symbols?

Inspect the actual executable, library, or object file. Apple documents lipo -archs for listing architectures and notes that an app’s executable is inside its bundle’s Contents/MacOS/ directory. Building a universal macOS binary.

xcrun lipo -archs build/arm64/tile-demo
xcrun lipo -archs build/x86_64/tile-demo

xcrun otool -l build/arm64/tile.o
xcrun otool -l build/arm64/tile-demo

xcrun nm -g build/arm64/libtile.a

The first two commands should identify their respective architectures. For the platform check, inspect the load commands printed by otool -l; LLVM’s compatible llvm-otool documents the same option. LLVM Mach-O dumping tool.

Look for LC_BUILD_VERSION. Its platform should identify macOS, and minos records the minimum operating-system version. The sdk field is separate. Some older binaries instead use an LC_VERSION_MIN_* command. Apple’s format definitions document both representations, including macOS platform identifier 1. Mach-O build-version structures.

For a static library, inspect the member objects rather than treating the archive wrapper as proof of platform compatibility. This example retains tile.o for that purpose. With an unfamiliar archive, use xcrun ar t to list its members and inspect them in a separate working directory. LLVM documents archive listing and extraction operations. LLVM archiver documentation.

Finally, check symbols. The sample library should define _tile_total on Mach-O. In traditional nm output, T indicates a global code symbol and U indicates an undefined reference. A definition and a reference are different evidence. LLVM symbol-table documentation.

Use this diagnostic sequence to choose the next action:

  1. Required CPU slice missing: rebuild the dependency for that architecture or obtain the matching binary.
  2. CPU matches but platform differs: obtain a native macOS variant. Changing the caller’s architecture does not establish platform compatibility.
  3. CPU and platform match but the function is unresolved: compare the declared function with the library’s exported symbol and confirm that the intended archive appears in the link command.
  4. Names differ across C and C++: correct the language linkage in the shared header, then rebuild the caller.
  5. Deployment versions differ: inspect the dependency’s minimum version and rebuild it for the intended deployment target where supported, or deliberately raise the application’s requirement.

This sequence is a decision aid derived from the target, Mach-O, and linkage documentation above. It separates missing machine code from incompatible platform metadata and language-interface problems, so each rebuild addresses a specific finding.

What changes when Swift calls the native library?

A matching CPU and platform do not make every language interface callable. Swift supports C interoperability by default; direct C++ interoperability must be enabled and supports a documented subset of C++ features. The C-facing header in this example offers one supported interface choice, provided the Swift build imports it through its configured header or module mechanism. Mixing Swift and C++.

For a direct C++ interface, Swift documents -cxx-interoperability-mode=default for command-line builds, Clang modules for importing headers, and -Xcc for passing C++ importer settings. Import configuration and linking the compiled implementation remain separate build requirements. Setting Up Mixed-Language Swift and C++ Projects.

Preserve the documented limits: C++ interoperability requires Swift 5.9 or later and C++14 or newer. Mixed Swift and C++ code must use the same C++ standard library; Apple platforms use libc++. Calling Swift from C++ requires the generated interface and a supported Clang compiler. Imported C++ reference types also carry an additional macOS 13.3 deployment requirement. Check the exact API against the current compatibility documentation before choosing this route. Supported Features and Constraints of C++ Interoperability.

When should you use a universal binary or XCFramework?

For this macOS-only executable, merge the two completed macOS builds into a universal binary. Apple documents compiling and linking each architecture before combining the resulting executables with lipo. Building a universal macOS binary.

xcrun lipo -create \
    build/arm64/tile-demo \
    build/x86_64/tile-demo \
    -output build/tile-demo

xcrun lipo -archs build/tile-demo
./build/tile-demo

On a supported Mac running macOS 13.0 or later, the final command should print Tiles: 12 and return success. Building both slices is distinct from testing both: an Intel Mac cannot run the arm64 slice. Apple’s universal binary guidance.

For a library supporting several Apple platforms or platform variants, use an XCFramework to package separately built variants. Apple explicitly instructs developers to keep iOS and iOS Simulator static libraries separate, and supports including native macOS and Mac Catalyst variants in the same XCFramework. For Xcode archive workflows, its documented approach uses a destination to select the platform and SDK. Creating a multiplatform binary framework bundle.

For your next macOS linking failure, record the intended target, inspect the selected dependency’s architecture and platform, and then check the language interface. Keep those findings beside the exact link command. That gives you a concrete basis for selecting the correct binary or rebuilding it from source.

Sources