Система типов 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 ) -> ResultType
Void
и поэтому ничего не возвращает. Мы могли бы так же легко сделать что-то вроде этого: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
в основном эквивалентное представление того же значения. За исключением того, что String
Swift имеет метод класса для инициализации изUnsafePointer
где CChar
— это typealias дляInt8
! Таким образом, мы можем передать наш новый указатель инициализатору и вернуть значение.После захвата результата
withUnsafePointer
(который, помните, пересылает возвращаемое значение предоставленного замыкания), мы можем проверить, получили ли мы значение с помощью оператора условного let, и распечатать результат. Для меня это дает «x86_64», как и ожидалось.
Вывод
Немного отказа от ответственности. Использование небезопасных API в Swift должно быть последним средством, потому что они по своей сути небезопасны! По мере того, как мы переходим от унаследованного кода C и Objective-C к Swift, вполне вероятно, что нам по-прежнему потребуется совместимость этих API с нашими существующими инструментами. Тем не менее, всегда следует скептически относиться к тому, что их первым средством является разрыв с UnsafePointer и unsafeBitCast.
Новый код должен стремиться быть настолько идиоматичным, насколько это возможно, что явно запрещает использование небезопасных API-интерфейсов Swift. Как разработчики программного обеспечения, так же важно знать, как использовать ваши инструменты, так и знать, когда и где их не использовать. Swift приносит с собой выгоду от модернизации большого подмножества разработки программного обеспечения, и мы должны уважать его идеологии, чтобы он действительно оказал влияние.