对于小型原生 Mac 实用工具,如果代码随可执行文件一起发布,可以从静态库开始。如果需要独立的运行时组件,可选择动态库;如果需要 Apple 的捆绑包封装,则可选择框架。本指南会以两种方式构建同一个 C++ 辅助程序。
具体任务是为本地 macOS 桌面实用工具打包可复用的网格计算功能。示例使用合成输入和终端命令,让你在为应用项目选择格式之前,分别了解编译、链接和加载过程。
你实际上在做什么选择?
这里有两个决策:代码如何链接,以及交付物如何打包。静态库通常以 .a 结尾,是一个目标文件归档。动态库以 .dylib 结尾,在运行时仍是独立映像。框架是一种用于组织库及相关内容的捆绑包。Apple 在 An Apple Library Primer 中介绍了这些底层格式。
框架也可以包含静态库。Apple 从 Xcode 15 起支持创建静态框架,其中也包括资源。因此,仅凭 .framework 后缀无法确定库采用静态链接还是动态链接。请参阅 Creating a static framework。
为从源代码构建、由应用自有的辅助组件选择时,可参考以下决策指南:
| 你的需求 | 初始选择 | 引入的责任 |
|---|---|---|
| 一个可执行文件和一个一起重新构建的辅助组件 | 静态 .a |
辅助组件发生变化时,重新链接可执行文件 |
| 运行时保持独立的辅助组件 | 动态 .dylib |
提供库并确保运行时查找路径有效 |
| 在 Xcode 中管理、带有公共头文件和捆绑资源的组件 | 框架 | 单独选择其链接方式,并配置目标成员资格 |
| 面向多个 Apple 平台的二进制变体 | XCFramework | 构建并打包所需的平台变体 |
这些是根据文档所述格式得出的实用建议,并不代表哪种方案性能更优。对于这个网格实用工具,静态归档是自然的起点:它只有一个客户端,没有资源捆绑包,也不需要替换独立库。
XCFramework 用于打包二进制变体;它不能取代静态与动态之间的选择。Apple 允许在这种分发格式中包含静态或动态框架和库。请参阅 Creating a multiplatform binary framework bundle。
选择容器前先检查兼容性
文件名或 CPU 架构匹配并不足够。macOS、Mac Catalyst 和 iOS 是不同的构建平台。为某个平台编译的 arm64 库,不能直接链接到另一个平台的可执行文件中。Apple 在 An Apple Library Primer 中明确区分了平台和架构。
对于此任务,请确保以下输入一致:macOS 目标、所需 CPU 架构、最低 macOS 版本,以及兼容的语言和运行时设置。通用 Mac 交付物需要为每个支持的架构提供匹配的依赖切片。Apple 的 Building a universal macOS binary 介绍了如何重新构建库和可执行文件。
C++ 兼容性也需要单独关注。选择 -std=c++17 是在选择语言模式;它并不能保证独立构建的组件之间具有二进制兼容性。LLVM 介绍了 libc++ 的 ABI 保证和供应商配置选项,其中某些设置会改变类型布局。对于应用自有的依赖,一个实用的默认做法是使用应用的工具链和一致的设置重新构建。请参阅 libc++’s ABI Guarantees。
此示例通过一个兼容 C 的头文件公开一个函数,并用 C++ 实现它。受条件编译保护的 extern \"C\" 声明为 C++ 编译指定 C 语言链接方式。它不会把任意 C++ 类转换成 C 接口,也不会让二进制跨平台通用。C++ 工作草案在 Linkage specifications 中定义了这一边界。
将同一个辅助程序分别构建为静态库和动态库
你需要一台装有 Apple 开发者工具和 macOS SDK 的 Mac。完整的 Xcode 包含命令行工具;Apple 也提供独立的 Command Line Tools 安装包。如果两者都没有安装,请运行 xcode-select --install,并在继续之前完成安装。请参阅 Installing the command-line tools。
下面的完整脚本会新建一个临时目录,并且只使用合成源文件。请在终端中运行。脚本会明确针对当前 shell 环境的架构,并将 macOS 11.0 作为示例部署最低版本。这是一个单架构练习;如果项目需要不同的最低版本,请相应调整。
我检查了这个示例,但无法执行 Mac 构建:研究环境运行的是 Linux,而且没有 clang++。显示的结果是预期结果,并非实际记录的测试输出。
bash <<'SH'
set -eu
work_dir=$(mktemp -d "${TMPDIR:-/tmp}/mac-library-demo.XXXXXX")
cd "$work_dir"
printf 'Example directory: %s\n' "$work_dir"
mkdir include src build
mkdir -p build/static build/dynamic/lib
cat > include/grid.h <<'EOF'
#ifndef GRID_H
#define GRID_H
#ifdef __cplusplus
extern "C" {
#endif
/* Inputs: positive dimensions whose product fits in int. */
int grid_cells(int rows, int columns);
#ifdef __cplusplus
}
#endif
#endif
EOF
cat > src/grid.cpp <<'EOF'
#include "grid.h"
int grid_cells(int rows, int columns) {
return rows * columns;
}
EOF
cat > src/main.cpp <<'EOF'
#include "grid.h"
#include <cstdio>
int main() {
const int cells = grid_cells(3, 4);
std::printf("Grid cells: %d\n", cells);
return cells == 12 ? 0 : 1;
}
EOF
sdk_path=$(xcrun --sdk macosx --show-sdk-path)
build_arch=$(uname -m)
case "$build_arch" in
arm64|x86_64) ;;
*) printf 'Unexpected architecture: %s\n' "$build_arch" >&2; exit 1 ;;
esac
xcrun --sdk macosx clang++ \
-std=c++17 -arch "$build_arch" -isysroot "$sdk_path" \
-mmacosx-version-min=11.0 -Iinclude \
-c src/grid.cpp -o build/grid.o
xcrun --sdk macosx ar rcs build/static/libGrid.a build/grid.o
xcrun --sdk macosx clang++ \
-std=c++17 -arch "$build_arch" -isysroot "$sdk_path" \
-mmacosx-version-min=11.0 -Iinclude \
src/main.cpp build/static/libGrid.a -o build/static/grid-demo
xcrun --sdk macosx clang++ \
-arch "$build_arch" -isysroot "$sdk_path" \
-mmacosx-version-min=11.0 -dynamiclib build/grid.o \
-Wl,-install_name,@rpath/libGrid.dylib \
-o build/dynamic/lib/libGrid.dylib
xcrun --sdk macosx clang++ \
-std=c++17 -arch "$build_arch" -isysroot "$sdk_path" \
-mmacosx-version-min=11.0 -Iinclude \
src/main.cpp build/dynamic/lib/libGrid.dylib \
-Wl,-rpath,@loader_path/lib -o build/dynamic/grid-demo
./build/static/grid-demo
./build/dynamic/grid-demo
xcrun --sdk macosx ar t build/static/libGrid.a
xcrun --sdk macosx otool -L build/static/grid-demo
xcrun --sdk macosx otool -L build/dynamic/grid-demo
xcrun --sdk macosx otool -l build/dynamic/grid-demo
xcrun --sdk macosx lipo -archs build/dynamic/grid-demo
SH
两次运行都应输出 Grid cells: 12。这个计算刻意保持简单:三行乘以四列。辅助程序声明的输入约定排除了乘积无法放入 int 的尺寸;此示例并未实现通用的输入验证 API。
静态构建展示了什么
-c 调用会生成 grid.o,而不链接可执行文件。归档命令会将该目标文件打包成 libGrid.a;其 r、c 和 s 选项分别请求插入或替换、创建归档以及生成符号索引。Clang 在其编译器命令指南中介绍了编译阶段,LLVM 则在 llvm-ar — LLVM archiver 中解释了归档操作。
客户端通过归档的显式路径进行链接。当静态和动态版本同时存在时,这样可以明确选中的输入。预期的归档列表中会包含 grid.o,而可执行文件的依赖列表中不应有对 libGrid.a 的运行时依赖。
这里的静态链接仅适用于 Grid 辅助程序。这并不表示生成的可执行文件不依赖 macOS 运行时库;请检查 otool -L 报告的其余条目。Apple 在 An Apple Library Primer 中区分了构建时链接器和运行时加载器。
动态构建展示了什么
-dynamiclib 调用会创建独立库。它的安装名称是 @rpath/libGrid.dylib。可执行文件记录的运行路径是 @loader_path/lib;对于此可执行文件的运行路径条目,它指向相邻的 lib 目录。Clang 在其命令行参数参考中列出了相关驱动程序选项。
预期的动态客户端 otool -L 输出会包含 @rpath/libGrid.dylib;加载命令输出应包含一个值为 @loader_path/lib 的 LC_RPATH 条目。Apple 在 Run-Path Dependent Libraries 中介绍了安装名称和可执行文件运行路径如何协同工作。
这样就有了一个具体的打包测试:整体移动 build/dynamic 目录,保留其内部布局,然后从新位置运行其中的可执行文件。只移动可执行文件会让它找不到 Grid 依赖。链接成功说明找到了构建输入,但并不能说明运行时软件包完整。
什么时候应将辅助程序做成框架?
当框架的捆绑包结构能满足项目需求时,选择框架。例如,组件需要在 Xcode 中统一管理公共头文件和资源。对于这个仅用于计算的辅助程序,框架会增加打包工作,却没有满足额外需求。
对于在 Xcode 15 或更高版本中创建静态框架,Apple 记录了以下步骤:
- 选择
File > New > Target,选取 macOS,然后使用Framework模板。 - 选择新目标的
Build Settings,并将Mach-O Type改为Static Library。 - 将实现文件添加到
Compile Sources,将资源添加到Copy Bundle Resources。 - 将导出的头文件添加到
Headers构建阶段,并在伞形头文件中包含公共头文件。 - 将框架添加到使用它的应用,并按照 Apple 记录的嵌入配置进行设置。
对于带有资源的静态框架,Apple 指定使用 Embed & Sign。Xcode 会从嵌入的捆绑包中省略已经链接的主二进制文件,同时保留捆绑包内容。请遵循 Creating a static framework,不要将所有静态框架都当作无需嵌入的归档文件。
如果要分发二进制变体,请使用 XCFramework。对于现有的独立 .a,Apple 记录了 xcodebuild -create-xcframework 的 -library 和 -headers 输入;并建议不要手动把归档伪装成框架。使用 xcodebuild 需要完整的 Xcode。请参阅 Creating a multiplatform binary framework bundle 和 Installing the command-line tools。
Swift 会如何改变选择?
Swift 会在底层链接方式选择的基础上,增加模块使用和演进方面的要求。ABI 稳定性、模块稳定性和库演进针对的是不同的兼容性问题;.framework 容器不会自动启用这些特性。
Swift 指南指出,与客户端一起构建和分发的框架通常不应启用库演进。对于单独构建并单独更新的二进制框架,应从首次发布起就决定是否支持演进。在 Xcode 中,BUILD_LIBRARY_FOR_DISTRIBUTION 可启用库演进和模块稳定性。这些选择会影响性能特征和允许的 API 更改,详见 Library Evolution in Swift。
请将这些保证限制在文档规定的平台范围内。Swift 文档说明了 Apple 平台上的跨编译器二进制兼容性;不要将此承诺扩展到所有支持 Swift 的平台,也不要将其理解为可以任意更改 API。此示例中的兼容 C 头文件也不构成完整的 Swift 模块配置。
按照构建发生的顺序诊断失败
根据失败阶段缩小下一步检查范围:
| 症状 | 首先检查 | 下一步操作 |
|---|---|---|
编译器找不到 grid.h |
头文件是否存在于 include 下 |
检查工作目录和 -Iinclude |
| Grid 函数出现未定义引用 | 实现和声明是否一致 | 确认链接命令中包含预期的归档或动态库 |
| 架构或平台不匹配 | 实际依赖切片和构建平台 | 为客户端的 macOS 目标重新构建依赖 |
| 可执行文件链接成功,但无法加载 Grid | 记录的依赖和运行路径 | 比较 otool -L、otool -l 和打包后的目录布局 |
| Intel 与 Apple silicon 构建的行为不同 | 每项依赖覆盖的架构 | 检查每个二进制文件,并测试目标架构 |
Clang 的编译器命令指南将编译与链接区分开来。Apple 的 Run-Path Dependent Libraries 解释了运行时解析,而 Building a universal macOS binary 介绍了使用 lipo -archs 检查架构。这些区别让诊断顺序很有用:更改头文件路径无法修复缺失的运行时库。
对于这个合成网格实用工具,请保留静态归档,直到有具体的打包或运行时需求足以说明更换方案的理由。如果出现此类需求,请将同一个小型计算保留为构建检查,然后分别验证所选库、目标兼容性和最终目录布局。
来源
- An Apple Library Primer — Apple Developer Technical Support。
- Creating a static framework — Apple。
- Creating a multiplatform binary framework bundle — Apple。
- Building a universal macOS binary — Apple。
- Installing the command-line tools — Apple。
- Run-Path Dependent Libraries — Apple Documentation Archive。
- clang — the Clang C, C++, and Objective-C compiler — LLVM Project。
- Clang command line argument reference — LLVM Project。
- llvm-ar — LLVM archiver — LLVM Project。
- libc++’s ABI Guarantees — LLVM Project。
- Linkage specifications — C++ Working Draft,由 eel.is 呈现。
- Library Evolution in Swift — Swift.org。