對於小型原生 Mac 工具程式,如果程式碼會與可執行檔一起發佈,請從靜態程式庫開始。當獨立的執行階段元件有其用途時,選擇動態程式庫;需要 Apple 的套件封裝方式時,則選擇架構。本指南會以兩種方式建置同一個 C++ 輔助程式。
實際任務是為本機 macOS 桌面工具程式封裝可重複使用的網格計算功能。範例使用合成輸入資料和終端機命令,讓你在為 App 專案選擇格式之前,分別檢視編譯、連結和載入。
你實際上要做什麼選擇?
這裡有兩項決定:程式碼如何連結,以及交付項目如何封裝。靜態程式庫通常以 .a 結尾,是物件檔的封存檔。以 .dylib 結尾的動態程式庫則保留為獨立的執行階段映像檔。架構是一種用來整理程式庫及相關資料的套件。Apple 在 An Apple Library Primer 中說明了這些底層格式。
架構也可以包含靜態程式庫。Apple 支援在 Xcode 15 及更新版本中建立靜態架構,並可包含資源。因此,單看 .framework 副檔名無法判斷程式庫是靜態連結還是動態連結。請參閱 Creating a static framework。
如果你要從原始碼建置由 App 管理的輔助程式,可參考以下決策指南:
| 需求 | 建議起始選擇 | 隨之而來的責任 |
|---|---|---|
| 一個可執行檔,並與輔助程式一同重新建置 | 靜態 .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 保證和廠商設定選項,包括會改變型別配置的設定。對於由 App 管理的相依項目,實用的預設做法是使用 App 的工具鏈及一致的設定重新建置。請參閱 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 在 compiler command guide 中說明編譯階段;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 在 command-line argument reference 中列出相關驅動程式選項。
動態用戶端預期的 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建置階段,並將公開標頭檔納入傘狀標頭檔。 - 將架構加入使用它的 App,並依照 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 晶片建置的行為不同 | 每個相依項目的架構涵蓋範圍 | 檢查每個二進位檔,並測試指定的架構 |
Clang 的 compiler command guide 將編譯與連結分開說明。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, rendered by eel.is。
- Library Evolution in Swift — Swift.org。