Paulo Andrade

Keeper of Secrets.

twitter github stackoverflow linkedin email
Unsafe Swift
Jan 7, 2020
9 minutes read

Swift Aveiro’s 2020 event was just announced and I thought now would be a good time to convert my workshop from last year’s event to a blog post.

The workshop was divided in two parts: working with pointers and interacting with C libraries. This post covers the first part.

If you’ve ever encountered the dreadful UnsafeMutableRawBufferPointer or one of its friends and ran to stackoverflow… then this post is for you!

Memory Layout

Before we get started it’s important to understand a few things about memory. Take this Foo struct for example.

struct Foo {
    let a: UInt8
    let b: UInt16
}
let t = Foo(a: 42, b: 4321)

It’s laid out in memory like so:

42 Unused 4321
0x...100 0x...101 0x...102 0x...103

Note how the second byte at address 0x...101 is unused. This happens because of memory alignment.

This article does a pretty good job on explaining this concept but to put it simply: your CPU reads memory in chunks1 and if the value it wants to read spans more than one chunk it has to work harder. So the compiler will lay out the struct’s properties in such a way that avoids that.

You can check the alignment of any given object by using Swift’s MemoryLayout<T> enum. For example, MemoryLayout<Foo>.alignment returns 2 which means it must be placed on an even memory address.

Besides alignment there are two other properties on MemoryLayout that are worth discussing: size and stride.

To understand what these are lets look at another example. Consider the following structs:

struct Bar1 {
    let i: Int
    let b1: Bool
    let b2: Bool
}
struct Bar2 {
    let b1: Bool
    let b2: Bool
    let i: Int
}
struct Bar3 {
    let b1: Bool
    let i: Int
    let b2: Bool
}

All three structs declare the same three properties but in a different order. In Swift an Int is 8 bytes and a Bool is 1 byte so one might think the size of any of these structs is 10 (8 + 1 + 1). But because of alignment that’s not actually the case. Here are the actual values of size and stride:

Bar1 Bar2 Bar3
Size 10 16 17
Stride 16 16 24

Only Bar1 has the expected size of 10. We can already conclude there are more and less efficient ways to declare what’s essentially the same struct. The order of the properties matters! Lets see how each of them is laid out to really understand why this is so.

Bar1:

i b1 b2 Unused
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115

Bar2:

b1 b2 Unused i
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115

Bar3:

b1 Unused i b2 Unused
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123

Because an Int has an alignment of 8 and a Bool has an alignment of 1 (it can be placed anywhere) all the three structs have a different size. The size value is how many bytes we need to store all of the struct’s properties – from the first byte of the first property to the last byte of the last property. If you were to allocate a single Bar1 you only need to allocate 10 bytes of memory, 16 for Bar2 and 17 for Bar3.

What if you needed an array of Bar1? That’s where stride comes in. Stride is the how many bytes you need for each item of the array. Stride is different from size because the alignment of any of these structs is 8 (the largest alignment value of its properties). Which means they must be placed on an address which is a multiple of 8.

So if you needed to allocate an array of 10 Bar1 structs you’d need 10 times the stride (10 * 16) which is 160 bytes. For Bar3 it would take 240, that’s ~50% less efficient.

So far we’ve only looked at structs but it’s interesting to see what the memory layout a class type is.

class Foo {
    let a: UInt8
    let b: UInt16
}
MemoryLayout<Foo>.size // 8
MemoryLayout<Foo>.alignment // 8
MemoryLayout<Foo>.stride // 8

As expected, because classes are reference types, their memory layout is simply the layout of a pointer.

“Unsafe” pointers

Now let’s look at “unsafe” pointers in Swift. We’ll look at both raw and typed pointers. But first a word about the why Swift prefixes this API with “Unsafe”.

That prefix is there to let you know that the compiler doesn’t have your back while you’re dealing with these pointers. And that it’s easy to shoot yourself in the foot. If you have any C experience you probably wouldn’t call this unsafe, you’d simply call it programming. Needless to say it is perfectly feasible to write correct and safe code using this API.

Raw pointers

Raw pointers are simply pointers to a chunk of memory. These pointers have no knowledge of what’s stored in that chunk. To them it’s just bytes. There are four of them: UnsafeRawPointer, UnsafeMutableRawPointer, UnsafeRawBufferPointer and UnsafeMutableRawBufferPointer.

The UnsafeRawPointer points to an immutable chunk of memory. If you need mutation then you need to use the UnsafeMutableRawPointer.

The buffer variants are similar except they treat the memory as a collection of bytes. And when I say “collection” I mean the Swift’s Collection protocol.

Lets see how we could create an array of 4 integers using raw pointers.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
let count = 4
let stride = MemoryLayout<Int>.stride
let alignment = MemoryLayout<Int>.alignment

let ptr = UnsafeMutableRawPointer.allocate(byteCount: count*stride, alignment: alignment)

for i in 0..<4 {
    // store the square of the index
    ptr.advanced(by: i*stride)
        .storeBytes(of: i*i, as: Int.self)
}

for i in 0..<4 {
    // print the values at each index
    let value = ptr.load(fromByteOffset: i*stride, as: Int.self)
    print("Int at \(i): \(value)")
}

ptr.deallocate()

On line 5 we allocate the space for the array. Note how we use stride instead of size to calculate the total size of the array2.

After that, on line 9, we advance the pointer in multiples of stride to store the integer value for that position.

Whenever we write (line 10) or read (line 15) using a raw pointer we need to tell it what the type is so it knows how to handle the underlying bytes. By specifying the type it knows how much to read/write, and in this particular case of an Int it will also handle endianness for us.

Running this code on a playground outputs:

Int at 0: 0
Int at 1: 1
Int at 2: 4
Int at 3: 9

An awful lot of work to do the equivalent of Array(0..<4).map { $0*$0 } isn’t it?

Typed pointers

Before we talk about typed pointers we need to learn about memory states first. A pointer can be in one of 4 states:

Unbound Bound
Uninitialised Unbound / Uninitialised Bound / Uninitialised
Initialised Unbound / Initialised Bound / Initialised

So far we’ve only used raw pointers, so we’ve only been in the unbound and uninitialised state. Typed pointers on the other hand are bound to a type.

Just like their raw counterparts, there are four typed pointers: UnsafePointer<T>, UnsafeMutablePointer<T>, UnsafeBufferPointer<T> and UnsafeMutableBufferPointer<T>.

You can think of using a typed pointer as embedding the information of the underlying type in the pointer itself. Instead of using the load and store methods of the raw pointer you used the pointee var to read and write from/to memory in a type safe manner.

Lets take our previous pointer to 4 integers, which is an UnsafeMutableRawPointer, and bind it to the Int type to get back an UnsafeMutablePointer<Int> to work with. We can then use it to double all items of the array:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
let intPtr = ptr.bindMemory(to: Int.self, capacity: 4)

for i in 0..<4 {
    let idxIntPtr = (intPtr + i)
    let prev = idxIntPtr.pointee
    idxIntPtr.pointee *= 2
    print("Int at \(i) was \(prev) and is now: \(idxIntPtr.pointee)")
}
/* 
Int at 0 was 0 and is now: 0
Int at 1 was 1 and is now: 2
Int at 2 was 4 and is now: 8
Int at 3 was 9 and is now: 18
*/

Using typed pointers instead of raw pointers brings many benefits, for instance they:

  1. Abstract away memory layout. Note how on line 4 we get a pointer to the integer at the current index simply by using pointer arithmetic. No need to think about stride when using typed pointers.
  2. Provide type safety. Replacing line 6 with idxIntPtr.pointee = "2" would result in a compiler error.
  3. Manage reference counting when needed.

That last one is important, and is our segue to talk about pointer initialisation.

It’s a lot easier to understand how initialisation is related to memory management if we look at another example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Foo {
    var integer: UInt8
    init(_ i: UInt8 = 0) {
        integer = i
    }
}
let fooPtr = UnsafeMutablePointer<Foo>.allocate(capacity: 2)
fooPtr.initialize(repeating: Foo(0), count: 2)
fooPtr.pointee = Foo(1)
(fooPtr+1).pointee = Foo(2)
fooPtr.deinitialize(count: 2)
fooPtr.deallocate()

Lets go over what’s happening here line by line.

  • First we declare our class Foo. Classes are reference types so they participate in reference counting.
  • On line 7 we allocate a typed pointer capable of storing two instances of Foo. Or to put it more precisely, they are capable of storing the address (pointers) of two instances of Foo.
  • On line 8 we initialise the pointer by having both addresses point to a single instance of Foo(0). At this point, Foo(0) has a reference count of 2.
  • On line 9, we replace the first address to point to a new instance of Foo(1). This single assignment will automatically decrement the reference count of Foo(0). At this point, both Foo(0) and Foo(1) have a reference count of 1.
  • On line 10, we replace the first address to point to a new instance of Foo(1). Again the reference count of Foo(0) is decremented. At this point, Foo(0) is deallocated and Foo(1) and Foo(2) have a reference count of 1.
  • On line 11 we deinitialize the pointer. Causing both the reference counts of both Foo(1) and Foo(2) to be decremented. Since the reference counts are now 0, both objects are deallocated.
  • Finally, on line 12 we deallocate the pointer itself.

Who said pointers weren’t fun?

If you have comments, questions or praise about this post I’d love to hear it on Twitter!


  1. For example, on a 64 bit machine this chunk should be 64 bit. [return]
  2. Although for an Int both size and stride are the same value. [return]


Back to posts