milen.me
milen.me

Swift Unsafe Pointers

Permalink | RSS

Swift 3 has several different pointer-like types that allow interfacing with the outside world. This article provides a concise overview of UnsafeRawPointer, UnsafePointer, AutoreleasingUnsafeMutablePointer, Unmanaged and OpaquePointer.

Note that UnsafeRawPointer and UnsafePointer have mutable variants, namely UnsafeMutableRawPointer and UnsafeMutablePointer. I'll refer to both variants using the shorter names and clarify as required.

In Swift 3, the pointer types now represent non-nullable pointers (UnsafePointer, UnsafeMutablePointer, AutoreleasingUnsafeMutablePointer, OpaquePointer, Selector and NSZone). This means you don't have to check whether they are nil as nullability is expressed via the type system using optionals. This allows the preservation of nullability annotations when interacting with legacy APIs.

UnsafeRawPointer

UnsafeRawPointer represents a untyped pointer (void*) and is declared as a struct:

public struct UnsafeRawPointer : Strideable, Hashable {
  // ... more code ...
  
  public func distance(to x: UnsafeRawPointer) -> Int
  public func advanced(by n: Int) -> UnsafeRawPointer
}

UnsafePointer

UnsafePointer represents a typed pointer. It's declared as a generic struct. Here's the mutable variant:

public struct UnsafeMutablePointer<Pointee> : Strideable, Hashable {
  // ... more code ...
  
  public var pointee: Pointee { get nonmutating set }
  
  public static func allocate(capacity count: Int) -> UnsafeMutablePointer<Pointee>
  public func deallocate(capacity: Int)
  
  public func initialize(to newValue: Pointee, count: Int = default)
  public func deinitialize(count: Int = default) -> UnsafeMutableRawPointer
}

Memory State

The memory referenced by an UnsafeMutablePointer can be in one of the following states:

  • Deallocated.
  • Allocated but not initialised.
  • Allocated and initialised.

It's only safe to use the pointee property if and only if the memory is allocated and initialised. This means you have to call allocate(), initialize(), deinitialize() and deallocate() in the correct order.

// Allocate storage on the heap for UInt64 (8 bytes) and point to it
let uint64Pointer = UnsafeMutablePointer<UInt64>.allocate(capacity: 1)

// The memory state is undefined, so before using the pointee property, it must be initialised
uint64Pointer.initialize(to: 0xAABBCCDD)
print("int value: " + String(format:"%X", uint64Pointer.pointee)) // prints AABBCCDD

// Treat the pointer as a pointer to UInt8
uint64Pointer.withMemoryRebound(to: UInt8.self, capacity: 1) { (byte: UnsafeMutablePointer<UInt8>) in
  // When executing on a little endian machine, it will print DD,CC,BB,AA
  // On big endian, it will print AA,BB,CC,DD
  print("first byte value: " + String(format:"%2X", byte.pointee))
  print("second byte value: " + String(format:"%2X", byte.advanced(by: 1).pointee))
  print("third byte value: " + String(format:"%2X", byte.advanced(by: 2).pointee))
  print("fourth byte value: " + String(format:"%2X", byte.advanced(by: 3).pointee))
}

uint64Pointer.deinitialize(count: 1)

// `pointee` should not be used after being deinitialised
// `uint64Pointer` can now be re-initialised

uint64Pointer.deallocate(capacity: 1)

// `uint64Pointer` cannot be used anymore

Pointers to Reference Types

It's important to realise that when UnsafePointer is used with reference types, it does not in fact point to an object but rather, it points to a managed reference (which itself points to an object). Reference type lvalues represent managed pointers which perform reference counting, so the storage of let x: NSObject translates to NSObject *x. Consequently, UnsafeMutablePointer<NSObject> can be thought of as pointing to a var x: NSObject and assigning through pointee is equivalent to assigning to var x (i.e., referencing counting will be performed). Let's illustrate with some code.

// Allocate storage for `var x: NSObject` (NSObject *x), the memory state is undefined
let objVarPointer = UnsafeMutablePointer<NSObject>.allocate(capacity: 1)

// Set the initial value to a new NSObject, equivalent to `var x = NSObject()`.
// Underneath, it's equivalent to the following pseudocode:
//   x = obj
//   retain(x)
objVarPointer.initialize(to: NSObject())
print("\(objVarPointer.pointee.description)")

// Assign another obj, equivalent to `x = NSObject()`.
// Underneath, it's equivalent to the following pseudocode:
//   release(x)
//   x = obj
//   retain(x)
objVarPointer.pointee = NSObject()
print("\(objVarPointer.pointee.description)")

// It's crucial to de-initialise ref type pointers. Underneath, equivalent to:
//   release(x)
objVarPointer.deinitialize(count: 1)

// Deallocate storage for the managed reference
objVarPointer.deallocate(capacity: 1)

AutoreleasingUnsafeMutablePointer

AutoreleasingUnsafeMutablePointer is a typed pointer to references and it must point to storage that does not own the referenced object. By contrast, UnsafeMutablePointer assumes the referenced storage owns a ref count to the referenced object.

Unmanaged

Unmanaged represents an unmanaged reference to an instance of a particular type. It is declared as a generic struct restricted to reference types. Its job is to facilitative the conversion from unmanaged to managed references and vice versa.

public struct Unmanaged<Instance : AnyObject> {
  // ... more code ...
  
  public static func passRetained(_ value: Instance) -> Unmanaged<Instance>
  public static func passUnretained(_ value: Instance) -> Unmanaged<Instance>
    
  public func takeUnretainedValue() -> Instance
  public func takeRetainedValue() -> Instance
  
  public static func fromOpaque(_ value: UnsafeRawPointer) -> Unmanaged<Instance>
  public func toOpaque() -> UnsafeMutableRawPointer
}

If you receive an unmanaged CFType reference from an unannotated API, you can convert it to a managed one using the take family of methods. Use takeUnretainedValue() when you're not responsible for releasing the instance (those usually come from Get family of functions). Use takeRetainedValue() when you're responsible for releasing the instance (those usually come from Copy or Create family of functions).

To go in the other direction, use the take pass family of methods. If there's an unannotated CFType API which expects to receive a +1 reference, you need to use passRetained(). Note that the receiver of the unmanaged reference is responsible for balancing the +1, otherwise the object will leak. Most of time, you need to use passUnretained() when interacting with APIs which expect a +0 reference.

Finally, fromOpaque() and toOpaque() allow you to convert between raw pointers (i.e., void* pointers) and Unmanaged references.

OpaquePointer

OpaquePointer is used for C pointers that cannot be represented in Swift (e.g., pointers to incomplete struct types). There's very little you can do with this type: it can be created, compared and debug logged.

← Back to Writings

Any opinions and viewpoints expressed, explicitly or implicitly, are not endorsed by and do not represent any of my previous, current or future employers.