[译]Unsafe Swift - 指针与C交互

2018-01-26 10:29:03来源:https://juejin.im/post/5a58703651882573473db316作者:稀土掘金人点击

分享

默认情况下,Swift是内存安全的,这就意味着 Swift 会避免内存的直接访问,并确定所有变量会在初始化后才进行使用。这里的关键词语是“默认的”。也就是说,当我们需要时,不安全的 Swift 还是可以通过指针来直接访问到内存地址的


这篇教程会告诉你为啥 Swift 又是“不安全的”。“不安全”这个词可能会使人误解,它不是说我们写的代码可能会发生出乎意料的问题,而是说我们需要额外注意这些代码,因为编译器在编译过程中并不会给予我们帮助,我们无法直观地捕获到错误。


在开发工作中,当你需要与C这些不安全的语言进行交互时,就会用得上这篇文章所介绍的特性了。


开始

这篇教程由3个 playgrounds 组成。在第一个 playground 中,将会创建几个简短的片段来了解内存布局并且使用不安全的指针。在第二个 playground 中,会封装C语言的API来执行流数据的压缩。最后的 playground ,创建 arc4random 的替代函数


首先创建一个新的 playground,名称为 UnsafeSwift


内存布局



不安全的 Swift 会直接在内存系统中进行交互。内存可以通过一系列格子来进行可视化,每个格子里是与内存相关的唯一性的内存地址。存储的最小单位是 byte,由8个 bits 组成。8 bit 的 byte 可以保存0-255大小的数字。


Swift 有一个 MemoryLayout 的类可以告诉我们每个类型对象的大小和分布。添加如下代码:


MemoryLayout<Int>.size// returns 8 (on 64-bit)
MemoryLayout<Int>.alignment // returns 8 (on 64-bit)
MemoryLayout<Int>.stride// returns 8 (on 64-bit)
MemoryLayout<Int16>.size// returns 2
MemoryLayout<Int16>.alignment // returns 2
MemoryLayout<Int16>.stride// returns 2
MemoryLayout<Bool>.size // returns 1
MemoryLayout<Bool>.alignment// returns 1
MemoryLayout<Bool>.stride // returns 1
MemoryLayout<Float>.size// returns 4
MemoryLayout<Float>.alignment // returns 4
MemoryLayout<Float>.stride// returns 4
MemoryLayout<Double>.size // returns 8
MemoryLayout<Double>.alignment// returns 8
MemoryLayout<Double>.stride // returns 8

MemoryLayout<Type> 会在编译时确定指定类型的 size , alignment 和 stride 。举个例子来说, Int16 的 size 是2个byte,内存对齐也是2.这就意味着其内存地址必须是偶数地址


因此,假设地址100和101给 Int16 分配地址的话,肯定就是选择100了, 因为101违背了对齐原则。当我们将一堆 Int16 打包在一起的话, stride 表示的是,当前类型的内存地址开头与下一个内存地址开头之间的距离


接下来,看看用户自定义的 structs 的内存布局:


MemoryLayout<EmptyStruct>.size// 0
MemoryLayout<EmptyStruct>.alignment // 1
MemoryLayout<EmptyStruct>.stride// 1
struct SampleStruct {
let number: UInt32
let flag: Bool
}
MemoryLayout<SampleStruct>.size // 5
MemoryLayout<SampleStruct>.alignment// 4
MemoryLayout<SampleStruct>.stride // 8

空的结构体的size为0.因为空结构体的 stride 为1,所以它可以分配在任意的地址上。


对于SampleStruct来说, 其 size 为5, stride 为8.这是因为内存对齐的位数是4个字节。


然后看下类对象的:


class EmptyClass {}
MemoryLayout<EmptyClass>.size// 8
MemoryLayout<EmptyClass>.alignment // 8
MemoryLayout<EmptyClass>.stride// 8
class SampleClass {
let number: Int64 = 0
let flag: Bool = false
}
MemoryLayout<SampleClass>.size// 8
MemoryLayout<SampleClass>.alignment // 8
MemoryLayout<SampleClass>.stride// 8

可以看到,类对象的 size , alignment 和 stride 都是8,且不管是否空的对象。


指针

指针对象包含了一个内存地址。直接涉及内存访问的类型会有一个 unsafe 的前缀,所以其指针称为 UnsafePointer . 虽然长长的类型看起来会比较烦,但是可以使我们清楚地知道相关的代码是没有经过相关的编译器检查,可能会导致未定义的行为(而不仅仅是一个可预见的崩溃)。


Swift 的设计者其实可以创建了一个 UnsafePointer 类型,并且该类型与C语言中的 char * 相等,可以用来以非结构化方式来访问内存。但是他们并没有。Swift 涵盖了大部分的指针类型,每种类型都有不同的用处和目的。使用合适的指针类型可以更好地达到我们的需求,更少地引起错误。


不安全的 Swift 指针的命名可以让我们知道该指针的特征。可变( Mutable )或者不可变( Immutable ), 原始的( raw ) 或者其他类型的( typed ),是否是缓存风格( buffer style ). 在 Swift 中,一共有8种类型组合:





Unsafe[Mutable][Raw][Buffer]Pointer[]


指针就是内存地址,直接访问内存就是 Unsafe 的


Mutable表示可写


Raw表示它是否指向了二进制数据类型的字节(blob of bytes)


Buffer表示其是否是一个结合


原始指针的使用
// 1
let count = 2
let stride = MemoryLayout<Int>.stride
let alignment = MemoryLayout<Int>.alignment
let byteCount = stride * count
// 2
do {
print("Raw pointers")
// 3
let pointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment)
// 4
defer {
pointer.deallocate(bytes: byteCount, alignedTo: alignment)
}
// 5
pointer.storeBytes(of: 42, as: Int.self)
pointer.advanced(by: stride).storeBytes(of: 6, as: Int.self)
pointer.load(as: Int.self)
pointer.advanced(by: stride).load(as: Int.self)
// 6
let bufferPointer = UnsafeRawBufferPointer(start: pointer, count: byteCount)
for (index, byte) in bufferPointer.enumerated() {
print("byte /(index): /(byte)")
}
}
// Output
// Raw pointers
// byte 0: 42
// byte 1: 0
// byte 2: 0
// byte 3: 0
// byte 4: 0
// byte 5: 0
// byte 6: 0
// byte 7: 0
// byte 8: 6
// byte 9: 0
// byte 10: 0
// byte 11: 0
// byte 12: 0
// byte 13: 0
// byte 14: 0
// byte 15: 0

上面的例子中,使用不安全的 Swift 指针来保存并加载两个整数:


常量:


count : 要保存的整数的个数
stride : Int 的步长
alignment : Int 的内存对齐位数
byteCount : 总字节数

do 添加了一个块级作用域,方便重新使用变量名


UnsafeMutableRawPointer.allocate 用于分配所需要的字节数。该方法返回一个 UnsafeMutableRawPointer 指针。从名称我们可以得知该指针可以用来加载和保存原始类型的字节


defer 用来保证指针能够得到释放。


storeBytes 和 load 用于存储和加载字节。第二个整数的内存地址根据指针的步长进行计算得出。


UnsafeRawBufferPointer 让我们可以像访问字节集合一样来对内存地址进行访问。就是我们可以遍历字节,通过下标访问,或者是调用 map , filter 等方法。缓存区的指针需要使用原始指针来进行初始化


类型指针的使用
do {
print("Typed pointers")
let pointer = UnsafeMutablePointer<Int>.allocate(capacity: count)
pointer.initialize(to: 0, count: count)
defer {
pointer.deinitialize(count: count)
pointer.deallocate(capacity: count)
}
pointer.pointee = 42
pointer.advanced(by: 1).pointee = 6
pointer.pointee
pointer.advanced(by: 1).pointee
let bufferPointer = UnsafeBufferPointer(start: pointer, count: count)
for (index, value) in bufferPointer.enumerated() {
print("value /(index): /(value)")
}
}
// Output
// Typed pointers
// value 0: 42
// value 1: 6

与原始指针的不同点在于:


UnsafeMutablePointer.allocate 方法用于分配内存。
类型对象的内存必须在使用前进行初始化,不再使用以后需要进行析构处理。
类型指针有一个属性 pointee 用于加载和保存值
当向前移动类型指针时,可以很方便标志指针所指向的位置。指针会根据其指向的类型,计算出正确地步长。
类型的 buffer 指针也可以遍历指针对象
原始指针转换为类型指针

类型指针并不一定需要直接进行初始化,也可以通过原始指针来进行转换:


do {
print("Converting raw pointers to typed pointers")
let rawPointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment)
defer {
rawPointer.deallocate(bytes: byteCount, alignedTo: alignment)
}
let typedPointer = rawPointer.bindMemory(to: Int.self, capacity: count)
typedPointer.initialize(to: 0, count: count)
defer {
typedPointer.deinitialize(count: count)
}
typedPointer.pointee = 42
typedPointer.advanced(by: 1).pointee = 6
typedPointer.pointee
typedPointer.advanced(by: 1).pointee
let bufferPointer = UnsafeBufferPointer(start: typedPointer, count: count)
for (index, value) in bufferPointer.enumerated() {
print("value /(index): /(value)")
}
}
// Output
// Converting raw pointers to typed pointers
// value 0: 42
// value 1: 6

这个例子除了首先创建了原始指针然后将内存绑定到类型指针上以外,与上一个相似。绑定内存后,我们就能通过一种类型安全的方式对内存进行访问。内存绑定在我们创建类型指针时会隐式进行


获取实例的字节数

一般情况下,我们可以通过 withUnsafeBytes(of:) 方法来获取一个实例对象的字节。


do {
print("Getting the bytes of an instance")
var sampleStruct = SampleStruct(number: 25, flag: true)
withUnsafeBytes(of: &sampleStruct) { bytes in
for byte in bytes {
print(byte)
}
}
}
// Output
// Getting the bytes of an instance
// 25
// 0
// 0
// 0
// 1

这个例子输出了 SampleStruct 的实例的原始字节, withUnsafeBytes(of:) 方法允许我们对 UnsafeRawBufferPointer 进行访问。


withUnsafeBytes 也可以用于 Array 和 Data 的实例。


计算校验和

可以使用 withUnsafeBytes(of:) 来返回一个结果。下面的例子用来计算结构体中的32位校验和


do {
print("Checksum the bytes of a struct")
var sampleStruct = SampleStruct(number: 25, flag: true)
let checksum = withUnsafeBytes(of: &sampleStruct) { (bytes) -> UInt32 in
return -bytes.reduce(UInt32(0)) { $0 + numericCast($1) }
}
print("checksum", checksum)
}

最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台