Создаем Swift Package на основе C++ библиотеки

Автор оригинала: Константин Семьянов
Фото Kira auf der Heide на Unsplash
Фото Kira auf der Heide на Unsplash

Данная статья поможет вам создать свой первый Swift Package. Мы воспользуемся популярной C++ библиотекой для линейной алгебры Eigen, чтобы продемонстрировать, как можно обращаться к ней из Swift. Для простоты, мы портируем только часть возможностей Eigen.


Трудности взаимодействия C++ и Swift

Использование C++ кода из Swift в общем случае достаточно трудная задача. Все сильно зависит от того, какой именно код вы хотите портировать. Данные 2 языка не имеют соответствия API один-к-одному. Для подмножества языка C++ существуют автоматические генераторы Swift интерфейса (например, ScapixGluecodium). Они могут помочь вам, если разрабатывая библиотеку, вы готовы следовать некоторым ограничениям, чтобы ваш код просто конвертировался в другие языки. Тем не менее, если вы хотите портировать чужую библиотеку, то, как правило, это будет не просто. В таких ситуациях зачастую ваш единственный выбор: написать обертку вручную.

Команда Swift уже предоставляет interop для C и Objective-C в их инструментарии. В то же время, C++ interop только запланирован и не имеет четких временных рамок по реализации. Одна из сложно портируемых возможностей C++ – шаблоны. Может показаться, что темплейты в C++ и дженерики в Swift схожи. Тем не менее, у них есть важные отличия. На момент написания данной статьи, Swift не поддерживает параметры шаблона не являющиеся типом, template template параметры и variadic параметры. Также, дженерики в Swift определяются для типов параметров, которые соблюдают объявленные ограничения (похоже на C++20 concepts). Также, в C++ шаблоны подставляют конкретный тип в месте вызова шаблона и проверяют поддерживает ли тип используемый синтаксис внутри шаблона.

Итого, если вам нужно портировать C++ библиотеку с обилием шаблонов, то ожидайте сложностей!


Постановка задачи

Давайте попробуем портировать вручную С++ библиотеку Eigen, в которой активно используются шаблоны. Эта популярная библиотека для линейной алгебры содержит определения для матриц, векторов и численных алгоритмов над ними. Базовой стратегией нашей обертки будет: выбрать конкретный тип, обернуть его в Objective-C класс, который будет импортироваться в Swift.

Один из способов импортировать Objective-C API в Swift – это добавить C++ библиотеку напрямую в Xcode проект и написать bridging header. Тем не менее, обычно удобнее, когда обертка компилируется в качестве отдельного модуля. В этом случае, вам понадобится помощь менеджера пакетов. Команда Swift активно продвигает Swift Package Manager (SPM). Исторически, в SPM отсутствовали некоторые важные возможности, из-за чего многие разработчики не могли перейти на него. Однако, SPM активно улучшался с момента его создания. В Xcode 12, вы можете добавлять в пакет произвольные ресурсы и даже попробовать пакет в Swift playground.

В данной статье мы создадим SPM пакет SwiftyEigen. В качестве конкретного типа мы возьмем вещественную float матрицу с произвольным числом строк и колонок. Класс Matrix будет иметь конструктор, индексатор и метод вычисляющий обратную матрицу. Полный проект можно найти на GitHub.


Структура проекта

SPM имеет удобный шаблон для создания новой библиотеки:

foo@bar:~$ mkdir SwiftyEigen && cd SwiftyEigen
foo@bar:~/SwiftyEigen$ swift package init
foo@bar:~/SwiftyEigen$ git init && git add . && git commit -m 'Initial commit'

Далее, мы добавляем стороннюю библиотеку (Eigen) в качестве сабмодуля:

foo@bar:~/SwiftyEigen$ git submodule add https://gitlab.com/libeigen/eigen Sources/CPP
foo@bar:~/SwiftyEigen$ cd Sources/CPP && git checkout 3.3.9

Отредактируем манифест нашего пакета, Package.swift:

// swift-tools-version:5.3
import PackageDescription

let package = Package(
    name: "SwiftyEigen",
    products: [
        .library(
            name: "SwiftyEigen",
            targets: ["ObjCEigen", "SwiftyEigen"]
        )
    ],
    dependencies: [],
    targets: [
        .target(
            name: "ObjCEigen",
            path: "Sources/ObjC",
            cxxSettings: [
                .headerSearchPath("../CPP/"),
                .define("EIGEN_MPL2_ONLY")
            ]
        ),
        .target(
            name: "SwiftyEigen",
            dependencies: ["ObjCEigen"],
            path: "Sources/Swift"
        )
    ]
)

Манифест является рецептом для компиляции пакета. Сборочная система Swift соберет два отдельных таргета для Objective-C и Swift кода. SPM не позволяет смешивать несколько языков в одном таргете. Таргет ObjCEigen использует файлы из папки Sources/ObjC, добавляет папку Sources/CPP в header search paths, и опеделяет EIGEN_MPL2_ONLY, чтобы гарантировать лицензию MPL2 при использовании Eigen. Таргет SwiftyEigen зависит от ObjCEigen и использует файлы из папки Sources/Swift.


Ручная обертка

Теперь напишем заголовочный файл для Objective-C класса в папке Sources/ObjCEigen/include:

#pragma once

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface EIGMatrix: NSObject

@property (readonly) ptrdiff_t rows;
@property (readonly) ptrdiff_t cols;

- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)matrixWithZeros:(ptrdiff_t)rows cols:(ptrdiff_t)cols
NS_SWIFT_NAME(zeros(rows:cols:));
+ (instancetype)matrixWithIdentity:(ptrdiff_t)rows cols:(ptrdiff_t)cols
NS_SWIFT_NAME(identity(rows:cols:));

- (float)valueAtRow:(ptrdiff_t)row col:(ptrdiff_t)col
NS_SWIFT_NAME(value(row:col:));
- (void)setValue:(float)value row:(ptrdiff_t)row col:(ptrdiff_t)col
NS_SWIFT_NAME(setValue(_:row:col:));

- (EIGMatrix*)inverse;

@end

NS_ASSUME_NONNULL_END

У класса есть readonly свойства rows и cols, конструктор для нулевой и единичной матрицы, способы получить и изменить отдельные значения, и метод вычисления обратной матрицы.

Дальше напишем файл реализации в Sources/ObjCEigen:

#import "EIGMatrix.h"

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdocumentation"
#import <Eigen/Dense>
#pragma clang diagnostic pop

#import <iostream>

using Matrix = Eigen::Matrix<float, Eigen::Dynamic, Eigen::Dynamic>;
using Map = Eigen::Map<Matrix>;

@interface EIGMatrix ()

@property (readonly) Matrix matrix;

- (instancetype)initWithMatrix:(Matrix)matrix;

@end

@implementation EIGMatrix

- (instancetype)initWithMatrix:(Matrix)matrix {
    self = [super init];
    _matrix = matrix;
    return self;
}

- (ptrdiff_t)rows {
    return _matrix.rows();
}

- (ptrdiff_t)cols {
    return _matrix.cols();
}

+ (instancetype)matrixWithZeros:(ptrdiff_t)rows cols:(ptrdiff_t)cols {
    return [[EIGMatrix alloc] initWithMatrix:Matrix::Zero(rows, cols)];
}

+ (instancetype)matrixWithIdentity:(ptrdiff_t)rows cols:(ptrdiff_t)cols {
    return [[EIGMatrix alloc] initWithMatrix:Matrix::Identity(rows, cols)];
}

- (float)valueAtRow:(ptrdiff_t)row col:(ptrdiff_t)col {
    return _matrix(row, col);
}

- (void)setValue:(float)value row:(ptrdiff_t)row col:(ptrdiff_t)col {
    _matrix(row, col) = value;
}

- (instancetype)inverse {
    const Matrix result = _matrix.inverse();
    return [[EIGMatrix alloc] initWithMatrix:result];
}

- (NSString*)description {
    std::stringstream buffer;
    buffer << _matrix;
    const std::string string = buffer.str();
    return [NSString stringWithUTF8String:string.c_str()];
}

@end

Теперь сделаем Objective-C код видимым из Swift с помощью файла в Sources/Swift (смотрите Swift Forums):

@_exported import ObjCEigen

И добавим индексирование для более чистого API:

extension EIGMatrix {
    public subscript(row: Int, col: Int) -> Float {
        get { return value(row: row, col: col) }
        set { setValue(newValue, row: row, col: col) }
    }
}

Пример использования

Теперь мы можем воспользоваться классом вот так:

import SwiftyEigen

// Create a new 3x3 identity matrix
let matrix = EIGMatrix.identity(rows: 3, cols: 3)

// Change a specific value
let row = 0
let col = 1
matrix[row, col] = -2

// Calculate the inverse of a matrix
let inverseMatrix = matrix.inverse()

Наконец, мы можем составить простой проект, который продемонстрирует возможности нашего пакета, SwiftyEigen. Приложение позволит вносить значения в матрицу 2x2 и вычислять обратную матрицу. Для этого, создаем новый iOS проект в Xcode, перетаскиваем папку с пакетом из Finder в project navigator, чтобы добавить локальную зависимость, и добавляем фреймворк SwiftyEigen в общие настройки проекта. Далее пишем UI и радуемся:

Смотрите полный проект на GitHub.


Ссылки

Спасибо за внимание!

Средняя зарплата в IT

113 000 ₽/мес.
Средняя зарплата по всем IT-специализациям на основании 10 037 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
Реклама
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее

Комментарии 2

    0
    Итого, если вам нужно портировать C++ библиотеку с обилием шаблонов, то ожидайте сложностей!

    Не очень понятно каких. Сейчас Cmake умеет в iOS toolchain, можно собрать статическую библиотеку и далее обернуть ее в Swift, как динамический framework. Можно даже написать скрипт, который сделает fat статическую библиотеку для работы как на устройстве, так и на симуляторе.
    В статье, как я понял, речь о header only части библиотеки.
      +1
      Безусловно, саму C++ библиотеку можно собрать и с помощью CMake под iOS. Здесь используются стандартные для экосистемы Apple сборочные инструменты, входящие в Xcode. Они тоже позволяют создавать, как динамический, так и статический фреймворк. На смену fat в Xcode 11, наконец, появился официальный способ сделать фреймворк под несколько архитектур XCFramework developer.apple.com/videos/play/wwdc2019/416

      При разработке под платформы Apple в большинстве случаев используется официальный SDK. Обычно библиотеки этого SDK имеют интерфейс для Objective-C и Swift, поэтому большинство проектов берут один из этих языков за основной. Здесь под портированием подразумевалось в том числе создание Swift интерфейса к библиотеке. И сгенерировать Swift интерфейс автоматически в общем случае не получится. В статье демонстрируется один из способов, как это сделать вручную.

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое