要链接原生 macOS 程序,每个选定的库切片都必须与其 CPU 架构和 Apple 平台匹配,并且语言 ABI 兼容。本指南介绍如何构建和检查一个小型 C 库,该库供 C++ 桌面命令行工具使用。
具体任务是从本地源代码生成 macOS 可执行文件,并诊断不兼容的原生依赖项。示例使用合成整数运算,无需下载库,并将不同架构的输出分开保存。
原生库链接前必须匹配哪些内容?
分别检查三项:架构、平台,以及语言之间的二进制接口。Clang 的目标规格不仅包含处理器信息;其目标和环境也会影响代码生成和库选择。依赖项也必须针对预期目标构建。使用 Clang 进行交叉编译。
| 检查项 | 此 macOS 任务要回答的问题 | 不匹配意味着什么 |
|---|---|---|
| CPU 架构 | 依赖项是否包含正在链接的 arm64 或 x86_64 切片? |
获取或构建缺失的架构。 |
| Apple 平台 | 该切片是否针对原生 macOS 构建? | 获取 macOS 构建版本;iOS 或模拟器构建属于不同变体。 |
| 语言 ABI | 声明、调用约定和所需运行时是否一致? | 修正接口,或使用该语言支持的互操作方式。 |
架构列表无法回答平台问题。Apple 的 Mach-O 格式将 CPU 信息与平台构建信息分开记录,并为 macOS、iOS、Mac Catalyst 和 iOS 模拟器定义不同的平台标识符。因此,仅标记为 arm64 的依赖项还需要进一步检查。Apple 的 Mach-O 格式定义。
本示例的预期构建约定是原生 macOS、最低部署目标为 macOS 13.0,并且每种架构分别构建一次。C 库提供 C 接口;可执行文件使用 C++17。这个版本选择仅供示例参考,并非 Mac 软件的通用最低要求。
使用明确的目标构建完整的 macOS 示例
使用安装了当前 Xcode 并已将其选作命令行开发工具的 Mac。Apple 将 xcrun 说明为在选定的开发者安装中查找或运行工具的机制。在排查源代码更改之前,先检查当前选择。使用 Xcode 从命令行构建常见问题解答。
以下命令适用于 macOS 上的 Bash。这里仅检查了这些命令,并未执行,因为研究环境是 Linux,且没有 Apple 工具链。
xcode-select --print-path
xcrun --sdk macosx --find clang
xcrun --sdk macosx --find clang++
xcrun --sdk macosx --show-sdk-path
如果这些检查失败,请先解决开发者工具选择或安装问题。构建不仅需要编译器,还需要 macOS SDK。SDK 提供构建产品所用的平台头文件和库。Xcode 构建设置参考:基础 SDK。
创建源文件
在 Bash 中运行此设置代码块。它会新建一个临时目录并打印其位置,因此不会改动现有项目文件。在同一个 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
当 C++ 翻译单元包含此头文件时,头文件会声明 C 语言链接。这可避免 C++ 调用方请求经过不同名称修饰的 C++ 函数名。C++ FAQ 还建议由 C++ 编译器驱动最终链接,以便包含它所需的库。Standard C++ FAQ:如何混用 C 和 C++。
分别编译、打包并链接每种架构
以下代码块会构建两个 macOS 变体。对于每种架构,它都将相同的目标和 SDK 传给库编译和可执行文件链接。Clang 说明 -c 会生成目标文件;如果未指定阶段选择选项,其驱动程序会继续执行链接。Clang 编译器命令指南。
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
这里,-target 选择架构和 macOS 部署目标,而 -isysroot 提供 SDK 根目录。这是两个独立的输入。明确指定归档文件路径,也能让命令清楚显示所选依赖项。Clang 交叉编译指南、Clang 命令行参数参考。
ar rcs 命令会将编译后的目标文件打包到静态归档文件中,并生成符号索引。归档操作只是打包现有目标文件,不会将它们编译为另一种目标。LLVM 记录了归档操作及其与其他 ar 实现的兼容性。LLVM 归档工具文档。
示例输入 3 和 4 应生成 Tiles: 12。此微型函数仅用于演示链接;对于乘积超出 int 范围的任意尺寸,其接口并未定义行为。
如何检查架构、平台和符号?
检查实际的可执行文件、库或目标文件。Apple 说明可使用 lipo -archs 列出架构,并指出应用程序的可执行文件位于其软件包的 Contents/MacOS/ 目录中。构建通用 macOS 二进制文件。
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
前两条命令应分别显示对应的架构。若要检查平台,请查看 otool -l 打印的加载命令;LLVM 兼容的 llvm-otool 也说明了相同选项。LLVM Mach-O 转储工具。
查找 LC_BUILD_VERSION。其 platform 应标识 macOS,而 minos 记录最低操作系统版本。sdk 字段是独立的。某些较旧的二进制文件则使用 LC_VERSION_MIN_* 命令。Apple 的格式定义说明了这两种表示方式,其中 macOS 平台标识符为 1。Mach-O 构建版本结构。
对于静态库,应检查其成员目标文件,而不要把归档包装器当作平台兼容性的证明。本示例保留 tile.o 以供此用途。对于不熟悉的归档文件,可使用 xcrun ar t 列出其成员,并在单独的工作目录中检查。LLVM 说明了归档文件的列出和提取操作。LLVM 归档工具文档。
最后检查符号。示例库应在 Mach-O 中定义 _tile_total。在传统的 nm 输出中,T 表示全局代码符号,U 表示未定义引用。定义和引用是不同的证据。LLVM 符号表文档。
使用以下诊断顺序决定下一步操作:
- 缺少所需的 CPU 切片: 为该架构重新构建依赖项,或获取匹配的二进制文件。
- CPU 匹配但平台不同: 获取原生 macOS 变体。更改调用方的架构并不能证明平台兼容。
- CPU 和平台匹配,但函数未解析: 比较声明的函数与库导出的符号,并确认链接命令中包含预期的归档文件。
- C 和 C++ 中的名称不同: 修正共享头文件中的语言链接声明,然后重新构建调用方。
- 部署版本不同: 检查依赖项的最低版本;在支持的情况下,针对预期部署目标重新构建,或者有意提高应用程序的系统要求。
此诊断顺序是根据上述目标、Mach-O 和链接文档整理而成的决策辅助方法。它将缺少机器代码与平台元数据不兼容、语言接口问题区分开来,让每次重新构建都能针对具体发现解决问题。
Swift 调用原生库时有哪些变化?
CPU 和平台匹配,并不意味着每种语言接口都可以调用。Swift 默认支持 C 互操作;直接使用 C++ 互操作则必须启用,并且只支持有文档说明的部分 C++ 特性。只要 Swift 构建通过已配置的头文件或模块机制导入 C 接口,本示例面向 C 的头文件就提供了一种受支持的接口选择。混合使用 Swift 和 C++。
对于直接使用 C++ 接口,Swift 文档说明命令行构建可使用 -cxx-interoperability-mode=default,导入头文件可使用 Clang 模块,传递 C++ 导入器设置可使用 -Xcc。导入配置与链接编译后的实现是两个独立的构建要求。设置 Swift 与 C++ 混合语言项目。
请遵守文档说明的限制:C++ 互操作需要 Swift 5.9 或更高版本,以及 C++14 或更高版本。Swift 和 C++ 混合代码必须使用相同的 C++ 标准库;Apple 平台使用 libc++。从 C++ 调用 Swift 需要生成的接口以及受支持的 Clang 编译器。导入的 C++ 引用类型还要求 macOS 13.3 或更高的部署版本。选择这条路线之前,请根据当前兼容性文档核对具体 API。C++ 互操作支持的功能和限制。
何时应使用通用二进制文件或 XCFramework?
对于此 macOS 专用可执行文件,将两个已完成的 macOS 构建合并为一个通用二进制文件。Apple 说明了先分别编译和链接每种架构,再使用 lipo 合并生成的可执行文件。构建通用 macOS 二进制文件。
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
在运行 macOS 13.0 或更高版本的受支持 Mac 上,最后一条命令应打印 Tiles: 12 并成功返回。构建两个切片与测试两个切片是两回事:Intel Mac 无法运行 arm64 切片。Apple 的通用二进制文件指南。
若要支持多个 Apple 平台或平台变体的库,请使用 XCFramework 打包分别构建的变体。Apple 明确要求开发者将 iOS 和 iOS 模拟器静态库分开,并支持在同一个 XCFramework 中包含原生 macOS 和 Mac Catalyst 变体。对于 Xcode 归档工作流,其文档中的做法是使用目标位置来选择平台和 SDK。创建多平台二进制框架包。
下次遇到 macOS 链接失败时,请记录预期目标,检查所选依赖项的架构和平台,然后检查语言接口。将这些发现与准确的链接命令放在一起。这样,你就能据此选择正确的二进制文件,或从源代码重新构建它。
来源
使用 Clang 进行交叉编译 — LLVM Project。
使用 Xcode 从命令行构建常见问题解答 — Apple。
构建设置参考 — Apple。
clang — Clang C、C++ 和 Objective-C 编译器 — LLVM Project。
Clang 命令行参数参考 — LLVM Project。
Standard C++:如何混用 C 和 C++ — Standard C++ Foundation。
llvm-ar — LLVM 归档工具 — LLVM Project。
llvm-otool — Mach-O 转储工具 — LLVM Project。
llvm-nm — 列出 LLVM 位码和目标文件的符号表 — LLVM Project。
Mach-O 格式定义:loader.h — Apple。
混合使用 Swift 和 C++ — Swift.org。
设置 Swift 与 C++ 混合语言项目 — Swift.org。
C++ 互操作支持的功能和限制 — Swift.org。
构建通用 macOS 二进制文件 — Apple。
创建多平台二进制框架包 — Apple。