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

默认情况下,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,alignmentstride。举个例子来说, Int16size是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
复制代码

可以看到,类对象的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

转载于:https://juejin.im/post/5a58703651882573473db316

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值