Статьи

Использование Legacy C API с Swift

Система типов Swift разработана, чтобы сделать нашу жизнь проще, применяя строгие правила относительно того, что мы можем и не можем делать в нашем коде. Это, несомненно, отличная вещь, и она побуждает программистов писать более качественный и правильный код. Тем не менее, это может показаться чрезвычайно запретным при взаимодействии с устаревшими базами кода, особенно с библиотеками на основе Си. Это реальность, что многие библиотеки C злоупотребляют типами таким образом, что это плохо сочетается с компилятором Swift. Это правда, что команда Swift в Apple изо всех сил старалась поддерживать некоторые из более базовых функций C, такие как строки C, но все еще есть много проблем при работе с устаревшей библиотекой C в Swift. Вот как с ними бороться.

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

Основы

В большинстве случаев указатели C импортируются в Swift одним из двух способов:

UnsafePointer<T> 

или

 UnsafeMutablePointer<T> 

Где T — эквивалентный тип Swift для исходного типа C. Указатели, объявленные как const в C, импортируются как UnsafePointer и указатели, которые не объявлены как const , импортируются как 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) 

Здесь есть две очень важные, но тонкие детали.

  1. При использовании оператора inout переменные, объявленные с помощью var и константы, объявленные с помощью let , преобразуются в UnsafePoiner и UnsafeMutablePointer соответственно. Это очень легко пропустить, если вы не обращаете пристальное внимание на исходный тип в коде. Попытка передать UnsafePointer где UnsafeMutablePointer Ожидаемые результаты приводят к, казалось бы, загадочной ошибке компилятора, так что будьте осторожны.

  2. Этот оператор работает только в контексте передачи значений и ссылок 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 (в любом случае на всех доступных в настоящее время платформах, где размер указателя равен 1 Word , а 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 собирается распечатать 256 Int8 , и только первые несколько представляют значения 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 приносит с собой выгоду от модернизации большого подмножества разработки программного обеспечения, и мы должны уважать его идеологии, чтобы он действительно оказал влияние.