Система типов Swift разработана, чтобы сделать нашу жизнь проще, применяя строгие правила относительно того, что мы можем и не можем делать в нашем коде. Это, несомненно, отличная вещь, и она побуждает программистов писать более качественный и правильный код. Тем не менее, это может показаться чрезвычайно запретным при взаимодействии с устаревшими базами кода, особенно с библиотеками на основе Си. Это реальность, что многие библиотеки C злоупотребляют типами таким образом, что это плохо сочетается с компилятором Swift. Это правда, что команда Swift в Apple изо всех сил старалась поддерживать некоторые из более базовых функций C, такие как строки C, но все еще есть много проблем при работе с устаревшей библиотекой C в Swift. Вот как с ними бороться.
Прежде чем мы начнем, я хотел бы отметить, что многие из операций в этой статье небезопасны, поскольку они полностью обходят систему типов компилятора Swift. Я рекомендую вам внимательно прочитать и воздерживаться от копирования и вставки кода из этой статьи. Это не переполнение стека, у этих фрагментов есть реальная возможность повредить память, вызвать утечку памяти или просто вывести из строя приложение, если оно используется неправильно.
Основы
В большинстве случаев указатели C импортируются в Swift одним из двух способов:
UnsafePointer<T>
или
UnsafeMutablePointer<T>
Где T — эквивалентный тип Swift для исходного типа C. Указатели, объявленные как const в C, импортируются как UnsafePointer UnsafeMutablePointer
Вот несколько примеров
C: void myFunction(const int *myConstIntPointer); Swift: func myFunction(myConstIntPointer: UnsafePointer<Int32>) C: void myOtherFunction(unsigned int *myUnsignedIntPointer); Swift: func myOtherFunction(myUnsignedIntPointer: UnsafeMutablePointer<UInt32>) C: void iTakeAVoidPointer(void *aVoidPointer); Swift: func iTakeAVoidPointer(aVoidPointer: UnsafeMutablePointer<Void>)
Если тип указателя не известен Swift, например, потому что он является предварительным объявлением, он представляется с использованием COpaquePointer .
C: struct SomeThing; void iTakeAnOpaquePointer(struct SomeThing *someThing); Swift: func iTakeAnOpaquePointer(someThing: COpaquePointer)
Передача указателей на быстрые объекты
Во многих случаях это так же просто, как использовать оператор inout , который совпадает со знакомым оператором address-of в C, амперсандом.
Swift: let myInt: = 42 myFunction(&myInt) var myUnsignedInt: UInt = 7 myOtherFunction(&myUnsignedInt)
Здесь есть две очень важные, но тонкие детали.
-
При использовании оператора
inoutпеременные, объявленные с помощьюvarи константы, объявленные с помощьюlet, преобразуются вUnsafePoinerи UnsafeMutablePointerсоответственно. Это очень легко пропустить, если вы не обращаете пристальное внимание на исходный тип в коде. Попытка передать UnsafePointerгде UnsafeMutablePointerОжидаемые результаты приводят к, казалось бы, загадочной ошибке компилятора, так что будьте осторожны. -
Этот оператор работает только в контексте передачи значений и ссылок Swift в качестве аргументов функции, которые ожидают
UnsafePointerили UnsafeMutablePointer, Вы не можете получить указатель в любом другом контексте. Например, это неверно и приведет к ошибке компилятора: Swift: let x = 42 let y = &xВремя от времени вам нужно будет взаимодействовать с API, который принимает или возвращает указатель void вместо явного типа. К сожалению, это часто встречается в C, где нет способа указать универсальный тип.
C: void takesAnObject(void *theObject);Если вы знаете ожидаемый тип, принятый функцией, вы можете привести объект к
withUnsafePointerуказателю, используяwithUnsafePointerиunsafeBitCast. Например, предположим, чтоtakesAnObjectожидает указатель наint.var test = 42 withUnsafePointer(&test, { (ptr: UnsafePointer<Int>) -> Void in var voidPtr: UnsafePointer<Void> = unsafeBitCast(ptr, UnsafePointer<Void>.self) takesAnObject(voidPtr) })Давайте разберемся с этим. Сначала мы делаем вызов
withUnsafeMutablePointer. Эта универсальная функция принимает два аргумента.Первый является
inoutтипаT, а второй является закрытием типа(UnsafePointer ) -> ResultType , Эта функция вызывает замыкание, беря указатель на первый аргумент функции и передавая его как единственный аргумент замыкания. Затем функция возвращает результат закрытия. В приведенном выше примере тип замыкания возвращает(UnsafePointer ) -> ResultTypeVoidи поэтому ничего не возвращает. Мы могли бы так же легко сделать что-то вроде этого:let ret = withUnsafePointer(&test, { (ptr: UnsafePointer<Int>) -> Int32 in var voidPtr: UnsafePointer<Void> = unsafeBitCast(ptr, UnsafePointer<Void>.self) return takesAnObjectAndReturnsAnInt(voidPtr) }) println(ret)Примечание : если вам нужно изменить сам указатель, есть
withUnsafeMutablePointerвариант. Для удобства в Swift также есть варианты, которые проходят два указателя:
var x: Int = 7 var y: Double = 4 withUnsafePointers(&x, &y, { (ptr1: UnsafePointer<Int>, ptr2: UnsafePointer<Double>) -> Void in var voidPtr1: UnsafePointer<Void> = unsafeBitCast(ptr1, UnsafePointer<Void>.self) var voidPtr2: UnsafePointer<Void> = unsafeBitCast(ptr2, UnsafePointer<Void>.self) takesTwoPointers(voidPtr1, voidPtr2) })О unsafeBitCast
unsafeBitCast— чрезвычайно опасная операция. Документация описывает это как «жестокое приведение битов к чему-либо одинакового размера». Причина, по которой мы можем безопасно использовать это выше, заключается в том, что мы просто приводим между указателями разных типов, и все указатели одинаковы размер на любой данной платформе. Вот почему мы должны вызватьwithUnsafePointerчтобы получить типизированныйUnsafePointerсначала перед приведением этого к UnsafePointerкак определяется C API. Вначале это может сбивать с толку, особенно при работе с типом, размер которого совпадает с указателем, таким как
Intв Swift (в любом случае на всех доступных в настоящее время платформах, где размер указателя равен 1Word, а 1 Word — размерInt).Это легко сделать ошибку, как это:
var x: Int = 7 let xPtr = unsafeBitCast(x, UnsafePointer<Void>.self)С целью получения указателя на х. Это невероятно вводит в заблуждение, потому что он компилируется и запускается , но приводит к неожиданным ошибкам, потому что вместо указателя на x API C получит указатель с местоположением 0x7 или мусор.
Поскольку
unsafeBitCastтребует, чтобы размер типов был одинаковым, он менее коварен при попытке привести что-либо, кромеInt, такое какInt8или однобайтовое целое число.var x: Int8 = 7 let xPtr = unsafeBitCast(x, UnsafePointer<Void>.self)Это просто приведет к тому, что
unsafeBitCastсгенерирует исключение иunsafeBitCastработу вашей программы!Взаимодействие с C структурами
Давайте разберемся с этим на конкретном примере. Вы хотите получить информацию о системе, на которой работает ваш компьютер. Для этого есть C API,
uname(2), который берет указатель на структуру и заполняет информацию в предоставленном объекте системной информацией, такой как имя и версия ОС или ее аппаратный идентификатор. Однако есть одна загвоздка: структура импортируется в Swift как:struct utsname { var sysname: (Int8, Int8, ...253 times..., Int8) var nodename: (Int8, Int8, ...253 times..., Int8) var release: (Int8, Int8, ...253 times..., Int8) var version: (Int8, Int8, ...253 times..., Int8) var machine: (Int8, Int8, ...253 times..., Int8) }о нет! Swift импортирует литералы массива C в виде кортежей! Кроме того, для инициализатора по умолчанию требуются значения для каждого поля. Так что, если бы вы делали это обычным способом Swift, это было бы так:
var name = utsname(sysname: (0, 0, 0, ..., 0), nodename: (0, 0, 0, ..., 0), etc) utsname(&name) var machine = name.machine println(machine)Это не очень хорошая идея! Но есть другая проблема.
machineполеutsnameявляется кортежем, поэтомуprintlnсобирается распечатать 256Int8, и только первые несколько представляют значения ASCII символов в строке, которую мы на самом деле хотим.Итак, как мы можем это исправить?
UnsafeMutablePointerСвифтапредоставляет два метода, alloc(Int)иdealloc(Int), для ручного выделения и освобождения соответственно. Аргумент количествоTдля выделения или освобождения. Мы можем использовать эти API для упрощения нашего кода.let name = UnsafeMutablePointer<utsname>.alloc(1) uname(name) let machine = withUnsafePointer(&name.memory.machine, { (ptr) -> String? in let int8Ptr = unsafeBitCast(ptr, UnsafePointer<Int8>.self) return String.fromCString(int8Ptr) }) name.dealloc(1) if let m = machine { println(m) }Первый шаг — сделать наш вызов
withUnsafePointer, передать ему кортеж машины и сказать, что наше закрытие вернет необязательную строку.Внутри замыкания мы берем предоставленный указатель и
UnsafePointerего кUnsafePointerв основном эквивалентное представление того же значения. За исключением того, что StringSwift имеет метод класса для инициализации изUnsafePointerгде CChar— это typealias дляInt8! Таким образом, мы можем передать наш новый указатель инициализатору и вернуть значение.После захвата результата
withUnsafePointer(который, помните, пересылает возвращаемое значение предоставленного замыкания), мы можем проверить, получили ли мы значение с помощью оператора условного let, и распечатать результат. Для меня это дает «x86_64», как и ожидалось.
Вывод
Немного отказа от ответственности. Использование небезопасных API в Swift должно быть последним средством, потому что они по своей сути небезопасны! По мере того, как мы переходим от унаследованного кода C и Objective-C к Swift, вполне вероятно, что нам по-прежнему потребуется совместимость этих API с нашими существующими инструментами. Тем не менее, всегда следует скептически относиться к тому, что их первым средством является разрыв с UnsafePointer и unsafeBitCast.
Новый код должен стремиться быть настолько идиоматичным, насколько это возможно, что явно запрещает использование небезопасных API-интерфейсов Swift. Как разработчики программного обеспечения, так же важно знать, как использовать ваши инструменты, так и знать, когда и где их не использовать. Swift приносит с собой выгоду от модернизации большого подмножества разработки программного обеспечения, и мы должны уважать его идеологии, чтобы он действительно оказал влияние.