Swift的类型系统的设计目的在于简化我们的生活,为此它强制用户遵守严格的代码规范来达到这一点。毫无疑问这是一件大好事,它鼓励程序员们编写 更好更正确的代码。然而,当Swift与历史遗留的代码库、特别是C语言库进行交互时,问题出现了。我们需要面对的现实是许多C语言库滥用类型,以至于它 们对Swift的编译器并不友好。苹果的Swift团队的确花了不少功夫来支持C的一些基础特性,比如C字符串。但当在Swift中使用历史遗留的C语言 库时,我们还是会面临一些问题。下面我们就来解决这些问题。
在开始之前我必须先提醒一下,这篇文章代码里的许多操作有潜在的安全问题,即使 它们绕过了Swift编译器的类型系统的检查,我建议你仔细的阅读并且不要复制粘贴文章内的代码。它们不是Stack Overflow,不恰当的使用它们会真的导致记忆体损坏、内存泄露,或者至少让你的程序崩溃。
基础概念
大多数时候,C语言指针有两种方法导入到Swift中:
1
|
UnsafePointerUnsafeMutablePointer |
这里的T是C类型的等价的Swift类型。声明为常量的指针被导入为UnsafePointer,非常量的指针则被导入为UnsafeMutablePoinger。
这里有一些示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
C: void myFunction(const int *myConstIntPointer); Swift: func myFunction(myConstIntPointer: UnsafePointer) C: void myOtherFunction(unsigned int *myUnsignedIntPointer); Swift: func myOtherFunction(myUnsignedIntPointer: UnsafeMutablePointer) C: void iTakeAVoidPointer(void *aVoidPointer); Swift: func iTakeAVoidPointer(aVoidPointer: UnsafeMutablePointer) |
如果不知道指针的类型,比如一个为前置声明的指针,则使用COpaquePointer。
1
2
3
4
5
6
|
C: struct SomeThing; void iTakeAnOpaquePointer(struct SomeThing *someThing); Swift: func iTakeAnOpaquePointer(someThing: COpaquePointer) |
传递指针到Swift对象
在很多情况下,传递指针到Swift对象和使用inout运算符一样简单,后者类似于C语言中的and运算符。
1
2
3
4
5
6
|
Swift: let myInt: = 42 myFunction(&myInt) var myUnsignedInt: UInt = 7 myOtherFunction(&myUnsignedInt) |
这里有两个非常重要但容易被忽视的细节。
1. 当使用inout运算符时,使用var声明的变量和使用let声明的常量被分别转换到UnsafePointer和 UnsafeMutablePoinger,如果你不注意原来代码中的类型,就很容易出错。你可以试着向本来是UnsafeMutablePoinger 的地方传递一个UnsafePointer看看,编译器会报错。
2. 这个运算符只在将Swift值和引用作为函数参数传递的上下文时生效,且该函数参数只接受UnsafePointer和UnsafeMutablePoinger两种类型。你不能在其它上下文获得这些指针。比如,下面的代码是无效的,并且会返回编译错误。
1
2
3
|
Swift: let x = 42 let y = &x |
你可能会不时的需要交互操作一个API。来获取或返回一个空指针以代替显式类型,不幸的是这种做法在C语言里很普遍,导致无法指定一个通用类型。
1
2
|
C: void takesAnObject(void *theObject); |
如果你确定函数需要获取什么类型的参数,你可以使用withUnsafePointer和unsafeBitCast将对象强制转换为空指针。比如,假设takesAnObject需要获取指向int的指针。
1
2
3
4
5
|
var test = 42 withUnsafePointer(&test, { (ptr: UnsafePointer) -> Void in var voidPtr: UnsafePointer = unsafeBitCast(ptr, UnsafePointer.self) takesAnObject(voidPtr) }) |
为了转换它,首先我们需要调用withUnsafeMutablePointer,这个通用函数包含两个参数。
第 一个参数是T类型的inout运算符,第二个是(UnsafePointer) -> ResultType的闭包。函数通过指向第一个参数的指针来调用闭包,然后然后将其作为闭包唯一的参数传递,最后函数返回闭包的结果。在上面的例子里, 闭包的类型被设置为Void,因此将不返回值。返回值的例子如下:
1
2
3
4
5
6
|
let ret = withUnsafePointer(&test, { (ptr: UnsafePointer) -> Int32 in var voidPtr: UnsafePointer = unsafeBitCast(ptr, UnsafePointer.self) return takesAnObjectAndReturnsAnInt(voidPtr) }) println(ret) |
注意:你需要自己修改指针,通过withUnsafeMutablePointer变体来完成修改。
为方便起见,Swift也包括传递两个指针的变体:
1
2
3
4
5
6
7
8
9
10
|
var x: Int = 7 var y: Double = 4 withUnsafePointers(&x, &y, { (ptr1: UnsafePointer, ptr2: UnsafePointer) -> Void in var voidPtr1: UnsafePointer = unsafeBitCast(ptr1, UnsafePointer.self) var voidPtr2: UnsafePointer = unsafeBitCast(ptr2, UnsafePointer.self) takesTwoPointers(voidPtr1, voidPtr2) }) |
关于unsafeBitCast
unsafeBitCast 是一个极度危险的操作。文档将其描述为“将某物强制转换为和其他东西相同的比特位”。在上面我们能够安全的使用它的原因是,我们只是简单的转换不同类型的 指针,并且这些指针的比特位都是相同的。这也是我们为什么必须先调用withUnsafePointer来获取UnsafePointer,然后将其转换 为UnsafePointer的原因。
在一开始这可能会造成迷惑,特别是当处理与指针相同的类型时,比如Swift里的Int(在目前所有可用的平台,一个指针的长度是一个字符,Int的长度同样也是一个字符)。
比如你很容易就会犯下面的错误:
1
2
|
var x: Int = 7 let xPtr = unsafeBitCast(x, UnsafePointer.self) |
这段代码的意图是获取一个指针并传递给x。它会给人造成误解,尽管编译能通过并且能运行,但会导致一个意外错误。这是因为C API没有获取指针并传递给x,而是接收位于0x7或者其他地方的指针。
因为unsafeBitCast要求类型的长度相等,所以当试图转换Int8或者一个字节的整型时没那么阴险了。
1
2
|
var x: Int8 = 7 let xPtr = unsafeBitCast(x, UnsafePointer.self) |
这段代码会简单的导致unsafeBitCast抛出异常和程序崩溃。
与C语言中的结构体交互
让 我们用实际的示例来展示这一部分。如果你想检索计算机所运行的系统信息,有一个C API:uname(2)可以达到目的。它接收指针指到一个数据结构,并且用系统信息填充所提供的对象,如OS名称和版本或者硬件识别符。但这里有一个问 题,导入到Swift的结构体是这样:
1
2
3
4
5
6
7
8
|
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通常的做法来做的话,它将会变成:
1
2
3
4
5
|
var name = utsname(sysname: (0, 0, 0, ..., 0), nodename: (0, 0, 0, ..., 0), etc) utsname(&name) var machine = name.machine println(machine) |
这不是一个好方法。并且还存在另一个问题。因为utsname里的machine字段是元组,所以当使用println时将输出256位的Int8,但实际上只有字符串开始的几个ASCII值是我们需要的。
那么,如何解决这个问题?
Swift里的UnsafeMutablePointer提供两个方法,alloc(Int)和dealloc(Int),分别用来分配和解除分配模板T的数量参数。我们可以用这些API来简化我们的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
let name = UnsafeMutablePointer.alloc(1) uname(name) let machine = withUnsafePointer(&name.memory.machine, { (ptr) -> String? in let int8Ptr = unsafeBitCast(ptr, UnsafePointer.self) return String.fromCString(int8Ptr) }) name.dealloc(1) if let m = machine { println(m) } |
第一步是调用withUnsafePointer,将机器的元组传递给它,并通知它我们的闭包将返回一个附加字符串。
在 闭包里面我们将指针转换为UnsafePointer,即该值的最等价的表述。除此之外,Swift的String包含一个类方法来初始化 UnsafePointer,这里的CChar是Int8类型的别名(typealias),所以我们能够将我们的新指针传递给初始化程序并且返回所需要 的信息。
在获取withUnsafePointer的结果之后,我们能够测试它是否是let的条件声明,并打印出结果。对这个例子来说,它输出了期望的字段“x86_64”。
总结
最 后,说一下免责声明。在Swift中使用不安全的API应该被视为最后手段,因为它们是潜在不安全的。当我们转换遗留的C和Objective-C代码到 Swift中,有很大可能性我们会继续需要这些API来兼容现有工具。然而,当使用withUnsafePointer和unsafeBitCast作为 首选手段时应该始终抱有怀疑态度,并寻找其他更好的解决方案。
新的代码应该尽可能符合语言习惯,不要在Swift代码中使用不安全的API。作为软件开发者,你应该了解如何使用你的工具以及什么地方该使用和什么地方不该使用。Swift将现代化带给了OS X和iOS开发,我们必须尊重它的理念。