Для небольшой нативной утилиты Mac начните со статической библиотеки, если код поставляется вместе с исполняемым файлом. Выберите динамическую библиотеку, если полезно иметь отдельный компонент времени выполнения, а фреймворк — если вам нужна упаковка в виде пакета Apple. В этом руководстве один и тот же вспомогательный компонент на C++ собирается обоими способами.

Конкретная задача — упаковать повторно используемый расчёт сетки для локальной настольной утилиты macOS. В примере используются синтетические входные данные и команды терминала, чтобы по отдельности увидеть компиляцию, компоновку и загрузку, прежде чем выбирать формат для проекта приложения.

Что именно вы выбираете?

Здесь есть два решения: как компонуется код и как упаковывается результат. Статическая библиотека, обычно с расширением .a, — это архив объектных файлов. Динамическая библиотека с расширением .dylib остаётся отдельным образом времени выполнения. Фреймворк — это пакет, в котором собраны библиотека и связанные с ней материалы. Apple описывает эти базовые форматы в документе An Apple Library Primer.

Фреймворк также может содержать статическую библиотеку. Начиная с Xcode 15 Apple поддерживает создание статических фреймворков, в том числе с ресурсами. Поэтому одного расширения .framework недостаточно, чтобы понять, статически или динамически компонуется библиотека. См. Creating a static framework.

Используйте эту памятку для вспомогательного компонента приложения, который вы собираете из исходного кода:

Ваше требование Начальный выбор Какая ответственность возникает
Один исполняемый файл и компонент, который пересобирается вместе с ним Статическая .a При изменении компонента нужно заново скомпоновать исполняемый файл
Компонент, который остаётся отдельной частью времени выполнения Динамическая .dylib Нужно предоставить библиотеку и обеспечить рабочий путь её поиска во время выполнения
Компонент Xcode с открытыми заголовками и включёнными в пакет ресурсами Фреймворк Нужно отдельно выбрать тип компоновки и настроить принадлежность файлов целевым объектам
Двоичные варианты для нескольких платформ Apple XCFramework Нужно собрать и упаковать варианты для требуемых платформ

Это практические рекомендации, основанные на документированных форматах. Они не определяют, какой вариант производительнее. Для этой утилиты расчёта сетки естественно начать со статического архива: клиент один, пакет ресурсов не нужен, а заменять отдельную библиотеку не требуется.

XCFramework упаковывает двоичные варианты, но не заменяет выбор между статической и динамической компоновкой. Apple допускает статические и динамические фреймворки и библиотеки в этом формате распространения. См. Creating a multiplatform binary framework bundle.

Перед выбором контейнера проверьте совместимость

Одинакового имени файла или архитектуры процессора недостаточно. macOS, Mac Catalyst и iOS — разные целевые платформы сборки. Библиотеку arm64, скомпилированную для одной платформы, нельзя просто скомпоновать с исполняемым файлом для другой платформы. Apple отдельно разграничивает платформу и архитектуру в документе An Apple Library Primer.

Для этой задачи согласуйте следующие параметры: целевую версию macOS, требуемую архитектуру процессора, минимальную версию macOS и совместимые настройки языка и среды выполнения. Для универсального приложения Mac нужны соответствующие срезы зависимостей для каждой поддерживаемой архитектуры. В документе Apple Building a universal macOS binary описана пересборка как библиотек, так и исполняемых файлов.

Совместимость C++ требует отдельного внимания. Выбор -std=c++17 задаёт режим языка, но не гарантирует двоичную совместимость независимо собранных компонентов. LLVM описывает гарантии ABI и варианты настроек libc++, в том числе настройки, меняющие расположение типов в памяти. Для зависимости, принадлежащей приложению, разумно пересобрать её тем же набором инструментов и с согласованными настройками. См. libc++’s ABI Guarantees.

В примере одна функция объявлена в заголовке, совместимом с C, а реализована на C++. Объявление с защитой extern \"C\" задаёт языковую компоновку C при компиляции на C++. Оно не превращает произвольные классы C++ в интерфейсы C и не обеспечивает переносимость двоичного файла между платформами. Проект стандарта C++ описывает эту границу в разделе Linkage specifications.

Соберите один вспомогательный компонент как статическую и динамическую библиотеки

Вам понадобится Mac с инструментами разработчика Apple и SDK для macOS. Полная версия Xcode включает инструменты командной строки; Apple также предоставляет отдельный пакет Command Line Tools. Если инструменты не установлены, выполните xcode-select --install и завершите установку, прежде чем продолжать. См. Installing the command-line tools.

Полный скрипт ниже создаёт новую временную папку и использует только синтетические исходные файлы. Запустите его в Terminal. Он явно задаёт архитектуру текущей среды оболочки и 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; вывод команд загрузки должен содержать запись LC_RPATH со значением @loader_path/lib. 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 документирует параметры -library и -headers команды xcodebuild -create-xcframework и не рекомендует вручную маскировать архив под фреймворк. Для 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. Эти различия помогают выстроить порядок диагностики: изменение путей к заголовкам не исправит отсутствие библиотеки времени выполнения.

Для синтетической утилиты расчёта сетки оставьте статический архив, пока конкретное требование к упаковке или времени выполнения не оправдает другой выбор. Если такое требование появится, сохраните тот же небольшой расчёт как проверку сборки, а затем отдельно проверьте выбранную библиотеку, совместимость целевой платформы и структуру итоговой папки.

Источники