-
Notifications
You must be signed in to change notification settings - Fork 4
Pointers
Prev: Integers
Given Swift's stance on safety, and it's focus on value types, reference types, and optionals to reflect the concept of null, you would be forgiven for believing that it has no concept of pointers.
But it actually has an entire family of types for working with them. Indeed, when developers first encounter then, they can be overwhelmed by the size and apparent complexity of the API surface.
A little study reveals just how expressive this API allows the language to be, while still retaining the readability and safety that Swift is known for.
The basic Swift pointer type for any type T
is named UnsafePointer<T>
, the name simultaneously reminding us that we're dealing with a pointer, and that all such dealings are inherently likely to be unsafe.
The value of the pointer itself is an address in memory, to access the value at that address, we use the .pointee
member.
To work with a pointer to a value, instead of the value itself, we can take advantage of the implicit bridging between an inout
value, and a pointer:
func example(ptr: UnsafePointer<Int>) {
print(ptr.pointee) // 42
}
var value: Int = 42
example(ptr: &value)
Unlike in C-based languages, the &
operator here does not mean address of, but creates an inout copy of value
. The function receives that copy, rather than the original.
This means that when the inout
is implicitly bridged to a pointer, that pointer does not actually necessarily have the same address as the original value. Thus the address of the pointer should not be stored outside of the call stack of the function or closure.
Swift provides a convenient withUnsafePointer(to:)
function in the standard library that's almost always contextually better than implementing functions such as the above:
var value: Int = 42
withUnsafePointer(to: &value) { ptr in
print(ptr.pointee) // 42
}
Another convenient bridge provided by Swift is that arrays of type T
can be implicitly bridged into functions that accept values of UnsafePointer<T>
:
func example(ptr: UnsafePointer<Int>) {
print(ptr.pointee) // 4
}
var values: [Int] = [4, 8, 15, 16, 23, 42]
example(ptr: &values)
The pointer points to the first member of the array, and can be advanced or subscripted to obtain the rest.
Note that in all of these examples, even though the pointer isn't mutable, because we are making an inout
copy, we have to declare the input variable as var
and not let
.
If we want to modify the data pointed to by a pointer, we need to use the mutable variant of the type. The mutable pointer type for any type T
is named UnsafeMutablePointer<T>
, again telling us everything we need to know about it.
As with the non-mutable version, we can pass inout
copies of variables of type T
, or arrays of T
, to functions that accept UnsafeMutablePointer<T>
. The copy is still a copy with the possibility of a different address, and the pointer is still not valid outside of the closure or function call.
And we can access the value the pointer points to through the .pointee
member, except this time, we can modify it. When we modify the inout
copy, at first the copy is modified, and then at the end of the call the new value is copied back to the original, modifying that.
Swift also provides a withUnsafeMutablePointer(to:)
function in the standard library:
var value: Int = 42
withUnsafeMutablePointer(to: &value) { ptr in
ptr.pointee = 43
}
print(value) // 43
Pointers of type UnsafeMutablePointer<T>
can be passed to any function that accepts an UnsafePointer<T>
.
Our first encounter when dealing with hardware will be with the UnsafeMutableRawPointer
type; mutable means that we can change the values pointed to, while raw means that Swift doesn't know the type of the values pointed at.
The difference between the two types is that the non-mutable variation is missing the functions to change the values.
One of the most basic things we can do with a pointer is advance it. This returns a new pointer that is further ahead in memory by an amount of strides given. For a raw pointer, the stride is always in single bytes, so this is a great way to advance from the base of a register mapping to a specific register offset given in the datasheet.
let pwmFifoOffset = 0x18
let pwmFifoPointer = pwmRegisters.advanced(by: pwmFifoOffset)
There's all sorts of operations we can do to pointers, such as copying, initializing, loading, and storing equivalently untyped memory; but we rarely need to worry about such things directly.
Instead the best operation we can do is to make a declaration about the type of memory that a pointer is bound to, and then continue to operate on it as an UnsafeMutablePointer<T>
or UnsafePointer<T>
instead.
For example we know from the datasheet that the PWM Fifo Register is a 32-bit value, so we can change that to a typed pointer:
let pwmFifo = pwmFifoPointer.bindMemory(to: Int.self, capacity: 1)
Note one of Swift's little quirks; Int
on its own is a type identifier, and as such is only valid where a type can be specified such as in a variable declaration. To pass the type as a value, we instead use the Int.self
syntax.
Once we've made this binding, we can rebind the memory later if we choose, provided we discard the previously bound pointer if we ever re-bind the memory to another.
Typed pointers are easier to work with than raw pointers because they know the correct memory layout of the underlying bytes, and can load and store directly from Swift's native types through the .pointee
member:
// Read the current status.
let status = pwmStatus.pointee
// Write to the FIFO.
pwmFifo.pointee = 0b11110000
We can advance these pointers as well, except now they advance by the stride of the underlying type. For example advancing an Int-typed pointer by 1 will advance it by 4 bytes.
let pwmData1 = pwmRange1.advanced(by: 1)
Direct iteration can also be performed through the .successor()
member, and we can even subscript them:
gpioPinOutputSet[1] = 0b00001000
Note that they are not iterable since the memory bounds are not known, and we must take care to never exceed the bounds.
We don't want to deal with pointers unless we have to, so for most purposes, our programs will use Swift's ordinary value and reference types. We'll convert them once we're ready to interface with hardware.
The simplest way is simply to always use a pointer for the hardware-side, and use ordinary assignment to copy the value into it:
pwmFifo.pointee = valueForFifo
But sometimes we need a pointer to the actual Swift value.
In Swift there is an equivalency between a pointer, and an inout
function parameter, and the standard library provides a couple of global functions to take advantage of this.
We can obtain a typed pointer, either mutable or not, to the underlying value type:
var value = 0x02011a2b
withUnsafeMutablePointer(to: &value) {
valuePtr in
valuePtr.pointee |= 0x70000000
}
// value now has value 0x72011a2b
Or we can obtain a raw pointer to the underlying bytes:
withUnsafeMutableBytes(of: &value) {
bytes in
bytes[0] |= 0xf0
}
// value now has value 0x72011afb
If that last value surprised you, the section under Integer Types on endianness is worth a read. The above illustrates the difference between the two sets of methods, just as with memory access, one is typed and the other is not.
When we access the bytes of an existing value, we don't receive a direct UnsafeRawPointer, instead we receive an UnsafeRawBufferPointer. The difference between the two is key: a buffer pointer knows how much data there is at the address, and knows that the pointer is to something held in another value.
The first means that we can treat the buffer largely as a collection of bytes, it allows iteration, enumeration, subscripting, etc. In the example above, we use a subscript to change the value of one of the bytes making up the integer.
The second means we can't just directly screw around with reassigning the type, there's no .pointee
as with raw pointers, but there's also no .assumingMemoryBound(to:)
or .advanced(by:)
either.
This is because of terms like strict-aliasing, type-punning, etc. that it's best to not try and understand too deeply. Simply put, an address of memory can only have one type of object at one time. It's not legal to take a pointer to a UInt32, and attempt to treat it as a pointer to a UInt16; it's even illegal in C, and hasn't stopped anyone trying for years there either.
The correct way to do that always involves a copy, and in Swift doesn't even require mucking around with pointers:
let partOfValue = UInt16(truncatingBitPattern: value)
You'll recall in the example code for reading the device-tree ranges file that we used the Data
struct to read from the file, but we used its own withUnsafeBytes
member and typed it, rather than the global function.
A little bit of mental squinting will realise that if we'd just used withUnsafeBytes(of:)
we would have been given the bytes of the structure itself, rather than the contents of the file that it wraps. Since Data
knows we will likely want the raw bytes, it provides that to us through its members.
And since it knows the data might be typed, and provides byte-at-a-time access through its own direct functions and properties, it doesn't just limit us to a byte at a time this way, thus we can obtain a pointer of whatever type we like.
Usually at this point somebody comes along to say that Swift is unsuitable for systems programming because it doesn't have a well-defined memory model; words like Rust are usually bandied about.
In part this is certainly true as a statement; for our purposes, the following code demonstrates one problem:
let a = thing.pointee
let b = thing.pointee
The Swift compiler provides no explicit guarantees about exactly what machine code will result from those two statements. For example whether two memory reads will be assigned to two values, or whether one memory read will be simply copied twice. Even in the case of two reads and two values, the compiler doesn't guarantee which order they might happen in.
Ordering between different reads has the same lack of guarantee:
let a = thing1.pointee
let b = thing2.pointee
Usually this isn't an issue, firstly because while it doesn't "guarantee" anything, the compiler tends to behave reasonably rationally in most cases. And secondly, because when you're dealing entirely with your own code, you don't usually change things under yourself.
Sadly when you're dealing with hardware, things aren't quite that simple. But forget worrying about the language, even the hardware doesn't provide any guarantees! On p7 of the datasheet, it explains that the hardware interface can return those values out of order all by itself, even in the presence of a programming language with a well-defined memory model.
In practice other hardware issues tend to come up first, especially when dealing with multiple peripherals. We frequently need to introduce arbitrary delays into the code after writing a command to one peripheral, before the next, simply for it to perform the operation we asked.
These delays happen to also introduce rationale compiler behavior, since they look like function calls.
I've not had any cause to need this yet, but since Swift can be freely linked to C code, we have a useful solution to such issues if they ever occur.
We can export a MemoryBarrier()
function from C into Swift, wrapping a compiler primitive available to the C compiler, and then use that:
$ cat > mb.h <<EOF
static void MemoryBarrier(void) {
__sync_synchronize()
}
EOF
$ cat > MyCode.h <<EOF
// Won't compile, but demonstrates the point.
let a = thing1.pointee
MemoryBarrier()
let b = thing2.pointee
EOF
$ swiftc -import-objc-header mb.h MyCode.swift
Next: Raspberry Pi Hardware