Without any doubts Swift is one of the best safe programming language among many programming languages. The safety comes from powerful constructs like strong typing, type checking at compile time, optionals and few others.
A small amount of Swift Standard Library provides a collection of unsafe API’s. Before going into unsafe world let’s first understand what does a safe code means.
Safe Operations
A piece of code is safe when its behaviour is fully defined covering all cases of inputs. Let’s use the force unwrap of optionals as a trivial example to define what a safe code is
let first: Int? = 43
print(first!) // Prints 43
let second: Int? = nil
print(second!) // Fatal error: Unexpectedly found nil while unwrapping an Optional value
In the first case the optional has a value and it executes correctly. In second case optional is nil and we get a runtime fatal error and code execution stops. This is a defined behaviour of optionals. This covers all the cases of input. In graphical form
Indeed force-unwrapping of optional is a severe programming error, but if we use it we have a well defined behaviour(i.e that our programme will crash).
Unsafe Operations
An unsafe operation is such that it will have undefined behaviour for at least one or more inputs that does not meet expectation of its documented input.
Optional also provides an unsafe force unwrapping operation. Similar to safe force-unwrap operation, unsafe operation required that underlying value to be non-nil. But if code is compiled using optimizations enabled, the unsafe operation does not verify the non-nil requirement, it proceeds with assumption that it contains a non-nil value. Let’s use optional from our last example to see an unsafe behaviour of force unwrap of optionals
let first: Int? = 43
print(first.unsafelyUnwrapped) // Prints 43
let second: Int? = nil
print(second.unsafelyUnwrapped) // Undefined behaviour
In above example for first case the code execute nicely. While for second case we cannot reason what will happen when we are using an unsafe operation. There are many possibilities
It is important to note that there are many possibilities if we break the contract of an unsafe operation. In above example of unsafe force unwrap of optionals; if there is no value, it might give some garbage value back or we get a crash or something else. Also the behaviour may not be same when same code is run again.
The unsafe API does not verifies its requirements and assume that developer has full responsibility of its input i.e to have non-nil value for above example. However we got a helping hand from build tools here. In optimized build the unsafe force unwrap does not performs any non-nil value validation while it does performs validation in unoptimized debug builds.
In Swift standard library the unsafe API’s are prefixed with unsafe naming convention. This prefix will act as precaution to anyone reading or writing the code.
Another example: Unsafe Pointers
Let see another example of unsafe pointers.
let pointer = UnsafeMutablePointer<Int>.allocate(capacity: 1) // allocate the memory pointed by the pointer
pointer.initialize(to: 43) // Initialize the memory with some value
print(pointer.pointee) // Use the value
pointer.deallocate() // Deallocate the memory pointed by pointer. After this point the pointer becomes a dangling pointer
pointer.pointee // Any attempt to access the memory after deallocation then it will result in an undefined behaviour
It is important to note that pointer does not know anything about when it is allocated or deallocated. This is the responsibility of programmer to manage it. If we attempt to access the memory after deallocation then it will result in an undefined behaviour. Xcode provides us with a runtime debugging tool called Address Senitizer to help identify and fix this type of memory issues.
Benefits and usage of Unsafe API’s
The unsafe API’s can used to build code that is still reliable. There are mainly two main usage categories of unsafe API’s.
- Interoperability with C and Objective-C
- Fine grain control of performance at run time.
Unsafe Swift API’s are used for interoperability with other unsafe programming languages such as C or Objective-C. Lets consider a below C function that processes an int
buffer in an array
void foo(const int* start, size_t count)
When this function is imported to Swift it will be in form
func foo(_ start: UnsafePointer<Int32>!, _ count: Int)
Note that the first const int pointer parameter is translated as unsafe implicitly unwrapped optional pointer type.
One of the way to call this C function from within Swift is as below
//1
let array = UnsafeMutablePointer<Int32>.allocate(capacity: 5)
//2
array.initialize(to: 23)
(array + 1).initialize(to: 54)
(array + 2).initialize(to: -43)
(array + 3).initialize(to: 0)
(array + 4).initialize(to: 13)
//3
foo(array, 5)
//4
array.deinitialize(count: 5)
//5
array.deallocate()
Below is discussion about each step and what is technically unsafe for each step.
- Use static allocate method of UnsafeMutablePointer to create a dynamic buffer to hold the integer values. Unsafe: Note that the lifetime of allocated buffer is not managed by the return pointer. We have to manage it ourself and manually deallocate it at some time otherwise it will stay in memory forever causing memory leak.
- Use pointer arthimetic to initialize array with some valid values. Unsafe: Initialization cannot automatically verify that calculated address is within memory range of buffer allocated. If we go out of range we will get an undefined behaviour.
- Unsafe: We are assuming that function is not going to take ownership of allocated buffer. This is by seeing the documentation of function or by looking into the implementation of function. The function will only access the buffer during its execution and does not hold on to memory or try to deallocate it .
- Unsafe: Deinitialization will only work if it was previously properly initialized with correct data type.
- Unsafe: We only deallocate a memory if it was previously allocated and is in deinitialized state.
At each step there are some unchecked assumptions and if we get any one of them wrong we will get an undefined behaviour.
There are two more issues with the above code
- The buffer is managed by its start address.
- Its length or size is in a separate value and that is scattered all over the code. In above case at least in three separate places.
The quality of code is greatly increased if we can somehow combine start address and size together. To address this Swift standard library provides below four pointer types.
- UnsafeBufferPointer<Element>
- UnsafeMutableBufferPointer<Element>
- UnsafeRawBufferPointer
- UnsafeMutableRawBufferPointer
These four buffer pointers are very helpful whenever we need to access the region of memory instead of pointer to individual elements. By combining region and size together these pointers will add some level of safety but developers are still in charge of managing the other aspects of safety. For unoptimized debug build these pointers check for out of bound access for subscript operations; thus having some safety.
Easy access to unsafe API’s in Swift Standard Collection
Some of the safety issues discussed in above example can be simplified by API’s provided by Swift standard collection library. These collection API’s provide an easy way to access their underlaying storage through simple unsafe methods.
Sequence.withContiguousStorageIfAvailable(_:)
MutableCollection.withContiguousMutableStorageIfAvailable(_:)
String.withCString(_:)
String.withUTF8(_:)
Array.withUnsafeBufferPointer(_:)
Array.withUnsafeMutableBufferPointer(_:)
Array.withUnsafeBytes(_:)
Array.withUnsafeMutableBytes(_:)
We can use above and few other methods to simplify our code and limit ourself to smallest number of unsafe code.
Taking our previous example and using the Swift Standard collection unsafe API’s, below will be new code
let array: [Int32] = [23, 54, -43, 0, 13]
array.withUnsafeBufferPointer { buffer in
foo(buffer.baseAddress!, buffer.count) // The C function call
}
Notice how nicely we have avoided some of the unsafe operations and used only minimum needs unsafe calls. The unsafe operations that we have avoided are
- Unsafe static allocation
- Unsafe pointer arithematic to initialize
- Unsafe deinitialization
- Unsafe deallocation
We have only make an unsafe call to C function foo
.
There is such a greater need to pass pointers to C functions such that Swift has special syntax to for it. We can simply pass array expecting an unsafe pointer and compiler will automatically generate the code equivalent for withUnsafeBufferPointer
.
let array: [Int32] = [23, 54, -43, 0, 13]
foo(array, array.count) // The compiler will generate the code equivalent of withUnsafeBufferPointer
An important point to remember is that the C function call is still unsafe.
Thank you for your time. If you liked the article please consider sharing it.