Оборачивание библиотеки C/C++ в Swift
Существует множество отличных библиотек, написанных на C/C++. Их можно использовать в вашем коде Swift, не переписывая их на Swift. Эта статья объяснит несколько способов достижения этого и лучшие практики при работе с C/C++ в Swift.
Пакет
- При необходимости создайте новый пакет Swift с каталогом
Package.swift, каталогомSourcesи т. д. - Создайте новый модуль/каталог в
Sourcesдля библиотеки C/C++. Предположим, что он называетсяCMyLibв остальной части этого раздела.- Одна из конвенций заключается в том, чтобы префикс модуля был
C. Например,CDataStaxDriver.
- Одна из конвенций заключается в том, чтобы префикс модуля был
- Добавьте каталог исходного кода библиотеки C/C++ в качестве подмодуля Git в
Sources/CMyLib.- Если всё настроено правильно, в корневом каталоге пакета Swift должен быть файл
.gitmodulesс содержимым, подобным этому:
[submodule "my-lib"] path = Sources/CMyLib/my-lib url = https://github.com/examples/my-lib.git - Если всё настроено правильно, в корневом каталоге пакета Swift должен быть файл
-
Измените
Package.swift, чтобы добавитьCMyLibкак целевой объект, и укажите расположения файлов исходного кода и заголовков..target( name: "CMyLib", dependencies: [], exclude: [ // Relative paths under 'CMyLib' of the files // and/or directories to exclude. For example: // "./my-lib/src/CMakeLists.txt", // "./my-lib/tests", ], sources: [ // Relative paths under 'CMyLib' of the source // files and/or directories. For example: // "./my-lib/src/foo.c", // "./my-lib/src/baz", ], cSettings: [ // .headerSearchPath("./my-lib/src"), ] ),Для библиотеки C++ используйте
cxxSettingsвместоcSettings. Доступны дополнительные параметры и параметры для определения целевого объекта. Подробную информацию см. в документации API SwiftPM. - Попробуйте скомпилировать пакет Swift с помощью
swift build. НастройтеPackage.swiftпо мере необходимости.
Карта модуля
Карта модуля генерируется автоматически для целевых объектов Clang (например, CMyLib), если не указана пользовательская карта. (т. е., файл module.modulemap существует в каталоге заголовков)
Правила генерации карты модуля можно найти здесь.
Файлы заголовков, сгенерированные сборкой библиотеки C/C++
Некоторые библиотеки C/C++ генерируют дополнительные необходимые файлы в процессе сборки (например, конфигурационный файл). Чтобы включить эти файлы в пакет Swift:
-
cdв корневой каталог библиотеки C/C++ (например,Sources/CMyLib/my-lib), затем выполните сборку.- Вспомните из шага 3, что это каталог подмодуля Git. Не нужно вносить изменения в этот каталог. Выходные файлы/каталоги, сгенерированные сборкой библиотеки C/C++, должны быть добавлены в
.gitignore.
- Вспомните из шага 3, что это каталог подмодуля Git. Не нужно вносить изменения в этот каталог. Выходные файлы/каталоги, сгенерированные сборкой библиотеки C/C++, должны быть добавлены в
- Создайте каталог в
Sources/CMyLib/для размещения необходимых файлов. (например,Sources/CMyLib/extra) - Скопируйте сгенерированные необходимые файлы из выходных данных сборки C/C++ в созданный на предыдущем шаге каталог.
- Обновите
Package.swift, добавив путь к созданному на шаге 2 каталогу (т. е.,extra) или пути к отдельным файлам (например,./extra/config.h) в массивsourcesцелевого объекта (т. е.,CMyLib) для файлов исходного кода или как.headerSearchPathдля заголовков.
Перезапись файлов в библиотеке C/C++
Чтобы использовать пользовательскую реализацию вместо предоставленной библиотекой C/C++:
- Создайте каталог в
Sources/CMyLibдля размещения файлов пользовательского кода. (например,Sources/CMyLib/custom) - Добавьте файлы пользовательского кода в созданный на предыдущем шаге каталог.
- При необходимости создайте отдельные подкаталоги для файлов исходного кода и заголовков.
- Обновите
Package.swift:- Добавьте путь к созданному на шаге 1 каталогу (т. е.,
custom) или пути к отдельным файлам (например,./custom/my_impl.c) в массивsourcesцелевого объекта (т. е.,CMyLib) для файлов исходного кода или как.headerSearchPathдля заголовков. - Добавьте пути к файлам библиотеки C/C++ в массив исключений целевого объекта (т. е.,
CMyLib). (например,./my-lib/impl.c)
- Добавьте путь к созданному на шаге 1 каталогу (т. е.,
CMake
Этот пример ориентирован на импорт C-библиотеки в Swift. Вам необходимо получить библиотеку, предоставить карту модулей, чтобы Swift мог её импортировать, а затем связать с ней. Механика в основном такая же для C++, и пример того, как двунаправленно взаимодействовать с библиотекой C++, созданной как часть одного проекта, доступен в проекте двунаправленного взаимодействия с cxx в репозитории примеров Swift-CMake.
Получение библиотеки
Если вы не собираете библиотеку C наряду с вашей библиотекой Swift, вам нужно как-то получить копию библиотеки.
- ExternalProject
- Выполняется во время сборки и имеет наименьшую настраиваемость, но также изолирует сборку библиотеки C/C++ от вашей сборки.
- Это хорошо, когда связь между вашим проектом и зависимостью низкая, и библиотека вряд ли будет установлена там, где должен работать ваш проект, или когда вам нужна некоторая настраиваемость сборки зависимости.
- Подробнее см. в документации по External Project.
include(ExternalProject)
ExternalProject_Add(ZLIB
GIT_REPOSITORY "https://www.github.com/madler/zlib.git"
GIT_TAG "09155eaa2f9270dc4ed1fa13e2b4b2613e6e4851" # v1.3
GIT_SHALLOW TRUE
UPDATE_COMMAND ""
CMAKE_ARGS
-DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR>
)
ExternalProject_Get_Property(ZLIB INSTALL_DIR)
add_library(zlib STATIC IMPORTED GLOBAL)
set_target_properties(zlib PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${INSTALL_DIR}/include"
IMPORTED_LOCATION "${INSTALL_DIR}/lib/libz.a"
)
add_executable(example example.c)
target_link_libraries(example PRIVATE zlib)
В этом примере zlib v1.3 загружается с GitHub и собирается. Поскольку мы установили зафиксированную метку, нам не нужно, чтобы CMake пытался обновить её; содержимое хэша коммита никогда не изменится. Целевой объект ZLIB, созданный External Project, является «утилитарной» библиотекой CMake, поэтому мы не можем связаться с ней напрямую. Вместо этого мы можем использовать find_package после установки ZLIB_DIR в каталог сборки, чтобы найти её, или, поскольку мы уже знаем, где она находится, мы можем создать импортированную статическую библиотеку. Установка INTERFACE_INCLUDE_DIRECTORIES в местоположение, где мы установили заголовки zlib, и IMPORTED_LOCATION в статический архив приводит к целевому объекту, с которым мы можем связать код. CMake затем сообщит компилятору, где искать заголовки и связать статический архив с любым целевым объектом, который связывается с импортированным zlib целевым объектом.
-
FetchContent- Выполняется во время конфигурации и приводит к объединённой диаграмме сборки. Это лучше всего подходит для включения внешних компонентов, которые являются деталями реализации вашей библиотеки. Обратите внимание, что поскольку диаграммы сборки объединены, имена переменных и целевых объектов необходимо соответствующим образом пространственно разделить, иначе они столкнутся, и вещи могут не собираться должным образом.
- Это хорошо, когда есть тесная связь между вашим проектом и зависимостью. Поскольку диаграммы сборки объединены, ваш проект может зависеть от отдельных целевых объектов сборки в зависимости, а не от проекта в целом, что может улучшить производительность сборки.
- Подробнее см. в документации по FetchContent.
-
find_package- Находит библиотеку и заголовки из sysroot. По умолчанию CMake будет искать в корне вашей ОС как sysroot, но может быть изолирован к другим sysroot для кросс-компиляции.
- Этот вариант подходит для выбора системных зависимостей из базовой системы или sysroot или для предоставления распространителю вашего проекта возможности использовать предварительно собранный проект с помощью
<PackageName>_ROOT. - Подробнее см. в документации по find_package.
В примере обёртывания существующей C-библиотеки в Swift с помощью CMake будет использоваться find_package, пользовательский файл карты модуля и наложение виртуальной файловой системы (VFS), а также вспомогательный уровень для миграции частей кодовой базы SQLite к чему-то, что может импортировать Swift.
Начало работы
Начните с базовой настройки CMake:
cmake_minimum_required(VERSION 3.26)
project(SQLiteImportExample LANGUAGES Swift C)
Это создаёт проект CMake под названием «SQLiteImportExample», который использует Swift и C и требует CMake версии 3.26 или новее.
Мы не будем собирать SQLite в этом примере, мы будем получать его из системы или из предоставленного sysroot.
find_package(SQLite3 REQUIRED)
Это сообщает CMake найти SQLite3 в соответствии с файлом пакета FindSQLite3.cmake. Поскольку мы отметили его как необходимую зависимость, CMake остановит конфигурацию сборки, если не найдёт части пакета.
После обнаружения CMake определяет следующие переменные:
-
SQLite3_INCLUDE_DIRS— Путь к файлу, гдеsqlite3.hбыл найден -
SQLite3_LIBRARIES— Библиотеки, с которыми пользователи sqlite должны будут связаться -
SQLite3_VERSION— Версия sqlite3, обнаруженная -
SQLite3_FOUND— Используется для того, чтобы сообщитьfind_packageо том, что SQLite был найден. Обратите внимание, что если мы не отметили его как пакетREQUIRED, мы могли бы позже проверить эту переменную, чтобы увидеть, был ли он найден, и перейти кExternalProjectдля сборки его отдельно, если его не было.
CMake также определит целевой объект сборки SQLite::SQLite3, который мы будем использовать позже для облегчения распространения информации о зависимости и месте поиска через нашу диаграмму сборки. Документация по пакету SQLite3 доступна здесь: FindSQLite3.
Импортирование SQLite в Swift
Swift не может импортировать заголовочные файлы напрямую. Некоторые инструменты, такие как SwiftPM и Xcode, иногда могут сгенерировать файл modulemap для связывающего заголовка, но другие, например CMake, этого не делают. Ручное создание файлов modulemap позволяет получить больший контроль над тем, как библиотека C импортируется в Swift. Подробная информация о написании файла modulemap доступна в спецификации Языка модулей.
Для нашего примера нам нужно только экспонировать заголовочный файл sqlite3.h для Swift.
Содержание нашего файла sqlite3.modulemap таково:
module CSQLite {
header "sqlite3.h"
}
Имя модуля представляет собой имя, которое мы используем для импорта этого модуля в Swift. Для нашего примера соответствующая строка импорта Swift будет import CSQLite.
Мы можем включить дополнительные директивы, такие как link "sqlite3", чтобы указать механизму автоматической привязки, что он должен автоматически подключаться к библиотеке sqlite3, но это не нужно для наших целей, так как CMake сделает это автоматически, когда мы скажем нашей программе подключаться к библиотеке sqlite.
Теперь нам нужно поместить файл modulemap в нужное место. Мы ожидаем, что файл modulemap будет находиться рядом с файлом sqlite.h, но в зависимости от расположения sqlite.h, это может быть недоступно. Здесь на помощь приходит виртуальная файловая система. Виртуальная файловая система (или VFS) — это представление файловой системы с точки зрения компилятора. Файл наложения VFS позволяет переопределить это представление, чтобы мы могли изменять имена файлов и размещать файлы в любом месте файловой системы с точки зрения компилятора, не размещая их физически на диске.
Формат ввода для наложения VFS — YAML (обратите внимание, что JSON является подмножеством YAML, поэтому вы можете представить это как объект JSON, если хотите). Недостатком является то, что этот файл ожидает абсолютные пути к корням или расположению, которое вы переопределяете. В зависимости от места записи этот путь может быть не переносимым, поэтому жёсткое кодирование этих файлов может не сработать. Однако мы можем использовать CMake для динамической генерации наложения, которое работает для нашей системы. Мы добавим следующую шаблон в свой проект и назовём его sqlite-vfs-overlay.yaml.
---
version: 0
case-sensitive: false
use-external-names: false
roots:
- name: "@SQLite3_INCLUDE_DIR@"
type: directory
contents:
- name: module.modulemap
type: file
external-contents: "@SQLite3_MODULEMAP_FILE@"
Однако файл неполный. Мы будем использовать шаблон наложения вместе со следующим CMake, чтобы сгенерировать окончательный файл наложения, соответствующий нашей среде.
# Setup the VFS-overlay to inject the custom modulemap to import SQLite into Swift
set(SQLite3_MODULEMAP_FILE "${CMAKE_CURRENT_SOURCE_DIR}/sqlite3.modulemap")
configure_file(sqlite-vfs-overlay.yaml "${CMAKE_CURRENT_BINARY_DIR}/sqlite3-overlay.yaml")
target_compile_options(SQLite::SQLite3 INTERFACE
"$<$<COMPILE_LANGUAGE:Swift>:SHELL:-vfsoverlay ${CMAKE_CURRENT_BINARY_DIR}/sqlite3-overlay.yaml>"
)
Результатом является файл наложения VFS, который вставляет пользовательский файл modulemap в каталог, где находится sqlite.h, одновременно переименовывая sqlite.3.modulemap в module.modulemap. Все программы Swift, использующие библиотеку SQLite3, должны использовать соответствующий файл наложения VFS для поиска файла modulemap. Мы используем target_compile_options для его добавления. Поскольку SQLite::SQLite3 — это импортируемая библиотека, она не может повлиять на сборку самого SQLite, поэтому мы добавляем её как опцию INTERFACE, гарантируя, что она будет передана всем зависимым от неё целям.
Запуск CMake в этом проекте должен либо сообщить о том, что у вас отсутствует SQLite, в этом случае вам нужно его установить, чтобы его использовать, или сгенерировать sqlite3-overlay.yaml в верхней части каталога сборки.
---
version: 0
case-sensitive: false
use-external-names: false
roots:
- name: "/usr/include"
type: directory
contents:
- name: module.modulemap
type: file
external-contents: "/home/ewilde/sqlite-import-example/sqlite3.modulemap"
Вот что генерируется на моей системе Linux, где sqlite3.h находится по адресу /usr/include, а исходные файлы проекта находятся в каталоге в моём домашнем каталоге.
Этого должно быть достаточно для импорта. Подводя итог, в нашем проекте есть четыре файла:
-
sqlite3.modulemapсообщает Swift, какие заголовочные файлы C связаны с каким импортированным модулем. -
sqlite-vfs-overlay.yamlсообщает Swift о необходимости вставки файла modulemap sqlite3 в нужное место для импорта без необходимости изменения системы. -
CMakeLists.txtорганизует настройку наложения VFS и сборку проекта. -
hello.swiftвызывает библиотеку C SQLite.
// sqlite3.modulemap
module CSQLite {
header "sqlite3.h"
}
# sqlite-vfs-overlay.yaml
---
version: 0
case-sensitive: false
use-external-names: false
roots:
- name: "@SQLite3_INCLUDE_DIR@"
type: directory
contents:
- name: module.modulemap
type: file
external-contents: "@SQLite3_MODULEMAP_FILE@"
# CMakeLists.txt
cmake_minimum_required(VERSION 3.26)
project(SQLiteImportExample LANGUAGES Swift C)
find_package(SQLite3 REQUIRED)
# Setup the VFS-overlay to inject the custom modulemap file
set(SQLite3_MODULEMAP_FILE "${CMAKE_CURRENT_SOURCE_DIR}/sqlite3.modulemap")
configure_file(sqlite-vfs-overlay.yaml
"${CMAKE_CURRENT_BINARY_DIR}/sqlite3-overlay.yaml")
target_compile_options(SQLite::SQLite3 INTERFACE
"$<$<COMPILE_LANGUAGE:Swift>:SHELL:-vfsoverlay ${CMAKE_CURRENT_BINARY_DIR}/sqlite3-overlay.yaml>")
add_executable(Hello hello.swift)
target_link_libraries(Hello PRIVATE SQLite::SQLite3)
// hello.swift
import CSQLite
public class Database {
var dbCon: OpaquePointer!
public struct Flags: OptionSet {
public let rawValue: Int32
public init(rawValue: Int32) {
self.rawValue = rawValue
}
public static let readonly = Flags(rawValue: SQLITE_OPEN_READONLY)
public static let readwrite = Flags(rawValue: SQLITE_OPEN_READWRITE)
public static let create = Flags(rawValue: SQLITE_OPEN_CREATE)
public static let deleteOnClose = Flags(rawValue: SQLITE_OPEN_DELETEONCLOSE)
}
public init?(filename: String, flags: Flags = [.create, .readwrite]) {
guard sqlite3_open_v2(filename, &dbCon, flags.rawValue, nil) == SQLITE_OK,
dbCon != nil else {
return nil
}
}
deinit {
sqlite3_close_v2(dbCon)
}
}
guard let database = Database(filename: ":memory:") else {
fatalError("Failed to load database for some reason")
}
Работа с C/C++
Управление жизненным циклом обернутого типа C/C++
При обёртке типа C/C++, у которого есть определённый жизненный цикл, такой как указанная инициализация и последующий вызов «разрушения», в Swift есть два способа подхода. Такая ситуация особенно распространена при обёртке типов C, у которых есть API для resource_init() и resource_destroy(the_resource).
Первый подход заключается в использовании класса Swift для обёртки ресурса и управления его жизненным циклом с помощью init/deinit класса. Вот пример обёртки объекта настроек C, управляемого RocksDB:
public final class WriteOptions {
let underlying: OpaquePointer!
public init() {
underlying = rocksdb_writeoptions_create()
}
deinit {
rocksdb_writeoptions_destroy(underlying)
}
}
Второй подход — использование типов «не копируемых» (которые вы можете знать как типы только перемещения из других языков). Чтобы объявить аналогичную WriteOptions оболочку с использованием некопируемого типа, можно сделать следующее:
public struct WriteOptions: ~Copyable {
let underlying: OpaquePointer!
public init() {
underlying = rocksdb_writeoptions_create()
}
deinit {
rocksdb_writeoptions_destroy(underlying)
}
}
Недостатком некопируемых типов является то, что в настоящее время они не могут быть использованы во всех контекстах. Например, в Swift 5.9 невозможно хранить некопируемый тип в качестве поля или передавать их через замыкания (так как замыкание может быть использовано многократно, что нарушит уникальность, необходимую для некопируемого типа).
Совместно внесён Эван Уайлд Конрад «ktoso» Малавский Конрад Малавский — член команды, разрабатывающей основополагающие серверные библиотеки Swift в Apple, с фокусом на распределённых системах и конкурентности. Иим Ли Иим Ли — инженер в команде Swift в Apple, разрабатывающий основополагающие серверные технологии с фокусом на распределённых системах.
Эван УайлдКонрад «ktoso» МалавскийИим Ли
The Swift Programming Language, Copyright © 2014-2025 Apple Inc.
Swift and the Swift logo are trademarks of Apple Inc.
Documentation for Swift 6.0.3
https://www.swift.org/documentation/articles/wrapping-c-cpp-library-in-swift.html