Как стать автором
Обновить

Swift и Си: туда и обратно

Время на прочтение 3 мин
Количество просмотров 3.7K
Всем привет!

Однажды мне поручили задачу под iOS — VPN-client со специфической криптографией.

Криптография в нашей компании традиционно своя, есть готовая реализация на Си.

В этой статье я расскажу, как мне удалось подружить Си и Swift.

Для наглядности в качестве примера напишем простую функцию преобразования строки на Си и вызовем ее из Swift.

Основная сложность в таких задачах — это передача параметров и возвращаемые значения. О них и поговорим. Пусть у нас есть функция:

uint8_t* flipString(uint8_t* str, int strlen){
  uint8_t* result = malloc(strlen);
  int i;
  int j=0;
  for(i = strlen-1; i>=0; --i){
      result[j] = str[i];
      j++;
  }
  return result;
}

Функция принимает указатель на массив байт для реверса и длину строки. Возвращаем мы указатель на полученный массив байт. В уме держим malloc. Пробегаемся-записываем-возвращаем.

Создаем MyCFile.h с заголовком для нашей функции. Добавляем Bridging-Header.h, в котором подключен этот самый MyCFile.h.

Далее приведения типов. Начнем с простого — int у нас это Int32. Потом указатели. Указателей в swift несколько. Нас интересует UnsafeMutablePointer.

let str = "qwerty"
var array: [UInt8] = Array(str.utf8)
let stringPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: array.count)
stringPointer.initialize(from: &array, count: array.count)
guard let res = flipString(stringPointer, Int32(array.count)) else {return}

Создаем массив UInt8(так то это один байт) из данной строки. Создаем указатель на данные определенного размера. Указываем. Вызываем, смотрим что не nil.

И если с передаваемым указателем все вроде бы просто, то res у нас на текущий момент имеет тип UnsafeMutablePointer?. В пару кликов, минуя StackOverflow, находим свойство указателя pointee. При помощи этого свойства можно получить доступ к памяти по этому указателю. Пробуем развернуть слово «qwerty» используя это свойство, а там… Бадум-тс… «121». Ладно, барабанная дробь — лишняя, но результат не тот который хотелось бы получить.

Хотя, если подумать, все логично. В Swift наш res(который вернула си функция) это указатель на массив, состоящий из Int8. Указатель издревле указывает на первый элемент массива. Так-так-так. Лезем в таблицу ASKII. 121 это код буквы 'y'. Совпадение? Не думаю. Один символ считали.

Далее, по старой сишной традиции, можно идти по массиву, смещая указатель, и получать следующие байты:

let p = res+1
print(p.pointee)

Так получаем 116, что есть код 't'.

Теоретически так можно идти и дальше, если знаешь размер выделенной памяти. А память эта выделяется внутри Си кода.

В нашем случаи проблем нет, а вот в чуть более серьезных программах придется повозиться. Чем я и занялся.

Решение пришло ко мне в виде старых добрых структур из Си.

План следующий: создаем структуру, копируем в нее перевернутую строку и размер в соответствующие поля, возвращаем указатель на это структуру.

struct FlipedStringStructure {
    void *result;
    int resultSize;
};

Функцию перепишем вот в такой вид:

struct FlipedStringStructure* flipStringToStruct(uint8_t* str, int strlen){
    uint8_t* result = malloc(strlen);
    int i;
    int j=0;
    for(i = strlen-1; i>=0; --i){
        result[j] = str[i];
        j++;
    }
    struct FlipedStringStructure* structure;
    structure = malloc(sizeof(struct FlipedStringStructure));
    structure->resultSize=j;
    structure->result = malloc(j);
    memcpy(structure->result,result,j);
    free(result);
    return structure;
}

Отмечаем, что память мы выделяем и под структуру и под строку.

Что ж — осталось переписать вызов. Следим за руками.


func flip(str:String)->String?{
    var array: [UInt8] = Array(str.utf8)
    let stringPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: array.count)
    stringPointer.initialize(from: &array, count: array.count)

    let structPointer = flipStringToStruct(stringPointer, Int32(array.count))
    guard structPointer != nil else{return nil}
    let tmp = structPointer!.pointee
    let res = Data(bytes: tmp.result, count: Int(tmp.resultSize))
    let resStr = String(decoding: res, as: UTF8.self)
    freeMemmory(tmp.result)
    freeSMemmory(structPointer)
    return resStr
}

Мы все так же используем свойство pointee, но теперь мы получаем первый элемент типа структуры, созданной нами в си коде. Прелесть идеи в том, что мы можем обратиться к типу данных, объявленному в си части кода без лишнего приведения типов. Первую часть уже разбирали. Дальше по шагам: Получаем указатель на заполненную в си структуру(structPointer).

Получаем доступ к памяти этой структуры. В структуре есть данные и размер данных.

Обращаться к ним можно как к полям структуры созданной в swift (через точку).

Собираем из этого массив байт (Data), который декодируем в String. Ну и не забываем прибраться за собой. Создаем 2 функции:


void freeMemmory(void* s){
    free(s);
}
void freeSMemmory(struct FlipedStringStructure* s){
    free(s);
}

Когда эти функции вызываются из Swift, параметрами мы в них передаем либо полученные указатели, либо указатели из структуры.

freeMemmory(tmp.result)
freeSMemmory(structPointer)

И вуаля — дело в шляпе!

Ничего нового в таком подходе, конечно же нет, однако он позволяет активно работать с кроссплатформенными функциями и довольно удобен.

Спасибо тем кто дочитал.

Ссылка на проект в git — тут
EOF
Теги:
Хабы:
+1
Комментарии 9
Комментарии Комментарии 9

Публикации

Истории

Работа

Swift разработчик
38 вакансий
Программист С
43 вакансии
iOS разработчик
23 вакансии

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн