Unsafe Swift:在 Swift 中使用指针 & Swift 与 C 交互

534 阅读14分钟

默认情况下,Swift 是内存安全的。其防止开发者直接访问内存,并确保开发者在使用其之前已经完成了初始化。但这只是“默认情况下”,开发中还可以使用 Unsafe Swift,直接通过指针访问内存。

Unsafe 不是可能无法运行的危险代码,它指的是需要格外小心的代码,这些代码限制了编译器的保护。如果开发者需要与 C 等不安全的语言进行互操作,或者需要获得额外的运行时性能,再者是想探索 Swift 的内部结构,这些功能非常有用。

我们将了解 Swift 的 Unsafe 特性,学习如何使用指针并直接与内存进行交互。

探索内存布局

基本数据类型的内存布局

Unsafe Swift 直接与内存一起工作。我们可以将内存想象成一堆的盒子,这些盒子可能有数十亿个,每个盒子都包含一个数字。

每个盒子都有一个唯一的内存地址(Memory Address ) 。最小的可寻址存储单元是一个字节(Byte) ,通常由八位(Bit) 组成。

一个八位的字节可以存储 0-255 的值。处理器通常可以有效地访问超过一个字节的内存字(Word)

在 64 位系统上,一个字是 8 个字节即 64 位。我们可以使用 MemoryLayout 得到原生 Swift 类型的 size 和 alignment:

import Foundation
​
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 2MemoryLayout<Bool>.size         // returns 1
MemoryLayout<Bool>.alignment    // returns 1
MemoryLayout<Bool>.stride       // returns 1MemoryLayout<Float>.size        // returns 4
MemoryLayout<Float>.alignment   // returns 4
MemoryLayout<Float>.stride      // returns 4MemoryLayout<Double>.size       // returns 8
MemoryLayout<Double>.alignment  // returns 8
MemoryLayout<Double>.stride     // returns 8

MemoryLayout<Type> 是编译时评估的泛型类型。它确定每个指定 Typesizealignmentstride,并返回一个以字节为单位的数字。

举一个例子,一个 Int16size 是两个字节,并且 alignment 也是两个字节。这意味着它必须从偶数地址开始——即可以被 2 整除的地址。例如,在地址 100 处分配一个 Int16 是合法的,但在 101 处则不合法——违反了所要求的对齐方式。当我们将一堆 Int16 打包时,它们以 stride 为间隔。对于这些基本类型,sizestride 相同。

结构体和类的内存布局

我们开看下结构体的内存布局:

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

空结构体的 size 为零。因为 alignment 是 1,并且所有数字都可以被 1 整除,所以它可以存在于任何地址。空结构体的 stride 为什么是 1 ?这是因为每个空结构体即使它的 size 为零,都必须具有唯一的内存地址。

因为 SampleStructsize 是 5 ,而 alignment 要求是它位于 4 字节边界上,鉴于此 Swift 就以 8 个字节的 stride 打包。

接着我们查看下类的内存布局有何不同:

class EmptyClass {}
​
MemoryLayout<EmptyClass>.size      // returns 8 (on 64-bit)
MemoryLayout<EmptyClass>.stride    // returns 8 (on 64-bit)
MemoryLayout<EmptyClass>.alignment // returns 8 (on 64-bit)class SampleClass {
    let number: Int64 = 0
    let flag = false
}
​
MemoryLayout<SampleClass>.size      // returns 8 (on 64-bit)
MemoryLayout<SampleClass>.stride    // returns 8 (on 64-bit)
MemoryLayout<SampleClass>.alignment // returns 8 (on 64-bit)

类是引用类型,因此 MemoryLayoutsize 为 8 个字节。

如果你想更详细地了解内存布局,欢迎查看 Mike Ash 的演讲,探索 Swift 内存布局

Unsafe Swift 中使用指针

指针封装了一个内存地址。涉及直接内存访问的类型会获得一个 unsafe 的前缀,因此指针类型名称是 UnsafePointer

这可能看起来很烦人,但它提醒开发者正在访问编译器未检查的内存。如果操作不当,可能会导致未定义的行为(Undefined Behavior),而不仅仅是可预测的崩溃。

Swift 的 UnsafePointer 不像 C 那样只提供一种以非结构化方式访问内存的单一类型,像 char *。Swift 包含近十种指针类型,每种都有不同的功能和用途。

开发者需要为其目的使用最合适的指针类型。这可以更好地传达意图、不易出错、避免未定义的行为。

不安全的 Swift 指针使用可预测的命名方案来描述指针的特征:mutable 或 immutable、raw 或类型化、 buffer 样式与否。总共有八种指针组合。

Pointer NameUnsafe?Write Access?Collection?Strideable?Typed?
UnsafeMutablePointer<T>yepyepnopeyepyep
UnsafePointer<T>yepnopenopeyepyep
UnsafeMutableBufferPointer<T>yepyepyepnopeyep
UnsafeBufferPointer<T>yepnopeyepnopeyep
UnsafeMutableRawPointeryepyepnopeyepnope
UnsafeRawPointeryepnopenopeyepnope
UnsafeMutableRawBufferPointeryepyepyepnopenope
UnsafeRawBufferPointeryepnopeyepnopenope

Unsafe[Mutable][Raw][Buffer]Pointer[<T>]

指针是内存地址,直接内存访问是 UnsafeMutable 意味可以写入; Raw 表示它指向一个字节块; Buffer 意味着它像一个集合一样工作; <T> 是类型化的指针。

使用原始(Raw)指针

我们使用 Unsafe Swift 指针来存储和加载两个整数:

// 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(
        byteCount: byteCount,
        alignment: alignment)
    // 4
    defer {
        pointer.deallocate()
    }
    
    // 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)")
    }
}
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

在上述代码中:

  1. 定义一些常量,这些常量包含:

    • count 保存要存储的整数个数。
    • stride 持有 Int type 的 stride
    • alignment 持有 Int type 的 alignment
    • byteCount 保存所需的总字节数。
  2. 添加了do范围级别,我们可以在后续示例中重用变量名称。

  3. UnsafeMutableRawPointer.allocate分配所需的字节。此方法返回一个UnsafeMutableRawPointer. 该类型的名称告诉我们此指针可以 load、store 或 mutate 原始字节。

  4. defer块确保可以正确地释放指针。这里需要手动进行内存管理而无法 依赖 ARC。

  5. storeBytes 并且 load,是存储和加载字节。我们可以通过 advanced 指针 stride 字节来计算第二个整数的内存地址。由于指针是 Strideable 的,我们还可以使用指针算术,例如:(pointer + stride).storeBytes(of: 6, as: Int.self)

  6. 一个 UnsafeRawBufferPointer 让我们像访问字节集合一样访问内存。这意味着我们可以遍历字节并使用下标访问它们。我们还可以使用高阶函数,例如 filtermapreduce等。我们使用原始指针初始化缓冲区指针。

即使 UnsafeRawBufferPointer 不安全,但我们仍然可以通过将其限制为特定类型来使其更安全。

使用类型化的指针

我们可以使用类型化指针来简化前面的示例:

do {
    print("Typed pointers")
    
    let pointer = UnsafeMutablePointer<Int>.allocate(capacity: count)
    pointer.initialize(repeating: 0, count: count)
    defer {
        pointer.deinitialize(count: count)
        pointer.deallocate()
    }
    
    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)")
    }
}
Typed pointers
value 0: 42
value 1: 6

请注意该示例的差异:

  • 我们使用带泛型参数的 UnsafeMutablePointer.allocate 让 Swift 知道正在使用指针来加载和存储 Int type 的值。
  • 我们必须在使用前初始化类型化内存并在使用后取消初始化。我们可以分别使用 initializedeinitialize 方法来执行此操作。只有非平凡类型(Non-trivial Type) 才需要取消初始化。但是这些是让我们的代码可以更好的面向未来,以防后期我们改变一些非平凡的类型,这通常不会有额外的成本,编译器会对其进行优化。
  • 类型化指针有一个 pointee 属性,它提供了一种类型安全的方式来加载和存储值。
  • advance 类型化指针时,我们可以简单地说明要 advance 的值的数量。指针可以根据它指向的值的类型来计算正确的步幅。同样,指针算术也有效。我们也可以使用(pointer + 1).pointee = 6
  • 类型化的缓冲区指针也是如此:它们迭代值而不是字节。

接下来我们将了解如何从不受约束的 UnsafeRawBufferPointer 到更安全的类型约束 UnsafeRawBufferPointer

将原始指针转换为类型化指针

我们并不总是需要直接初始化类型化指针,也可以从原始指针派生它们:

do {
    print("Converting raw pointers to typed pointers")
    
    let rawPointer = UnsafeMutableRawPointer.allocate(
        byteCount: byteCount,
        alignment: alignment)
    defer {
        rawPointer.deallocate()
    }
    
    let typedPointer = rawPointer.bindMemory(to: Int.self, capacity: count)
    typedPointer.initialize(repeating: 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)")
    }
}
Converting raw pointers to typed pointers
value 0: 42
value 1: 6

此示例与上一个示例类似,不同之处在于它首先创建了一个原始指针。 通过将内存绑定到所需的 Int 类型来创建类型化指针。通过绑定内存(BindMemory) ,我们可以以类型安全的方式访问它。

获取字节实例

如果我们有一个类型的现有实例,并且想要检查它的字节。我们可以使用名为 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)
        }
    }
}
Getting the bytes of an instance
25
0
0
0
1

这将打印出 SampleStruct 实例的原始字节。withUnsafeBytes(of:) 使我们可以在闭包内使用 UnsafeRawBufferPointerwithUnsafeBytes 也可用作 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) // prints checksum 4294967269
}

reduce 添加字节,然后 ~ 翻转位。 上述代码显示了这个概念。

Unsafe 的注意事项

在编写 Unsafe 代码时要小心避免未定义的行为。以下是一些错误代码的示例:

不要从 withUnsafeBytes 返回指针

// Rule #1
do {
    print("1. Don't return the pointer from withUnsafeBytes!")
    
    var sampleStruct = SampleStruct(number: 25, flag: true)
    
    let bytes = withUnsafeBytes(of: &sampleStruct) { bytes in
        return bytes // strange bugs here we come ☠️☠️☠️
    }
    
    print("Horse is out of the barn!", bytes) // undefined!!!
}

你不应该让指针脱离 withUnsafeBytes(of:) 闭包。 即使代码可能可以工作,将来也可能会导致奇怪的错误。

一次只能绑定一种类型

// Rule #2
do {
    print("2. Only bind to one type at a time!")
    
    let count = 3
    let stride = MemoryLayout<Int16>.stride
    let alignment = MemoryLayout<Int16>.alignment
    let byteCount = count * stride
    
    let pointer = UnsafeMutableRawPointer.allocate(
        byteCount: byteCount,
        alignment: alignment)
    
    let typedPointer1 = pointer.bindMemory(to: UInt16.self, capacity: count)
    
    // Breakin' the Law... Breakin' the Law (Undefined behavior)
    let typedPointer2 = pointer.bindMemory(to: Bool.self, capacity: count * 2)
    
    // If you must, do it this way:
    typedPointer1.withMemoryRebound(to: Bool.self, capacity: count * 2) {
        (boolPointer: UnsafeMutablePointer<Bool>) in
        print(boolPointer.pointee) // See Rule #1, don't return the pointer
    }
}

永远不要一次将内存绑定到两个不相关的类型。相反,使用 withMemoryRebound(to:capacity:) 之类的方法临时重新绑定内存。此外,将普通类型(如 Int)重新绑定到非普通类型(如类)是非法的,不要这样做。

不要越界

// Rule #3... wait
do {
    print("3. Don't walk off the end... whoops!")
    
    let count = 3
    let stride = MemoryLayout<Int16>.stride
    let alignment = MemoryLayout<Int16>.alignment
    let byteCount =  count * stride
    
    let pointer = UnsafeMutableRawPointer.allocate(
        byteCount: byteCount,
        alignment: alignment)
    let bufferPointer = UnsafeRawBufferPointer(start: pointer, count: byteCount + 1)
    // OMG +1????
    
    for byte in bufferPointer {
        print(byte) // pawing through memory like an animal
    }
}

始终存在的一个错误的问题,在 Unsafe 代码中会变得更加严重。

Unsafe Swift 示例 1:压缩

我们将使用 Unsafe Swift 来包装 C API。 Cocoa 包含一个 C 模块,它实现了一些常见的数据压缩算法。 这些包括:

  • LZ4 适用于速度很重要的情况。
  • LZ4A 适用于需要最高压缩比且不关心速度的情况。
  • ZLIB,平衡空间和速度。
  • 新的开源的 LZFSE ,可以更好地平衡空间和速度。

首先,我们将使用 Data 定义一个纯 Swift API:

import Foundation
import Compression

enum CompressionAlgorithm {
    case lz4   // speed is critical
    case lz4a  // space is critical
    case zlib  // reasonable speed and space
    case lzfse // better speed and space
}

enum CompressionOperation {
    case compression, decompression
}

/// return compressed or uncompressed data depending on the operation
func perform(
    _ operation: CompressionOperation,
    on input: Data,
    using algorithm: CompressionAlgorithm,
    workingBufferSize: Int = 2000)
-> Data?  {
    return nil
}

执行压缩和解压缩的函数,当前返回 nil。接下来添加代码:

/// Compressed keeps the compressed data and the algorithm
/// together as one unit, so you never forget how the data was
/// compressed.
struct Compressed {
    let data: Data
    let algorithm: CompressionAlgorithm
    
    init(data: Data, algorithm: CompressionAlgorithm) {
        self.data = data
        self.algorithm = algorithm
    }
    
    /// Compresses the input with the specified algorithm. Returns nil if it fails.
    static func compress(
        input: Data,with algorithm: CompressionAlgorithm)
    -> Compressed? {
        guard let data = perform(.compression, on: input, using: algorithm) else {
            return nil
        }
        return Compressed(data: data, algorithm: algorithm)
    }
    
    /// Uncompressed data. Returns nil if the data cannot be decompressed.
    func decompressed() -> Data? {
        return perform(.decompression, on: data, using: algorithm)
    }
}

Compressed 结构体储压缩数据和用于创建算法。这使得在决定使用哪种解压缩算法时不容易出错。接下来添加代码:

/// For discoverability, adds a compressed method to Data
extension Data {
    /// Returns compressed data or nil if compression fails.
    func compressed(with algorithm: CompressionAlgorithm) -> Compressed? {
        return Compressed.compress(input: self, with: algorithm)
    }
}

// Example usage:

let input = Data(Array(repeating: UInt8(123), count: 10000))

let compressed = input.compressed(with: .lzfse)
compressed?.data.count // in most cases much less than original input count

let restoredInput = compressed?.decompressed()
input == restoredInput // true

我们添加了一个名为 compressed(with:) 的方法,该方法返回一个可选的 Compressed 结构体。 这个方法简单地调用 Compressed 上的静态方法 compress(input:with:)

最后有一个例子,但它目前不起作用。向上滚动到我们输入的第一个代码块并开始执行 perform(_:on:using:workingBufferSize:)return nil 之前插入以下内容:

// set the algorithm
let streamAlgorithm: compression_algorithm
switch algorithm {
case .lz4:   streamAlgorithm = COMPRESSION_LZ4
case .lz4a:  streamAlgorithm = COMPRESSION_LZMA
case .zlib:  streamAlgorithm = COMPRESSION_ZLIB
case .lzfse: streamAlgorithm = COMPRESSION_LZFSE
}

// set the stream operation and flags
let streamOperation: compression_stream_operation
let flags: Int32
switch operation {
case .compression:
    streamOperation = COMPRESSION_STREAM_ENCODE
    flags = Int32(COMPRESSION_STREAM_FINALIZE.rawValue)
case .decompression:
    streamOperation = COMPRESSION_STREAM_DECODE
    flags = 0
}

这会将我们的 Swift 类型转换为压缩算法所需的 C 类型。接下来,将 return nil 替换为:

// 1: create a stream
var streamPointer = UnsafeMutablePointer<compression_stream>.allocate(capacity: 1)
defer {
    streamPointer.deallocate()
}

// 2: initialize the stream
var stream = streamPointer.pointee
var status = compression_stream_init(&stream, streamOperation, streamAlgorithm)
guard status != COMPRESSION_STATUS_ERROR else {
    return nil
}
defer {
    compression_stream_destroy(&stream)
}

// 3: set up a destination buffer
let dstSize = workingBufferSize
let dstPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: dstSize)
defer {
    dstPointer.deallocate()
}

return nil // To be continued

在上述代码中:

  1. 分配一个 compression_stream 并用 defer 处理释放。
  2. 使用 pointee 属性,获取 stream 并将其传递给 compression_stream_init 函数。 编译器在这里做了一些特别的事情:它使用 in-out & 标记来获取您的 compression_stream 并将其转换为 UnsafeMutablePointer<compression_stream>。 或者我们可以通过使用 streamPointer。 那么就不需要这种特殊的转换了。
  3. 最后,您创建一个目标缓冲区作为工作缓冲区。

接下来,通过将最终的 return nil 替换为:

// process the input
return input.withUnsafeBytes { srcRawBufferPointer in
    // 1
    var output = Data()
    
    // 2
    let srcBufferPointer = srcRawBufferPointer.bindMemory(to: UInt8.self)
    guard let srcPointer = srcBufferPointer.baseAddress else {
        return nil
    }
    stream.src_ptr = srcPointer
    stream.src_size = input.count
    stream.dst_ptr = dstPointer
    stream.dst_size = dstSize
    
    // 3
    while status == COMPRESSION_STATUS_OK {
        // process the stream
        status = compression_stream_process(&stream, flags)
        
        // collect bytes from the stream and reset
        switch status {
            
        case COMPRESSION_STATUS_OK:
            // 4
            output.append(dstPointer, count: dstSize)
            stream.dst_ptr = dstPointer
            stream.dst_size = dstSize
            
        case COMPRESSION_STATUS_ERROR:
            return nil
            
        case COMPRESSION_STATUS_END:
            // 5
            output.append(dstPointer, count: stream.dst_ptr - dstPointer)
            
        default:
            fatalError()
        }
    }
    return output
}

上述代码中:

  1. 创建一个包含输出的 Data 对象——压缩或解压缩的数据,具体取决于这是什么操作。
  2. 使用我们分配的指针及其大小设置源缓冲区和目标缓冲区。
  3. 在这里,除非它返回 COMPRESSION_STATUS_OK,否则就一直调用 compression_stream_process
  4. 我们将目标缓冲区复制到最终从此函数返回的输出中。
  5. 当最后一个数据包进来时,用 COMPRESSION_STATUS_END 标记,我们可能只需要复制目标缓冲区的一部分。

在此示例中,我们可以看到 10,000 个元素的数组被压缩到 153 个字节。

Unsafe Swift 示例 2:随机数

随机数对于许多应用都很重要。macOS 提供 arc4random,它可以产生加密的随机数。但是这个调用在 Linux 上不可用。 此外,arc4random 仅提供 UInt32 形式的随机数。 /dev/urandom 提供了无限的良好随机数来源。我们将读此文件并创建类型安全的随机数。

import Foundation

enum RandomSource {
  static let file = fopen("/dev/urandom", "r")!
  static let queue = DispatchQueue(label: "random")
  
  static func get(count: Int) -> [Int8] {
    let capacity = count + 1 // fgets adds null termination
    var data = UnsafeMutablePointer<Int8>.allocate(capacity: capacity)
    defer {
      data.deallocate()
    }
    queue.sync {
      fgets(data, Int32(capacity), file)
    }
    return Array(UnsafeMutableBufferPointer(start: data, count: count))
  }
}

系统中仅存在一个该文件,我们将文件变量声明为静态。当进程退出时,我们将依赖系统将其关闭。由于多个线程可能需要随机数,因此我们需要使用串行 GCD 队列来保护对它的访问。get 函数是实际工作发生的地方。首先,创建一个超出我们需要的未分配存储,因为 fgets 始终以 0 终止。接下来,从文件中获取数据,确保在对 GCD 队列进行操作时这样做。最后,将数据复制到标准数组,首先将其包装在可以充当序列的 UnsafeMutableBufferPointer 中。

到目前为止,这只会给你一个 Int8 值的数组。 现在我们要扩展它:

extension BinaryInteger {
    static var randomized: Self {
        let numbers = RandomSource.get(count: MemoryLayout<Self>.size)
        return numbers.withUnsafeBufferPointer { bufferPointer in
            return bufferPointer.baseAddress!.withMemoryRebound(
                to: Self.self,
                capacity: 1) {
                    return $0.pointee
                }
        }
    }
}

Int8.randomized
UInt8.randomized
Int16.randomized
UInt16.randomized
Int16.randomized
UInt32.randomized
Int64.randomized
UInt64.randomized

这为 BinaryInteger 协议的所有子类型添加了一个静态随机属性。首先,我们得到随机数,使用返回的数组字节,然后将 Int8 值重新绑定为请求的类型并返回一个副本。我们现在正在以安全的方式生成随机数。

参考