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

Hôm nay chúng ta sẽ cùng nhau đề cập đến việc sử dụng unsafe với swift, tại sao tôi lại đề cập đến vấn đề này? Bởi thông thường với swift khi các bạn sử dụng nó đều là Memory safe nên dường như chúng ta rất ít khi quan tâm đến bản chất của nó như thế nào mà chỉ khi có vấn đề gì đó phát sinh chúng ta mới đi tìm hiểu, vậy memory safe là gì? Tức là việc ngăn chặn việc truy suất trực tiếp đến vùng nhớ và luôn đảm bảo mọi thứ sử dụng phải được khởi tạo. Unsafe swift sẽ giúp bạn truy suất trực tiếp tới vùng nhớ thông qua con trỏ (pointer) nếu bạn cần dùng đến nó.

Bài hướng dẫn này sẽ giúp bạn tìm hiểu khái niện unsafe và cách dùng của nó với swift. Khi nói tới unsafe thì có thể bạn nghĩ ngay đến việc code không được an toàn và có thể nó sẽ không hoạt động tốt. Nhưng thực tế nó có nghĩa là bạn viết code theo kiểu này sẽ khiến việc sử dụng vủa bạn trở nên đa dạng hơn và ít bị trình biên dịch giới hạn so với cách viết thông thường.

Vậy dùng unsafe swift khi nào, đó chính là khi mà bạn cần tương tác với 1 ngôn ngữ unsafe như C chẳng hạn để tận dụng các chức năng của nó, với những dự án thực tế thì tôi cá rằng sẽ không thiếu các trường hợp bạn sẽ phải sử dụng đến unsafe swift.

Chúng ta sẽ bắt đầu với việc tạo 1 playground, tạm gọi là unsafeswift.playground, hãy chắc chắn rằng bạn đã import Foundation framework.

Memory Layout

Unsafe swift thao tác trực tiếp với hệ thống bộ nhớ, bạn có thể tưởng tượng bộ nhớ sẽ được sắp xếp giống như 1 loạt các ô nhớ, mỗi ô nhớ có đánh số tướng ứng, và mỗi ô nhớ có địa chỉ duy nhất, Đơn bị nhỏ nhất của bộ nhớ là 1 byte (bao gồm 8 bit 0 hoặc 1) mỗi 1 byte có thể lưu trữ giá trị từ 0 tới 255. Swift có 1 Memory layout có các thông tin như kích thước và sự liên kết của mọi thứ trong chương trình của bạn.

Hãy bắt đầu với việc gõ các dòng lệnh sau vào playground bạn đã tạo:

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> là kiểu generic, có 3 thông số được thể hiện khi chúng ta biên dịch là: size, alignment và stride. Số được trả về thể hiện số byte tương ứng của từng kiểu. Ví dụ, kiểu Int16 có size bằng 2, alignment cũng bằng 2. Điều này có nghĩa 1 địa chỉ bắt đầu ở 1 số chẵn (chia hết cho 2) Ví dụ, Kiểu dữ liệu Int16 được khởi tạo tại địa chỉ số 100, nó không thể là 101 bởi vì nó không chia hết cho alignment. Khi bạn lưu nhiều số kiểu Int16 , chúng sẽ tạo thành các cụm theo stride (bước nhảy). Đối với những kiểu cơ bản thì nó có chung size và stride.

Tiếp theo chúng ta sẽ tìm hiểu kiểu được định nghĩa bởi người dùng, thêm đoạn code này vào playground của bạn:

struct EmptyStruct {}

MemoryLayout<EmptyStruct>.size      // returns 0
MemoryLayout<EmptyStruct>.alignment // returns 1
MemoryLayout<EmptyStruct>.stride    // returns 1

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

Đầu tiên là EmptyStruct có size = 0, nó có thể đặt ở bất kỳ vị trí nào trong các ô nhớ bởi, alignment = 1 (tất cả các số điều chia hết cho 1, như khái niệm khởi tạo đã nói ở trên). Stride là 1 bởi EmptyStruct bạn tạo phải có 1 địa chỉ duy nhất cho dù nó có size = 0. Trường hợp với SampleStruct size = 5 nhưng stride lại bằng 8. Điều này được tạo ra bởi alignment = 4, khi khởi tạo liên tiếp các biến SampleStruct này trong khoảng 8 byte sẽ đáp ứng được.

Tiếp theo hãy thử với đoạn code sau:

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: Bool = 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)

Với môi trường 64 bit thì các kiểu dữ liệu có kích thước 8 byte Để hiểu rõ hơn nữa về memory layout các bạn có thể đọc tại đây, hoặc có thể chờ bản dịch sắp tới của mình: https://news.realm.io/news/goto-mike-ash-exploring-swift-memory-layout/

Tìm hiểu về con trỏ (pointer)

Với con trỏ thì khái niệm sẽ tương đương nhau với những ngôn ngữ có sử dụng, con trỏ trỏ tới 1 địa chỉ vùng nhớ, các loại có truy suất trực tiếp tới địa chỉ vùng nhớ sẽ có tiền tố unsafe, hay UnsafePointer. Trong khi việc gõ thêm nữa sẽ khiến bạn cảm thấy phiền, nó cho bạn biết rằng bạn đang truy suất vào vùng nhớ không được biên dịch, điều này đồng nghĩa với việc nếu bạn không thực hiện đúng sẽ dẫn tới những hành vi không mong muốn đôi khi khá nguy hiểm, và cũng có thể là không đoán trước được, nên việc lập trình với con trỏ luôn phải làm rất cẩn thận. Các nhà thiết kế ra Swift đã tạo ra 1 kiểu UnsafePointer duy nhất và làm cho nó tương ứng với C char * , vậy có thể truy cập bộ nhớ theo cách không có cấu trúc? Họ không làm như thế, thay vào đó Swift chứ ~10 loại con trỏ khác nhau, mỗi loại có khả năng và mục đích khác nhau, như vậy tuỳ từng trường hợp bạn sẽ chọn 1 loại con trỏ phù hợp, nó sẽ giúp bạn giảm thiểu tối đa các lỗi phát sinh không mong muốn do con trỏ. Còn việc đặt tên các con trỏ cũng khá là clear, gần như bạn biết được chức năng của nó khi biết tên nó, con trỏ đó có thể thay đổi hoặc không, dạng raw hay kiểu biết trước, buffer style hay không.

Các loại con trỏ được list ra bên dưới để bạn có thể tham khảo: Ok Tiếp theo hãy cùng nhau tìm hiểu thêm về các loại con trỏ đã được nói ở trên xem chức năng cụ thể nó là gì?

Đầu tiên là RAW POINTER

Thêm đoạn code sau vào playground của bạn:


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

Trong ví dụ này chúng ta sử dụng Unsafe pointer để lưu 1 số nguyên sau đó lại print nó ra. Hãy xem chuyện gì đang xảy ra: 1/ các hằng số thì luôn cố định khai báo với từ khoá let biến count chính là số nguyên được lưu trữ stride là bước nhảy kiểu Int, đã được nhắc đến ở đầu bài alignment: của Int byteCount: là tổng số byte cần thiết

Trong khối lệnh “do” Khởi tạo 1 unsafe pointer với byteCount và alignment trong khối defer {} để đảm bảo con trỏ được dellocated, bởi khi bạn sử dụng pointer thì ARC không giúp bạn xử lý cái này đâu, nên đừng có mà quên.

Mục 5: Địa chỉ bộ nhớ của số nguyên thứ 2 được tính bằng cách advancing các byte theo stride

Mục 6: UnsafeRawBufferPointer cho phép bạn truy cập bộ nhớ theo cách xem nó như 1 tập hợp các byte, có nghĩa là bạn có thể duyệt qua các byte, truy cập đến chúng bằng cách sử dụng enumerated.

bạn chạy và xem kết quả được in ra.

Tiếp theo: Typed Pointer

Hãy xem 1 ví dụ đơn giản ứng dụng typed pointer, thêm đoạn code sau vào playground:

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

Lưu ý những điển khác biệt sau: Bộ nhớ được cấp phát dùng phương pháp UnsafeMutablePointer.allocate. Các generic params cho swift biết con trỏ sẽ dùng để thao tác với kiểu Int. Bổ nhớ phải được cấp phát trước khi sử dụng, phải giải phóng sau khi sử dụng (tương tự trong khối defer) Typed Pointer là 1 pointer an toàn để lưu và thao tác trên các giá trị

Chuyển đổi con trỏ raw sang typed

Typed pointer không nhất thiết phải được khởi tạo trực tiếp, chúng cũng có thể được gán từ con trỏ raw. Hãy cùng xem đoạn code sau:

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

Ở ví dụ này cũng tương tự cái trước, ngoài trừ việc ban đầu ta khởi tạo 1 con trỏ raw, con trỏ typed được tạo ra bằng cách gán reference con trỏ raw với kiểu dữ liệu Int, nó có thể đưa truy cập 1 cách an toàn, sau khi gán reference sang kiểu int xong bạn có thể sử dụng pointee như thường.

Có gì trong phần 2:

  • Tìm hiểu về các quy tắc khi sử dụng unsafe
  • Nói thêm về cách sử dụng con trỏ (getting the bytes of an instance, computing a checksum, and some example)

Bài viết được tổng hợp từ https://www.raywenderlich.com/148569/unsafe-swift thanks for reading. Hẹn gặp lại tại phần 2.