четверг, 10 января 2019 г.

[Перевод] Как встроить С-библиотеку в Swift-фреймворк

В 2014 году был представлен Swift, новый язык для разработки приложений экосистемы Apple. Новинка принесла не только новые возможности и функции, но и проблемы — тем, кто хотел пользоваться старыми добрыми C-библиотеками. В этой статье я рассмотрю одну из них — бандлинг C-библиотеки в Swift-фреймворк. Существует несколько способов её решения; в данном случае я объясню, как сделать это при помощи clang explicit-модулей.

Для примера мы возьмём внешнюю C-библиотеку libgif и встроим её в наш Swift-фреймворк GifSwift. Если вы хотите сразу увидеть результат, полностью проект можно посмотреть здесь.


Прежде чем внедрять в наш проект библиотеку libgif, её нужно собрать из исходников.
  1. Скачиваем последнюю версию тарболла здесь.
  2. Распаковываем архив, при помощи консоли заходим в папку и запускаем:
    ./configure && make check
    

    Примечание: для простоты мы собираем библиотеку для платформы x86-64, а потому она будет работать только в iOS-симуляторе или на macOS. Построение мультиархитектурной статической библиотеки – отдельная тема, которой я не касаюсь в этой статье. Полезные инструкции вы найдёте здесь.
  3. Если всё пройдёт без ошибок, файлы библиотеки можно будет найти в ${lib_gif_source}/lib/.libs. Нас интересуют два файла:
    lib/.libs/libgif.a # Статическая библиотека
    lib/gif_lib.h # Интерфейс
    
    

Теперь настроим проект под наши нужды.
  1. Создаём новый проект при помощи шаблона Cocoa Touch Framework, даём ему имя GifSwift.
  2. Добавляем созданные нами файлы библиотеки libgif в отдельную группу внутри проекта.
  3. Добавляем в проект новый таргет для тестового приложения, чтобы посмотреть результат.

Итоговая структура проекта должна выглядеть примерно так:


Для того чтобы импортировать C-библиотеку в Swift, мы должны описать её как модуль. Описание представляет собой файл .modulemap, содержащий список заголовочных файлов для импорта и статических библиотек для линковки. Полученный модуль может быть импортирован в Swift или Objective-C-код (при помощи @import).

Этот способ импорта библиотеки во фреймворк будет работать в большинстве случаев (более подробно об этом подходе читайте здесь). Он отлично подходит, если вы создаёте внутренний фреймворк или просто разбиваете своё приложение на модули. Но такой способ также имеет и недостатки. Например, он неэффективен в том случае, если вы хотите передать свою библиотеку кому-то при помощи Carthage, Cocoapods или в виду бинарного артефакта. Причина в том, что получившийся фреймворк в общем случае не портируем, поскольку при компиляции он привязывается к конкретному расположению заголовочных файлов и библиотек из module map на вашем компьютере.


Чтобы обойти эти ограничения, воспользуемся ещё одним способом — explicit-модулем для библиотеки. Еxplicit-модуль — это модуль, который объявляется подмодулем при помощи ключевого слова explicit, помещается в родительский модуль и не импортируется автоматически. Он работает аналогично *_Private.h для фреймворков Objective-C. Если вы хотите использовать объявленные в нём API, необходимо импортировать модуль явно (explicitly).

Мы создаём явный модуль для C-библиотеки внутри фреймворка. Для этого нам нужно провести переопределение сгенерированного XCode-модуля. Также обратите внимание на то, что мы не указываем библиотеку libgif.a для линковки (link gif), а вместо этого сделаем это прямо в проекте, используя интерфейс XCode.

Примечание: узнать больше об explicit-модулях можно по ссылке

  1. Добавляем в корневую папку проекта файл с названием GifSwift.modulemap:
    framework module GifSwift {
        umbrella header "GifSwift.h"
    
        explicit module CLibgif {
            private header "gif_lib.h"
        }
    
        export *
    }
    
    

    Этот файл содержит спецификацию для явного модуля CLibgif и состоит из одного объявленного заголовочного файла (поскольку в нашей библиотеке как раз один такой). Файл загружается в получившийся модуль для фреймворка.
  2. Файл с описанием модуля не нужно добавлять в состав фреймворка, но он должен быть указан в настройках таргета:
    Build Settings — Packaging — Module Map (MODULEMAP_FILE)
    =
    $SRCROOT/GifSwift/GifSwift.modulemap
    
  3. libgif-файлы должны быть добавлены в таргет фреймворка в виде приватного хедера (gif_lib.h) и статической библиотеки (libgif.a). Обратите внимание на то, что заголовочный файл для C-библиотеки добавлен в таргет как private. Это необходимо для нашего explicit-модуля. Ничто не мешает добавить этот заголовочный файл как public, но наша задача – скрыть детали реализации как можно более простыми средствами.


  4. Теперь можно импортировать явный модуль внутри фреймворка при помощи import GifSwift.CLibgif

Теперь можно заняться интерфейсом нашего фреймворка. Достаточно одного класса, представляющего собой гифку с парой свойств:
import Foundation
import GifSwift.CLibgif

public class GifFile {

    private let path: URL
    private let fileHandlePtr: UnsafeMutablePointer<GifFileType>
    private var fileHandle: GifFileType {
        return self.fileHandlePtr.pointee
    }

    deinit {
        DGifCloseFile(self.fileHandlePtr, nil)
    }

    // MARK: - API
    public init?(path: URL) {
        self.path = path

        let errorCode = UnsafeMutablePointer<Int32>.allocate(capacity: 1)
        if let handle = path.path.withCString({ DGifOpenFileName($0, errorCode) }) {
            self.fileHandlePtr = handle
            DGifSlurp(handle)
        } else {
            debugPrint("Error opening file \(errorCode.pointee)")
            return nil
        }
    }

    public var size: CGSize {
        return CGSize(width: Double(fileHandle.SWidth), height: Double(fileHandle.SHeight))
    }

    public var imagesCount: Int {
        return Int(fileHandle.ImageCount)
    }
}

GifFile.swift оборачивает низкоуровневые программные интерфейсы для обработки файлов и получает доступ к некоторым свойствам, отображая их на более удобные типы Foundation.
Для того чтобы протестировать нашу библиотеку, я добавил в проект файл cat.gif:
import UIKit
import GifSwift

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        if let file = GifFile(path: Bundle.main.url(forResource: "cat", withExtension: "gif")!) {
            debugPrint("Image has size: \(file.size) and contains \(file.imagesCount) images")
        }
    }
}

При запуске этого кода в консоли мы увидим следующее:

"Image has size: (250.0, 208.0) and contains 44 images"


Получившийся фреймворк содержит всё необходимое для использования, имеет Swift-интерфейс и по умолчанию скрывает C-код от клиентов. Впрочем, это не совсем правда. Как я писал выше, импортируя GifSwift.CLibgif, вы получите доступ ко всем закрытым модулям, однако по умолчанию такого метода инкапсуляции достаточно, чтобы скрыть детали реализации фреймворка.

Let's block ads! (Why?)

Комментариев нет:

Отправить комментарий