小規模なネイティブMacユーティリティでは、コードを実行ファイルと一緒に配布する場合、まず静的ライブラリを選びましょう。独立したランタイムコンポーネントが役立つ場合は動的ライブラリを、Appleのバンドル形式でのパッケージ化が必要な場合はフレームワークを選びます。このガイドでは、同じC++ヘルパーを2通りの方法でビルドします。

ここで扱う具体的な作業は、ローカルのmacOSデスクトップユーティリティ向けに、再利用可能なグリッド計算をパッケージ化することです。この例では、合成入力とターミナルコマンドを使い、アプリプロジェクトの形式を選ぶ前に、コンパイル、リンク、読み込みを個別に確認できます。

何を選んでいるのか?

決めることは2つあります。コードのリンク方法と、成果物のパッケージ形式です。通常.aで終わる静的ライブラリは、オブジェクトファイルのアーカイブです。.dylibで終わる動的ライブラリは、独立したランタイムイメージとして残ります。フレームワークは、ライブラリと関連ファイルをまとめるバンドルです。Appleは、これらの基礎となる形式をAn Apple Library Primerで説明しています。

フレームワークには静的ライブラリを含めることもできます。AppleはXcode 15以降で、リソースを含む静的フレームワークの作成をサポートしています。そのため、.frameworkという拡張子だけでは、ライブラリが静的リンクか動的リンクかは分かりません。Creating a static frameworkを参照してください。

ソースからビルドする、アプリが所有するヘルパーには、次の判断基準を使ってください。

要件 最初に選ぶ形式 伴う責任
実行ファイルとヘルパーを一緒に再ビルドする 静的.a ヘルパーを変更したら実行ファイルを再リンクする
独立したランタイムコンポーネントとしてヘルパーを維持する 動的.dylib ライブラリを提供し、実行時の検索パスを機能させる
公開ヘッダーとバンドルリソースを備えたXcodeコンポーネント フレームワーク リンク形式を別途選び、ターゲットへの所属を設定する
複数のAppleプラットフォーム向けのバイナリバリアント XCFramework 必要なプラットフォームバリアントをビルドしてパッケージ化する

これらは、文書化された形式に基づく実用的な推奨事項です。性能上の優劣を示すものではありません。このグリッドユーティリティでは、クライアントは1つだけで、リソースバンドルもなく、独立したライブラリを差し替える要件もないため、静的アーカイブが自然な出発点です。

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++で実装した1つの関数を、C互換のヘッダーを通じて公開します。ガード付きのextern \"C\"宣言は、C++コンパイル時にC言語リンケージを指定します。任意のC++クラスをCインターフェースに変換したり、バイナリをプラットフォーム間で移植可能にしたりするものではありません。C++ワーキングドラフトでは、この境界をLinkage specificationsで定義しています。

1つのヘルパーを静的ライブラリと動的ライブラリの両方としてビルドする

Appleの開発ツールとmacOS SDKを備えたMacが必要です。完全版のXcodeにはコマンドラインツールが含まれています。Appleは、単体のCommand Line Toolsパッケージも提供しています。どちらもインストールされていない場合は、xcode-select --installを実行し、インストーラーを完了してから続けてください。Installing the command-line toolsを参照してください。

以下のスクリプトは、新しい一時ディレクトリを作成し、合成ソースファイルだけを使います。ターミナルで実行してください。現在のシェル環境のアーキテクチャと、例としてmacOS 11.0を最小デプロイメントバージョンに指定しています。これは単一アーキテクチャ向けの演習です。プロジェクトの要件に応じて別の最小バージョンを指定してください。

例は確認しましたが、調査環境はLinuxでclang++もないため、Macでのビルドは実行できませんでした。表示される結果は予想される結果であり、テストで記録された出力ではありません。

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と表示されるはずです。この計算は意図的に小さくしてあり、3行に4列を掛けています。ヘルパーの入力仕様では、積が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は次のように説明しています。

  1. File > New > Targetを選び、macOSを指定してFrameworkテンプレートを使います。
  2. 新しいターゲットのBuild Settingsを選び、Mach-O TypeをStatic Libraryに変更します。
  3. 実装ファイルをCompile Sourcesに、リソースをCopy Bundle Resourcesに追加します。
  4. エクスポートするヘッダーをHeadersビルドフェーズに追加し、公開ヘッダーをアンブレラヘッダーに含めます。
  5. フレームワークを利用するアプリに追加し、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のcompiler command guideは、コンパイルとリンクを分けて説明しています。AppleのRun-Path Dependent Librariesはランタイムでの解決を、Building a universal macOS binaryはlipo -archsによるアーキテクチャの確認方法を説明しています。こうした区別に沿って診断することで、ヘッダーパスを変更してもランタイムライブラリの欠落は直らないことが分かります。

この合成グリッドユーティリティでは、具体的なパッケージ要件またはランタイム要件が別の選択を正当化するまで、静的アーカイブを使ってください。その要件が生じたら、同じ小さな計算をビルド確認に使い、選択したライブラリ、ターゲットとの互換性、最終的なディレクトリ構成を個別に検証します。

参考資料