Всем привет!
Однажды мне поручили задачу под iOS — VPN-client со специфической криптографией.
Криптография в нашей компании традиционно своя, есть готовая реализация на Си.
В этой статье я расскажу, как мне удалось подружить Си и Swift.
Для наглядности в качестве примера напишем простую функцию преобразования строки на Си и вызовем ее из Swift.
Основная сложность в таких задачах — это передача параметров и возвращаемые значения. О них и поговорим. Пусть у нас есть функция:
Функция принимает указатель на массив байт для реверса и длину строки. Возвращаем мы указатель на полученный массив байт. В уме держим malloc. Пробегаемся-записываем-возвращаем.
Создаем MyCFile.h с заголовком для нашей функции. Добавляем Bridging-Header.h, в котором подключен этот самый MyCFile.h.
Далее приведения типов. Начнем с простого — int у нас это Int32. Потом указатели. Указателей в swift несколько. Нас интересует UnsafeMutablePointer.
Создаем массив UInt8(так то это один байт) из данной строки. Создаем указатель на данные определенного размера. Указываем. Вызываем, смотрим что не nil.
И если с передаваемым указателем все вроде бы просто, то res у нас на текущий момент имеет тип UnsafeMutablePointer?. В пару кликов, минуя StackOverflow, находим свойство указателя pointee. При помощи этого свойства можно получить доступ к памяти по этому указателю. Пробуем развернуть слово «qwerty» используя это свойство, а там… Бадум-тс… «121». Ладно, барабанная дробь — лишняя, но результат не тот который хотелось бы получить.
Хотя, если подумать, все логично. В Swift наш res(который вернула си функция) это указатель на массив, состоящий из Int8. Указатель издревле указывает на первый элемент массива. Так-так-так. Лезем в таблицу ASKII. 121 это код буквы 'y'. Совпадение? Не думаю. Один символ считали.
Далее, по старой сишной традиции, можно идти по массиву, смещая указатель, и получать следующие байты:
Так получаем 116, что есть код 't'.
Теоретически так можно идти и дальше, если знаешь размер выделенной памяти. А память эта выделяется внутри Си кода.
В нашем случаи проблем нет, а вот в чуть более серьезных программах придется повозиться. Чем я и занялся.
Решение пришло ко мне в виде старых добрых структур из Си.
План следующий: создаем структуру, копируем в нее перевернутую строку и размер в соответствующие поля, возвращаем указатель на это структуру.
Функцию перепишем вот в такой вид:
Отмечаем, что память мы выделяем и под структуру и под строку.
Что ж — осталось переписать вызов. Следим за руками.
Мы все так же используем свойство pointee, но теперь мы получаем первый элемент типа структуры, созданной нами в си коде. Прелесть идеи в том, что мы можем обратиться к типу данных, объявленному в си части кода без лишнего приведения типов. Первую часть уже разбирали. Дальше по шагам: Получаем указатель на заполненную в си структуру(structPointer).
Получаем доступ к памяти этой структуры. В структуре есть данные и размер данных.
Обращаться к ним можно как к полям структуры созданной в swift (через точку).
Собираем из этого массив байт (Data), который декодируем в String. Ну и не забываем прибраться за собой. Создаем 2 функции:
Когда эти функции вызываются из Swift, параметрами мы в них передаем либо полученные указатели, либо указатели из структуры.
И вуаля — дело в шляпе!
Ничего нового в таком подходе, конечно же нет, однако он позволяет активно работать с кроссплатформенными функциями и довольно удобен.
Спасибо тем кто дочитал.
Ссылка на проект в git — тут
EOF
Однажды мне поручили задачу под 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
storoj
в Swift нельзя просто вызвать free?
почему freeSMemmory не может освободить и строку тоже? почему "memmory" а не memory?..
зачем нужна длина строки в структуре? строка как была null-terminated, так и осталась
зачем два раза выделять память в функции, которая возвращает структуру?
одни вопросы
anonymous Автор
Здравствуйте! Спасибо за ваш комментарий.
В Swift можно вызвать функцию free и передать в нее любой указатель, но так как память выделяется в Си коде, я решил ее там и освобождать. Кроме того, если вы будете использовать эти функции на другом языке программирования, то вам все равно придется как то освобождать память.
Я так понимаю, что тут вопрос в том, почему «freeMemmory»(она принимает параметр с void*) не может также освободить и структуру? Она может. Отвечая на ваш вопрос, потому что функции freeSMemmory ждет указатель на структуру. Вы получите ошибку типа «Cannot convert value of type 'UnsafeMutableRawPointer' to expected argument type 'UnsafeMutablePointer?'». Хотелось обратить внимание на типы данных по указателю. Можно было и обойтись, согласен.
Ошибся.
В этом конкретном случае, вы конечно же правы. Можно считать каждый символ и смотреть не '\0' ли он. Но если у вас есть массив байт, который не обязательно закрыт, размер данных пригодится.
Вы про то, что можно написать что то типа:
и не освобождать ее внутри? Если да, то конечно можно.
anonymous
А через withUnsafeMutableBufferPointer по идее тоже можно напрямую взять указатель на array, а не выделять лишнюю память?
anonymous Автор
Честно говоря, я не пользовался, но да, действительно можно.
На сколько я понял вы предлагаете что то такое?
spiceginger
Да, только весь остальной код должен быть внутри этого блока — это гарантирует что свифт не осободит память на которую ссылается указатель за ненадобностью
anonymous
да, все верно