[Swift3] Unsafe Swift: Sử dụng con trỏ(pointers) Và cách tương tác với C [Phần 2]

Tiếp theo phần 1, hôm nay chúng ta sẽ tiếp tục những phần đang còn dang dở, ở phần 1 chúng ta đã biết về các loại con trỏ và cách để chuyển đổi qua lại giữa các loại với nhau, còn trong phần 2(cũng là phần cuối) này chúng ta sẽ tìm hiểu: Lấy về byte của Instance Bạn có 1 thể hiện của biến bạn đã biết kiểu của nó và bạn muốn lấy về byte từ kiểu dữ liệu đó, điều này có thể thực hiện bằng cách gọi qua phương thức withUnsafeBytes(of:) Tiếp tục với playground của bạn, thêm các dòng code dưới đây:

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)
    }
  }
}

Với thao tác trên sẽ in ra các raw byte của SampleStruct, phương thức withUnsafeBytes(of:) cho phép bạn truy cập vào UnsafeRawBufferPointer mà bạn có thể sử dụng trong closure, withUnsafeBytes(of:) cũng có thể được dùng với Array và Data bất kỳ.

Tính toán 1 checksum

khi dùng withUnsafeBytes(of:) bạn có thể trả về 1 giá trị (return trong closure). Hãy xem ví dụ về việc sử dụng nó để tính tổng các bytes trong Struct (32-bit) Thêm đoạn code sau vào playground:

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 cộng các byte lại với nhau theo thứ tự giảm trong mảng, cùng với toán tử ~ làm nhiệm vụ đảo bit, đây là ví dụ rất tốt để thể hiện việc tính toán checksum trên các byte

Ba quy tắc quan trọng trong việc sử dụng Unsafe swift

Khi thao tác với Unsafe bạn cần phải cực kỳ thận trọng, để tránh những trường hợp không mong muốn. Tôi có thể chỉ ra cho bạn 1 số các tình huống mà code không được tốt cho lắm.

Không return về 1 con trỏ từ trong closure của phương thức withUnsafeBytes, trông nó như sau:

 // 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 !!!
}

bạn không bao giờ return cả đống bytes trong khối withUnsafeBytes, có thể bạn chạy thấy ok vào thời điểm đó nhưng tương lai có thể gánh chịu hậu quả nghiêm trọng

Chỉ bind về 1 kiểu trong 1 lần chạy, không bind con trỏ về những kiểu khác nhau

// 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(bytes: byteCount, alignedTo: alignment)

  let typedPointer1 = pointer.bindMemory(to: UInt16.self, capacity: count)

  // Vui lòng giữ đúng quy tắc  (Hành vì kiểu này có nghĩa là gì?)
  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
  }
}

Bạn có thể thấy 2 loại Uint16 và Bool không liên quan gì tới nhau, không bao giờ được bind như thế, điều này gọi là "Type Punning", swift không phải trò đùa, nó rất rõ ràng. Thay vào đó, bạn có thể tạm thời khôi phục bộ nhớ bằng method withMemoryRebound(to:capacity:), ngoài ra các quy tắc còn nói không được ép 1 kiểu basic như int, bool về 1 kiểu khác hẳn như struct, class

3. Không bao giờ vượt khỏi phạm vi, hãy kiểm soát nó 1 cách cẩn thận

// 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(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
  }
} 

hãy định hình rõ mục đích khi code, bufferPointer, countByte + 1 có ý nghĩa gì? hãy cẩn thận khi làm điều đó.

Ví dụ thực tế sử dụng Unsafe swift: Nén dữ liệu Chúng ta có thể biết Cocoa bao gồm 1 module C thực hiện 1 số thuật toán nén dữ liệu thông thường, các kiểu nén này có LZ4, chúng ta dùng khi quan tâm đến tốc độ của ứng dụng, nhưng tỷ lệ nén lại thấp, LZ4A có tỷ lệ nén cao và tốc độ thì chậm, còn với LZFSE thì nó cân bằng được 2 yếu tố vừa nói. tạo thêm 1 playground và sử dụng đoạn code sau:

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
}

Chức năng nén và giải nén được thực hiện nhưng đang trả về nil. Chúng ta sẽ thêm vào 1 số mã Unsafe, thêm vào cuối playground của bạn:

// 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
  }
  
  // Compress 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)
  }

}

Cấu trúc nén lưu trữ cả dữ liệu nén và thuật toán đã được sử dụng để tạo ra nó. Điều đó làm cho nó dễ bị lỗi khi quyết định sử dụng thuật toán giải nén nào. Tiếp theo, thêm mã sau:

// For discoverability, add 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(bytes: Array(repeating: UInt8(123), count: 10000))

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

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

extension này sẽ chỉ định bạn dùng thuật toán nào để nén kèm luôn example, nhưng example chưa làm việc cho đến khi chúng ta sửa nó đôi chút:

func perform(_ 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
  }
  
  return nil /// To be continued
}

với việc sử dụng này chúng ta thực sự đã kết nối được Swift và C để phát huy tác dụng của C trong thuật toán nén. Tiếp theo thay thế dòng return nil bằng đoạn code sau:

// 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_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(capacity: dstSize)
}

return nil /// Chưa xong đâu, xem giải thích đã

Điều gì đã xảy ra ở bên trên:

Khởi tạo 1 stream kèm theo việc giải phóng trong khối defer sử dụng thuộc tính pointee chuyển vào hàm compression_stream_init, theo thuật toán nén đã định sẵn, qua hàm này biến thành 1 UnsafeMutablePointer<compression_stream> cuối cùng tạo buffer kèm với khối defer

Hoàn thành function perform bằng cách thay thế return nil bằng:

// process the input
return input.withUnsafeBytes { (srcPointer: UnsafePointer<UInt8>) in
  // 1
  var output = Data()
  
  // 2
  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
}

Hãy phân tích chút những gì đang xảy ra:

tạo 1 output có thể là dữ liệu nén hoặc giải nén tuỳ thuộc vào giá trị bạn truyền vào Thiết lập buffer nguồn và đích, khởi tạo với size của nó Cứ gọi compression_stream_process cho tới khi sate nó trả về là COMPRESSION_STATUS_OK Khi chạm đến cuối gói tin sẽ được đánh dấu bằng COMPRESSION_STATUS_END, chỉ 1 phần của buffer đích có thể cần dc sao chép. Trong ví dụ ta thấy mảng 100000 phần tử dc nén xuống còn 153 bytes, con số không đến nỗi nào.

Ngoài ra còn có ví dụ về việc sử dụng để tạo số random, bạn có thể tham khảo thêm tại link nguồn mình có nói ở phần 1, happy coding, thanks all.