Непростая линковка Swift и C

от автора

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

Ну а чтобы покачаться в разработке под платформу самое лучше – написать какой-нить системный утиль, а тут Fugu14 выкатили поэтому я решил написать небольшую систему дампа фримвари для айфонов. И в качестве начала было решено переписать igetnonce на swift.

Почему swift? – ну неповторимый оригинал уже на Си, так что этот вариант отпадает, а красоту синтаксиса Objective-C  я чет так и не оценил.

Посмотрев как что нынче носят в swift  — я был крайне впечатлен концепцией пакетов и SPM – лаконичное описание для сборки проекта – это всегда приятно. По этой причине было решено реализовывать проект в виде пакета.

Чистый swift это конечно хорошо, однако igetnonce в качестве зависимостей тащит ряд Си-шных библиотек среди которых широко известная в кругах любителей jailbreak -ов libimobiledevice. С нее то и начались мои проблемы 🙂

Swift и Си-библиотеки

Но для начала давайте обсудим как вообще связать swift и Си-шную библиотеку. Толковой информации об этом в интернете не то чтобы много – могу порекомендовать эту и эту, однако и они не достаточно полно описывают то как это правильно сделать.

Но тут (внезапно) на помощь приходит официальная документация – которая гуглится через коленку, и содержит пару ошибок… Так что думаю ничего страшного не будет от того, что я тут продублирую шаги документации с некоторыми исправлениями.

Для работы с Си-шными библиотеками в swift требуется создать специальный пакет-обертку.

mkdir Clibimobiledevice  # конвенция именований в формате Clibname описана в официальной доке так что не будем ее нарушать cd Clibimobiledevice swift package init --type system-module  

В результате получаем следующую структуру файлов:

Clibimobiledevice     ├── Package.swift     ├── README.md     └── module.modulemap

Однако данная структура ошибочна, о чем расскажу чуть позже, сейчас давайте просто приведем ее к правильному виду.

Clibimobiledevice % mkdir -p ./Source/Clibimobiledevice Clibimobiledevice % mv module.modulemap ./Source/Clibimobiledevice

В результате имеем следующую структуру:

Clibimobiledevice     ├── Package.swift     ├── README.md     └── Source         └── Clibimobiledevice             └── module.modulemap

Так теперь следует отредактировать module.modulemap. Подробное описание формата modulemap приведено в официальной документации, но нам достаточно небольшого сабсета всех возможностей модулей, а именно нам нужно обьявить один системный модуль и выдернуть все содержимое заголовочных файлов libimobiledevice.

Получить путь до папки с заголовочными файлами можно с помощью команды brew --prefix libimobiledevice. В итоге module.modulemap будет иметь следующее содержимое:

Clibimobiledevice % cat > Source/Clibimobiledevice/module.modulemap module Clibimobiledevice [system] {   header "/usr/local/opt/libimobiledevice/include/libimobiledevice/libimobiledevice.h"   export * } ^D

Некоторые из озвученных далее проблем можно решить с помощью файла module.modulemap, однако это будут полумеры не до конца решающие проблему.

Теперь перейдем к файлу Package.swift. И отредактируем его следующим образом:

Clibimobiledevice % cat > Package.swift // swift-tools-version:5.5 // The swift-tools-version declares the minimum version of Swift required to build this package.  import PackageDescription  let package = Package(     name: "Clibimobiledevice",     products: [         .library(name: "Clibimobiledevice", targets: ["Clibimobiledevice"]),     ],     targets: [         .systemLibrary(             name: "Clibimobiledevice",  // path:             pkgConfig: "libimobiledevice-1.0",             providers: [                 .brew(["libimobiledevice"])             ]         )     ] ) ^D

Для взаимодействия с Си-шными библиотеками у SPM существует специальный таргет врапер – systemLibrary. Как можно увидеть из документации  — параметр path по умолчанию смотрит в [PackageRoot]/Sources/[TargetName] — как раз поэтому нам и пришлось изменить структуру каталогов проекта ранее.

Так же данный таргет опционально готов получить на вход источник пакетов – в нашем случае brew и имя (именно имя, без полного пути и без расширения) pkg-config-а используемой Си-шной библиотеки. Об этом конфиге и о том причем тут brew дальше и пойдет речь.

В целом наш врапер уже готов и теперь надо создать проект использующий его функциональность:

Clibimobiledevice % cd .. % mkdir foo % cd foo % swift package init --type executable

В результате получаем следующую структуру файлов:

foo ├── Package.swift ├── README.md ├── Sources │   └── foo │       └── main.swift └── Tests     └── fooTests         └── fooTests.swift

Отредактируем Package.swift, чтобы добавить в зависимости Clibimobiledevice:

foo % cat > Package.swift // swift-tools-version:5.5 // The swift-tools-version declares the minimum version of Swift required to build this package.  import PackageDescription  let package = Package(     name: "foo",     dependencies: [             .package(name: "Clibimobiledevice", path: "../Clibimobiledevice"),         ],     targets: [         .executableTarget(             name: "foo",             dependencies: [                 .product(name: "Clibimobiledevice", package: "Clibimobiledevice")             ]),         .testTarget(             name: "fooTests",             dependencies: ["foo"]),     ] ) ^D

И вызовем в main.swift какую-нибудь функцию libimobiledevice:

foo % cat > Sources/foo/main.swift import Clibimobiledevice  idevice_set_debug_level(1) ^D

И попробуем собрать что получилось:

foo % swift build warning: you may be able to install libimobiledevice-1.0 using your system-packager:     brew install libimobiledevice  Undefined symbols for architecture x86_64:   "_idevice_set_debug_level", referenced from:       _foo_main in main.swift.o ld: symbol(s) not found for architecture x86_64 [2/3] Linking foo

И так у нас на лицо проблема линковки, плюс странный варнинг о том, что мы не установили libimobiledevice. Что могло пойти не так? Может у нас какая-то проблема с версией библиотеки? Может у нас армовая версия? Проверим это:

foo % ARCH=x86_64 jtool2 -S /usr/local/opt/libimobiledevice/lib/libimobiledevice-1.0.dylib | grep idevice_set_debug_level 0000000000003e89 T _idevice_set_debug_level

Да нет – нужный символы на месте. Тогда попробуем руками указать линковщику где искать нужные символы

foo % swift build -Xlinker -L/usr/local/opt/libimobiledevice/lib -Xlinker -limobiledevice-1.0 warning: you may be able to install libimobiledevice-1.0 using your system-packager:     brew install libimobiledevice  ld: warning: dylib (/usr/local/opt/libimobiledevice/lib/libimobiledevice-1.0.dylib) was built for newer macOS version (12.0) than being linked (10.10) [1/1] Build complete!

Все собралось. Можно считать это победой – передать флаги через LinkerSetting.unsafeFlags и пойти пить чай, но это же не наши методы.

Помните функцию systemLibrary формирующую таргет для Си-шных библотек?

static func systemLibrary(   name: String,    path: String? = nil,    pkgConfig: String? = nil,    providers: [SystemPackageProvider]? = nil ) -> Target

Нам интересен ее параметр pkg-config. Что такое pkg-config? если коротко – это утилита определяющая формат в котором библиотеки указывают необходимые для их сборки зависимости и флаги компиляции. Файлы для pkg-config имеют расширение .pc.

Мы в качестве такого файла указали libimobiledevice-1.0. Давайте взглянем на него чтобы немного освежить/познакомиться с форматом:

foo % cat /usr/local/opt/libimobiledevice/lib/pkgconfig/libimobiledevice-1.0.pc # объвляются константы сокращающие запись prefix=/usr/local/Cellar/libimobiledevice/1.3.0 exec_prefix=${prefix} libdir=${exec_prefix}/lib includedir=${prefix}/include  Name: libimobiledevice Description: A library to communicate with services running on Apple iOS devices. Version: 1.3.0 Libs: -L${libdir} -limobiledevice-1.0# флаги для ld Cflags: -I${includedir}# флаги для копилятора Requires: libplist-2.0 >= 2.2.0# зависимости Requires.private: libusbmuxd-2.0 >= 2.0.2 openssl >= 0.9.8

Как видим из конфига – флаги для ld аналогичны тем что использовали мы для успешной сборки и по замыслу лежащему в основе pkg-config SPM должен был сам вытащить эти флаги из конфига и подставить куда надо. А раз он этого не сделал то что-то пошло не так, да и варнинг выведенный при сборки только закрепляет мысль о том, что SPM не обработал наш .pc файл.

Таким образом возникает резонный вопрос где SPM ищет .pc файлы?

Для поиска ответа пришлось идти в исходники SPM. После некоторого времени, потраченного на поиски стало ясно что за обработку pkg-config-ов отвечает (ВНИМАНИЕ!) PkgConfig.swift, а за поиск – расположенная в нем структура PCFileFinder, в особенности функция locatePCFile.

Строчка 417 показывает все источники путей используемые SPM для поиска .pc файлов. А именно:

  • PCFileFinder.searchPaths – константа заданная в структуре

    • /usr/local/lib/pkgconfig

    • /usr/local/share/pkgconfig

    • /usr/lib/pkgconfig

    • /usr/share/pkgconfig

  • PCFileFinder.pkgConfigPaths – является результатом выполнения команды pkg-config --variable pc_path pkg-config и на моей системе имело следующее содержимое:

    • /usr/local/lib/pkgconfig:/usr/local/share/pkgconfig

    • /usr/lib/pkgconfig

    • /usr/local/Homebrew/Library/Homebrew/os/mac/pkgconfig/10.15

  • customSearchPaths – складывается из содержимого переменной окружения PKG_CONFIG_PATH и внешнего аргумента additionalSearchPaths который в случай использования brew будет содержать /usr/local/opt/(NAME)/lib/pkgconfig см тут и тут

    • PKG_CONFIG_PATH

    • /usr/local/opt/(NAME)/lib/pkgconfig

Исходя из собранных путей становиться ясно, что libimobiledevice-1.0.pc должен был быть найден еще на по пути /usr/local/lib/pkgconfig

foo % file /usr/local/lib/pkgconfig/libimobiledevice-1.0.pc /usr/local/lib/pkgconfig/libimobiledevice-1.0.pc: ASCII text

Но что же тогда пошло не так? Для того чтобы разобраться в этом было решено создать небольшой проект, который создаст экземпляр PkgConfig напрямую.

foo % cd .. % mkdir spm_test % cd spm_test spm_test % swift package init --type executable spm_test % cat > Package.swift // swift-tools-version:5.5 // The swift-tools-version declares the minimum version of Swift required to build this package.  import PackageDescription  let package = Package(     name: "spm_test",     platforms: [         .macOS("10.15.4")     ],     dependencies: [         // Dependencies declare other packages that this package depends on.         .package(name: "SwiftPM", url: "https://github.com/apple/swift-package-manager.git", .revision("658654765f5a7dfb3456c37dafd3ed8cd8b363b4"))     ],     targets: [         // Targets are the basic building blocks of a package. A target can define a module or a test suite.         // Targets can depend on other targets in this package, and on products in packages this package depends on.         .executableTarget(             name: "spm_test",             dependencies: [                 "SwiftPM"             ])     ] ) ^D  spm_test %cat > Sources/spm_test/main.swift import Basics import PackageLoading import PackageModel import TSCBasic  typealias Diagnostic = Basics.Diagnostic  // Подспер из тестов spm ))) struct Collector: ObservabilityHandlerProvider, DiagnosticsHandler {     private let _diagnostics = ThreadSafeArrayStore<Diagnostic>()      var diagnosticsHandler: DiagnosticsHandler { self }      var diagnostics: [Diagnostic] {         self._diagnostics.get()     }      func clear() {         self._diagnostics.clear()     }      func handleDiagnostic(scope: ObservabilityScope, diagnostic: Diagnostic) {         self._diagnostics.append(diagnostic)     } }  let collector = Collector() let observabilitySystem = ObservabilitySystem(collector)  let observability = observabilitySystem.topScope.makeChildScope(description: "test") let result = try PkgConfig(name: "libimobiledevice-1.0", additionalSearchPaths: [], fileSystem: localFileSystem, observabilityScope: observability) print(result)  ^D

Собираем и запускаем:

spm_test % ./.build/x86_64-apple-macosx/debug/spm_test ... [1086/1086] Build complete! spm_test % ./.build/x86_64-apple-macosx/debug/spm_test Swift/ErrorType.swift:200: Fatal error:  Error raised at top level: couldn't find pc file for openssl zsh: illegal hardware instruction  ./.build/x86_64-apple-macosx/debug/spm_test

Иииии вот она ошибка! Проблема в том что мы не можем найти .pc для openssl. Openssl действительно находился в списке зависимостей для libimobiledevice. Получается что PkgConfig рекурсивно ищет и разбирает .pc файлы для всех зависимостей и если с одной из них произойдет какая-то проблема то никаких внятных сообщений об ошибках в консоли не появиться, а только бесполезный варнинг о том что исходный пакет не установлен.

Попробуем установить openssl через brew:

spm_test % brew install openssl Running `brew update --preinstall`... …  openssl@3 is keg-only, which means it was not symlinked into /usr/local, because macOS provides LibreSSL.  If you need to have openssl@3 first in your PATH, run:   echo 'export PATH="/usr/local/opt/openssl@3/bin:$PATH"' >> ~/.zshrc  For compilers to find openssl@3 you may need to set:   export LDFLAGS="-L/usr/local/opt/openssl@3/lib"   export CPPFLAGS="-I/usr/local/opt/openssl@3/include"  For pkg-config to find openssl@3 you may need to set:   export PKG_CONFIG_PATH="/usr/local/opt/openssl@3/lib/pkgconfig"  …

Как видно из логов brew установка нам не сильно поможет, так как brew не создает линков на необходимые нам .pc файлы. Тут есть два варианта:

  • создать линки самому – но это потребует аналогичных манипуляций при использовании пакета на другой машине, что гемор

  • использовать PKG_CONFIG_PATH – этот вариант очевидно более гуманный если мы сможем прописать это в коде

Изменим наш тестовый проект:

spm_test %cat > Sources/spm_test/main.swift import Basics import Basics import PackageLoading import PackageModel import TSCBasic import Foundation  typealias Diagnostic = Basics.Diagnostic  // Подспер из тестов spm ))) struct Collector: ObservabilityHandlerProvider, DiagnosticsHandler {     private let _diagnostics = ThreadSafeArrayStore<Diagnostic>()      var diagnosticsHandler: DiagnosticsHandler { self }      var diagnostics: [Diagnostic] {         self._diagnostics.get()     }      func clear() {         self._diagnostics.clear()     }      func handleDiagnostic(scope: ObservabilityScope, diagnostic: Diagnostic) {         self._diagnostics.append(diagnostic)     } }  let collector = Collector() let observabilitySystem = ObservabilitySystem(collector)  let observability = observabilitySystem.topScope.makeChildScope(description: "test")  let pkg_config_path_env = "PKG_CONFIG_PATH"  var pkg_config_path = "/usr/local/opt/openssl@3/lib/pkgconfig" if let current_pkg_config_path = ProcessInfo.processInfo.environment[pkg_config_path_env] {     pkg_config_path = current_pkg_config_path + ":" + pkg_config_path }  setenv(pkg_config_path_env, pkg_config_path, 1)  let result = try PkgConfig(name: "libimobiledevice-1.0", additionalSearchPaths: [], fileSystem: localFileSystem, observabilityScope: observability) print(result)  ^D

Соберем и запустим:

spm_test % swift build [3/3] Build complete! spm_test % ./.build/x86_64-apple-macosx/debug/spm_test PkgConfig(name: "libimobiledevice-1.0", pcFile: <AbsolutePath:"/usr/local/lib/pkgconfig/libimobiledevice-1.0.pc">, cFlags: ["-I/usr/local/Cellar/libimobiledevice/1.3.0/include", "-I/usr/local/Cellar/libplist/2.2.0/include", "-I/usr/local/Cellar/libusbmuxd/2.0.2/include", "-I/usr/local/Cellar/libplist/2.2.0/include", "-I/usr/local/Cellar/openssl@3/3.0.1/include", "-I/usr/local/Cellar/openssl@3/3.0.1/include", "-I/usr/local/Cellar/openssl@3/3.0.1/include"], libs: ["-L/usr/local/Cellar/libimobiledevice/1.3.0/lib", "-limobiledevice-1.0", "-L/usr/local/Cellar/libplist/2.2.0/lib", "-lplist-2.0"])

БИНГО!!! Осталось реализовать аналогичную логику для пакета. Ииии это оказалось невозможно. Нет, правильнее сказать – я так и не понял как можно в пакете установить переменную окружения. Если кто-то знает как – буду рад такой информации.

Таким образом у нас остается только один путь:

spm_test % cat /usr/local/Cellar/openssl@3/3.0.1/lib/pkgconfig/openssl.pc prefix=/usr/local/Cellar/openssl@3/3.0.1 exec_prefix=${prefix} libdir=/usr/local/Cellar/openssl@3/3.0.1/lib includedir=${prefix}/include  Name: OpenSSL Description: Secure Sockets Layer and cryptography libraries and tools Version: 3.0.1 Requires: libssl libcrypto spm_test % ln  /usr/local/Cellar/openssl@3/3.0.1/lib/pkgconfig/openssl.pc /usr/local/lib/pkgconfig/openssl.pc spm_test % ln  /usr/local/Cellar/openssl@3/3.0.1/lib/pkgconfig/libcrypto.pc /usr/local/lib/pkgconfig/libcrypto.pc spm_test % ln  /usr/local/Cellar/openssl@3/3.0.1/lib/pkgconfig/libssl.pc /usr/local/lib/pkgconfig/libssl.pc spm_test % cd ../foo foo % swift build [0/0] Build complete!

Надеюсь данный материал поможет другим быстрее разобраться с проблемами линковки Си и Swift.


ссылка на оригинал статьи https://habr.com/ru/post/651885/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *