Чтобы собрать нативный исполняемый файл для Mac из нескольких файлов C++, передайте в clang++ все необходимые файлы реализации или скомпилируйте их в объектные файлы и слинкуйте их вместе. В этом руководстве создаётся небольшая программа для Терминала и объясняется, как диагностировать отсутствующие определения.
Подготовьте инструменты компиляции для Mac
Используйте этот порядок действий для небольшой программы на C++, которую хотите запускать локально в Терминале. Apple предоставляет компилятор и SDK macOS в составе Xcode или отдельного пакета Command Line Tools. Если Xcode уже установлен, отдельный пакет не нужен. В противном случае выполните следующую команду и завершите установку в открывшемся диалоговом окне. См. Установка инструментов командной строки от Apple.
xcode-select --install
После установки проверьте, доступен ли компилятор:
clang++ --version
Для финальной компоновки используйте clang++, в том числе если входные данные уже скомпилированы в объектные файлы. В документации Clang прямо рекомендуется этот драйвер для программ на C++, поскольку режим языка определяет, какие библиотеки времени выполнения он подключает автоматически. См. Сборка полного набора инструментов.
Соберите полноценную программу на C++ из трёх файлов
В этом вымышленном примере программа считает предметы на трёх настольных лотках и форматирует результат. В ней есть точка входа и два вспомогательных файла реализации. Создайте новую пустую рабочую папку и сохраните в ней следующие четыре файла:
desk-demo/
desk.hpp
main.cpp
label.cpp
totals.cpp
Сохраните общие объявления в desk.hpp:
#ifndef DESK_DEMO_DESK_HPP
#define DESK_DEMO_DESK_HPP
#include <string>
std::string make_label(int count);
int total_items(int trays, int per_tray);
#endif
Сохраните точку входа в main.cpp:
#include "desk.hpp"
#include <iostream>
int main() {
const int count = total_items(3, 4);
std::cout << make_label(count) << '\n';
return 0;
}
Сохраните реализацию форматирования в label.cpp:
#include "desk.hpp"
std::string make_label(int count) {
return "Desk items: " + std::to_string(count);
}
Сохраните реализацию подсчёта в totals.cpp:
#include "desk.hpp"
int total_items(int trays, int per_tray) {
return trays * per_tray;
}
Каждый файл реализации подключает один и тот же заголовок, поэтому компилятор может проверить соответствие определения функции общему объявлению. Защита заголовка предотвращает повторное включение в пределах одной единицы трансляции. Оставьте тела этих обычных невстраиваемых функций в файлах реализации: в C++ требования к определениям для всей программы задаёт правило одного определения.
Скомпилируйте и слинкуйте одной командой
Откройте Терминал в папке с четырьмя файлами и выполните команду:
clang++ -std=c++17 main.cpp label.cpp totals.cpp -o desk-demo && ./desk-demo
Ожидаемый вывод:
Desk items: 12
Команда перечисляет все три файла реализации и задаёт имя выходного файла с помощью -o. Поскольку она не содержит параметра, останавливающего процесс на одном из этапов, Clang компилирует входные файлы и запускает компоновщик. Явный параметр -std=c++17 выбирает версию языка, вместо того чтобы полагаться на значение компилятора по умолчанию. Эти параметры описаны в руководстве по командам компилятора Clang.
Оболочка запускает исполняемый файл, только если сборка прошла успешно, поскольку команды соединены оператором &&. Это важно при повторной сборке: после ошибки не следует случайно запускать старый исполняемый файл и принимать его вывод за результат новой сборки.
Когда уместно использовать шаблон имени файла?
Для этой конкретной папки следующая команда — альтернатива явному списку исходников:
clang++ -std=c++17 ./*.cpp -o desk-demo && ./desk-demo
Оболочка раскрывает ./*.cpp в список подходящих файлов текущей папки. Она не ищет файлы во вложенных папках и не включает файлы с расширениями .cc или .cxx. Если вы явно указываете пути с пробелами, заключайте каждый такой путь в кавычки.
Используйте шаблон, только пока каждый совпавший файл нужен этому исполняемому файлу. Если позднее вы добавите отдельную демонстрационную программу со своей main или альтернативную реализацию для другой платформы, используйте явный список исходников. Здесь «все файлы реализации» означает все файлы, необходимые для одной цели сборки, причём каждое требуемое определение должно быть предоставлено подходящим образом.
Сначала скомпилируйте объектные файлы, затем слинкуйте их
Разделение этапов помогает определить, происходит ли ошибка при компиляции отдельного исходного файла или при объединении программы. Используя те же четыре файла и рабочую папку, выполните команду:
(
set -e
mkdir -p build
clang++ -std=c++17 -c main.cpp -o build/main.o
clang++ -std=c++17 -c label.cpp -o build/label.o
clang++ -std=c++17 -c totals.cpp -o build/totals.o
clang++ build/main.o build/label.o build/totals.o -o build/desk-demo
./build/desk-demo
)
Параметр -c создаёт объектный файл без компоновки. При финальном вызове компилятора передаются три объектных файла, и компоновщик собирает из них исполняемый файл. Это соответствует описанным в документации Clang этапам компиляции. Команда set -e в подоболочке останавливает эту последовательность, если одна из команд завершается с ошибкой.
Здесь также указывайте имена объектных файлов явно. Компоновка всех объектных файлов из повторно используемой папки сборки может случайно включить устаревший файл от предыдущей версии программы. Если исходные файлы в разных папках имеют одинаковые базовые имена, задайте для их объектных файлов разные пути, чтобы не записать оба под одним и тем же именем.
Если вы меняете только label.cpp, можно перекомпилировать этот объектный файл и повторить команду компоновки. Если вы меняете desk.hpp, пересоберите все исходные файлы, которые подключают этот заголовок. В этом ручном процессе нужно отслеживать такие зависимости самостоятельно; по мере роста проекта удобнее использовать систему сборки.
Прежде чем менять команду сборки, диагностируйте ошибку
Следующая последовательность применяет к этому примеру этапы компиляции и правила C++ для определений. Точная формулировка сообщений об ошибках зависит от установленного набора инструментов.
| Симптом | Что проверить в первую очередь | Подходящий следующий шаг |
|---|---|---|
| Не найден заголовок | Написание пути подключения и расположение заголовка | Восстановите показанную структуру папок или настройте правильную директорию поиска заголовков. |
При компоновке не определена make_label или total_items |
Входит ли её реализация в список входных файлов | Добавьте недостающий файл .cpp или скомпилированный объектный файл в команду компоновки. |
| Функция всё ещё не определена, хотя её файл подключён | Сигнатуры объявления и определения | Сравните пространства имён, типы параметров и квалификаторы; подключите общий заголовок в файле реализации. |
| Обнаружены дублирующиеся символы | Повторяющиеся файлы реализации среди входных данных | Удалите повторно указанный объектный файл, подключённый файл .cpp или непреднамеренное альтернативное определение. |
Не определена или дублируется main |
Файлы с точкой входа | Добавьте нужную main.cpp ровно один раз и исключите точки входа других программ. |
| Не определены символы библиотеки C++ | Драйвер финальной компоновки | Используйте clang++, чтобы подключить библиотеки времени выполнения C++. |
Например, компиляция только main.cpp может пройти: заголовок объявляет обе функции, поэтому вызовы принимаются. Но для финальной компоновки всё равно нужны их определения. Подключение ещё одного заголовка не добавит недостающий объектный файл реализации. Эти требования объясняются правилом одного определения в C++.
Не устраняйте ошибки из-за отсутствующих символов, подключая label.cpp внутри main.cpp и одновременно компилируя label.cpp отдельно. В этом случае одно и то же обычное определение функции будет предоставлено через две единицы трансляции. Общий заголовок и отдельные файлы реализации уже задают нужную структуру.
Чтобы посмотреть команды инструментов, не выполняя сборку, используйте документированный параметр Clang -###. Заключите его в кавычки, чтобы оболочка передала его без изменений:
clang++ '-###' -std=c++17 main.cpp label.cpp totals.cpp -o desk-demo
Это покажет выбранные драйвером вызовы компилятора и компоновщика и поможет отличить желаемую команду от той, которую запустит набор инструментов. См. Сборка полного набора инструментов.
Соблюдайте совместимость архитектуры, платформы и ABI
Добавление всех исходных файлов не исправит несовместимость двоичных входных данных. Для локальной сборки используйте один установленный набор инструментов Apple и собирайте исходные файлы программы вместе. При добавлении готовых библиотек проверьте поддерживаемые ими платформы и архитектуры, прежде чем менять списки исходников.
Для необязательной универсальной сборки Apple описывает компиляцию и компоновку отдельно для каждой архитектуры с последующим объединением готовых исполняемых файлов с помощью lipo. Если набор инструментов и SDK поддерживают обе цели, пример можно собрать следующим образом. Указанные ниже целевые версии macOS 11 приведены лишь для примера:
(
set -e
mkdir -p build
clang++ -std=c++17 -target arm64-apple-macos11 \
main.cpp label.cpp totals.cpp -o build/desk-arm64
clang++ -std=c++17 -target x86_64-apple-macos11 \
main.cpp label.cpp totals.cpp -o build/desk-x86_64
lipo -create build/desk-arm64 build/desk-x86_64 \
-output build/desk-universal
lipo -archs build/desk-universal
)
Каждая подключаемая зависимость должна предоставлять подходящие входные данные для соответствующей архитектурной сборки. Бинарный файл для iOS не становится бинарным файлом для macOS лишь потому, что обе платформы используют arm64. Apple объясняет архитектурные срезы, различия между платформами и проверку исполняемых файлов в статье Создание универсального бинарного файла для macOS.
Версия языка C++ также отличается от двоичной совместимости, или ABI. Одного совпадения -std=c++17 недостаточно, чтобы гарантировать совместимость структур и конфигураций отдельно собранных библиотек. LLVM описывает параметры конфигурации библиотеки, влияющие на ABI, и указывает, что пользователям не следует самостоятельно задавать эти параметры поставщика. Для этой задачи разумнее оставить стандартную конфигурацию библиотеки набора инструментов. См. Гарантии ABI libc++.
Используйте тот же список исходников в Xcode или проекте со Swift
В существующей цели Xcode выберите цель и откройте раздел Build Phases. Проверьте список Compile Sources — в нём должны быть файлы реализации, необходимые этому исполняемому файлу. Добавляйте пользовательские зависимости от библиотек через Link Binary with Libraries. Apple описывает эти отдельные задачи в статье Настройка этапов сборки цели.
Для этого примера список исходников должен учитывать main.cpp, label.cpp и totals.cpp. Добавление заголовка в проект не заменяет компиляцию файлов реализации.
Если окружающее приложение написано на Swift, приведённые выше команды для чистой программы на C++ охватывают только эту программу. Документированный способ Swift требует включить совместимость с C++, чтобы открыть API для взаимодействия языков; через эту границу доступны только поддерживаемые языковые возможности. См. Совместное использование Swift и C++.
Код на Swift и C++ должен использовать одну и ту же стандартную библиотеку C++; на платформах Apple Swift использует libc++. В документации Swift также описаны ограничения компилятора и версии языка, в том числе требование C++14 или новее для совместимости. Прежде чем считать существующую библиотеку совместимой зависимостью, проверьте эти требования. См. Поддерживаемые возможности и ограничения совместимости с C++.
Для локальной задачи в Терминале начните с явной сборки трёх исходных файлов. Если она не удаётся, определите этап, на котором возникла ошибка, проверьте отсутствующее или повторяющееся определение и убедитесь в совместимости двоичных входных данных. После того как команда выдаст ожидаемый результат, у вас будет конкретный список исходников, который можно перенести в более крупную сборку.
Источники
- Установка инструментов командной строки — Apple
- clang — компилятор Clang для C, C++ и Objective-C — проект LLVM
- Сборка полного набора инструментов — проект LLVM
- Правило одного определения — рабочий проект стандарта C++
- Создание универсального бинарного файла для macOS — Apple
- Гарантии ABI libc++ — проект LLVM
- Настройка этапов сборки цели — Apple
- Совместное использование Swift и C++ — Swift.org
- Поддерживаемые возможности и ограничения совместимости с C++ — Swift.org