本文将带你领略 Swift 的 “Unsafe” 特性。这里的 “Unsafe“ 可能有点让人困惑,它并不意味着你写的代码即危险又糟糕,相反它是在提醒你,提醒你要格外小心地编写自己的代码,因为编译器将不在帮你进行一些必要的审查。
在运用 Swift 一些非安全特性与诸如 C 这类非安全语言交互之前,我们还需要了解一些额外的与运行相关的知识。本文讨论的是一个进阶性的话题,如果有 Swift 基础,那么你会更好的理解本文所涉内容。C 语言开发经验也会有所帮助,但不是必须的。
开始(Getting Started)
本文由三个 playgrounds 构成。首先,会通过几段代码来熟悉一些内存排布和非安全指针操作的相关知识。其次,会把一个用于数据流压缩的 C API 封装成 SWift API。最后, 创建一个全平台的随机数生成器用于取代arc4random, 并通过 Unsafe Swift 相关技术向用户隐藏细节.
首先新建一个 playground, 命名为 UnsafeSwift。 平台任意, 本文所涉的代码均无平台限制。接下来导入 Foundation 框架。
let bufferPointer = UnsafeBufferPointer(start: pointer, count: count) for (index, value) in bufferPointer.enumerated() { print("value \(index): \(value)") } }
注意以下不同:
通过UnsafeMutablePointer.allocate方法开辟了一块内存. 而传入的参数是告诉swift该指针用于装载和存储 Int 类型
类型明确了的内存,在使用和销毁前都必需初始化,初始化和销毁可通过initialize 和 deinitialize 方法来完成。 Update: as noted by user atrick in the comments below, deinitialization is only required for non-trivial types. That said, including deinitialization is a good way to future proof your code in case you change to something non-trivial. Also, it usually doesn’t cost anything since the compiler will optimize it out.
// Rule #2 do { print("2. Only bind to one type at a time!")
letcount = 3 letstride = MemoryLayout<Int16>.stride let alignment = MemoryLayout<Int16>.alignment let byteCount = count * stride
let pointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: 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 } }
绝不要将内存同时绑定给两个不相关的类型。这被称为类型双关而 Swift 不喜欢双关。但可以通过 withMemoryRebound(to:capacity:) 方法暂时地重新绑定内存。同样,将一个普通类型(例如:Int)重新绑定到一个非普通类型(例如:类)也是不合法的。所以千万别这么做。
千万别越界(Don’t walk off the end… whoops)!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// Rule #3... wait do { print("3. Don't walk off the end... whoops!")
letcount = 3 letstride = MemoryLayout<Int16>.stride let alignment = MemoryLayout<Int16>.alignment let byteCount = count * stride
let pointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment) let bufferPointer = UnsafeRawBufferPointer(start: pointer, count: byteCount + 1) // OMG +1????
for byte in bufferPointer { print(byte) // pawing through memory like an animal } }
在已出现的 off-by-one 错误中,尤以不安全代码最糟糕。所以务必小心审查,测试你的代码!
不安全的Swift 示例 1: 压缩(算法)(Unsafe Swift Example 1: Compression)
接下来我们运用之前所讲的知识来对一个 C API 进行封装。Coca 框架中包含一个 C 模块,其实现了一些常用的压缩算法。例如 LZ4压缩算法速度最快,LZ4A算法的压缩比最高,但相对速度较慢,ZLIB算法在时间和压缩比上比较均衡,此外还有一个新的(开源)LZFSE算法,更好的平衡了空间和压缩速率。
enumCompressionAlgorithm{ case lz4 // speed is critical case lz4a // space is critical case zlib // reasonable speed and space case lzfse // better speed and space }
enumCompressionOperation{ case compression, decompression }
// return compressed or uncompressed data depending on the operation funcperform(_ operation: CompressionOperation, on input: Data, using algorithm: CompressionAlgorithm, workingBufferSize: Int = 2000) -> Data? { returnnil }
// Compress the input with the specified algorithm. Returns nil if it fails. staticfunccompress(input: Data, with algorithm: CompressionAlgorithm) -> Compressed? { guardlet data = perform(.compression, on: input, using: algorithm) else { returnnil } returnCompressed(data: data, algorithm: algorithm) }
// Uncompressed data. Returns nil if the data cannot be decompressed. funcdecompressed() -> Data? { return perform(.decompression, on: data, using: algorithm) }
funcperform(_ operation: CompressionOperation, on input: Data, using algorithm: CompressionAlgorithm, workingBufferSize: Int = 2000) -> Data? {
// 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 }
// 1: create a stream var streamPointer = UnsafeMutablePointer<compression_stream>.allocate(capacity: 1) defer { streamPointer.deallocate(capacity: 1) }
// 2: initialize the stream var stream = streamPointer.pointee var status = compression_stream_init(&stream, streamOperation, streamAlgorithm) guard status != COMPRESSION_STATUS_ERRORelse { returnnil } defer { compression_stream_destroy(&stream) }
// 3: set up a destination buffer let dstSize = workingBufferSize let dstPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: dstSize) defer { dstPointer.deallocate(capacity: dstSize) }