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

默认状况下,Swift是内存安全的,这就意味着 Swift 会避免内存的直接访问,并肯定全部变量会在初始化后才进行使用。这里的关键词语是“默认的”。也就是说,当咱们须要时,不安全的 Swift 仍是能够经过指针来直接访问到内存地址的swift

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

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

开始

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

首先建立一个新的 playground,名称为UnsafeSwiftide

内存布局

不安全的 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,alignmentstride。举个例子来讲, Int16size是2个byte,内存对齐也是2.这就意味着其内存地址必须是偶数地址ui

所以,假设地址100和101给Int16分配地址的话,确定就是选择100了, 由于101违背了对齐原则。当咱们将一堆Int16打包在一块儿的话,stride表示的是,当前类型的内存地址开头与下一个内存地址开头之间的距离spa

接下来,看看用户自定义的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
复制代码

能够看到,类对象的sizealignmentstride都是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 指针来保存并加载两个整数:

  1. 常量:

    • count: 要保存的整数的个数
    • stride: Int的步长
    • alignment: Int的内存对齐位数
    • byteCount: 总字节数
  2. do添加了一个块级做用域,方便从新使用变量名

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

  4. defer用来保证指针可以获得释放。

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

  6. 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也能够用于ArrayData的实例。

计算校验和

可使用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)
}
复制代码

Unsafe 的使用规则

  • 不要在withUnsafeBytes中返回指针
  • 一次只绑定一种类型
  • 避免指针指向最后的位置

原文地址:Unsafe Swift: Using Pointers And Interacting With C

相关文章
相关标签/搜索