Unsafe Swift: Using Pointers and Interacting With C
In this tutorial, you’ll learn how to use unsafe Swift to directly access memory through a variety of pointer types. By Brody Eller.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Unsafe Swift: Using Pointers and Interacting With C
30 mins
- Getting Started
- Examining Struct Layouts
- Using Pointers in Unsafe Swift
- Using Raw Pointers
- Using Typed Pointers
- Converting Raw Pointers to Typed Pointers
- Getting the Bytes of an Instance
- Computing a Checksum
- Three Rules of the Unsafe Club
- Don’t Return the Pointer From withUnsafeBytes!
- Only Bind to One Type at a Time!
- Don’t Walk Off the End… Whoops!
- Unsafe Swift Example 1: Compression
- Unsafe Swift Example 2: Random Generator
- Where to Go From Here?
Don’t Return the Pointer From withUnsafeBytes!
// Rule #1
do {
print("1. Don't return the pointer from withUnsafeBytes!")
var sampleStruct = SampleStruct(number: 25, flag: true)
let bytes = withUnsafeBytes(of: &sampleStruct) { bytes in
return bytes // strange bugs here we come ☠️☠️☠️
}
print("Horse is out of the barn!", bytes) // undefined!!!
}
You should never let the pointer escape the withUnsafeBytes(of:) closure. Even if your code works today, it may cause strange bugs in the future.
Only Bind to One Type at a Time!
// Rule #2
do {
print("2. Only bind to one type at a time!")
let count = 3
let stride = MemoryLayout<Int16>.stride
let alignment = MemoryLayout<Int16>.alignment
let byteCount = count * stride
let pointer = UnsafeMutableRawPointer.allocate(
byteCount: byteCount,
alignment: alignment)
let typedPointer1 = pointer.bindMemory(to: UInt16.self, capacity: count)
// Breakin' the Law... Breakin' the Law (Undefined behavior)
let typedPointer2 = pointer.bindMemory(to: Bool.self, capacity: count * 2)
// If you must, do it this way:
typedPointer1.withMemoryRebound(to: Bool.self, capacity: count * 2) {
(boolPointer: UnsafeMutablePointer<Bool>) in
print(boolPointer.pointee) // See Rule #1, don't return the pointer
}
}
Never bind memory to two unrelated types at once. This is called Type Punning and Swift does not like puns. :]
Instead, temporarily rebind memory with a method like withMemoryRebound(to:capacity:).
Also, it is illegal to rebind from a trivial type, such as an Int, to a non-trivial type, such as a class. Don’t do it.
Don’t Walk Off the End… Whoops!
// Rule #3... wait
do {
print("3. Don't walk off the end... whoops!")
let count = 3
let stride = MemoryLayout<Int16>.stride
let alignment = MemoryLayout<Int16>.alignment
let byteCount = count * stride
let pointer = UnsafeMutableRawPointer.allocate(
byteCount: byteCount,
alignment: alignment)
let bufferPointer = UnsafeRawBufferPointer(start: pointer, count: byteCount + 1)
// OMG +1????
for byte in bufferPointer {
print(byte) // pawing through memory like an animal
}
}
The ever-present problem of off-by-one errors becomes even worse with unsafe code. Be careful, review and test!
Unsafe Swift Example 1: Compression
Time to take all your knowledge and use it to wrap a C API. Cocoa includes a C module that implements some common data compression algorithms. These include:
- LZ4 for when speed is critical.
- LZ4A for when you need the highest compression ratio and don’t care about speed.
- ZLIB, which balances space and speed.
- The new, open-source LZFSE, which does an even better job balancing space and speed.
Now, open the Compression playground in the begin project.
First, you’ll define a pure Swift API using Data by replacing the contents of your playground with the following code:
import Foundation
import Compression
enum CompressionAlgorithm {
case lz4 // speed is critical
case lz4a // space is critical
case zlib // reasonable speed and space
case lzfse // better speed and space
}
enum CompressionOperation {
case compression, decompression
}
/// return compressed or uncompressed data depending on the operation
func perform(
_ operation: CompressionOperation,
on input: Data,
using algorithm: CompressionAlgorithm,
workingBufferSize: Int = 2000)
-> Data? {
return nil
}
The function that does the compression and decompression is perform, which is currently stubbed out to return nil. You’ll add some unsafe code to it shortly.
Next, add the following code to the end of the playground:
/// Compressed keeps the compressed data and the algorithm
/// together as one unit, so you never forget how the data was
/// compressed.
struct Compressed {
let data: Data
let algorithm: CompressionAlgorithm
init(data: Data, algorithm: CompressionAlgorithm) {
self.data = data
self.algorithm = algorithm
}
/// Compresses the input with the specified algorithm. Returns nil if it fails.
static func compress(
input: Data,with algorithm: CompressionAlgorithm)
-> Compressed? {
guard let data = perform(.compression, on: input, using: algorithm) else {
return nil
}
return Compressed(data: data, algorithm: algorithm)
}
/// Uncompressed data. Returns nil if the data cannot be decompressed.
func decompressed() -> Data? {
return perform(.decompression, on: data, using: algorithm)
}
}
The Compressed structure stores both the compressed data and the algorithm used to create it. That makes it less error-prone when deciding what decompression algorithm to use.
Next, add the following code to the end of the playground:
/// For discoverability, adds a compressed method to Data
extension Data {
/// Returns compressed data or nil if compression fails.
func compressed(with algorithm: CompressionAlgorithm) -> Compressed? {
return Compressed.compress(input: self, with: algorithm)
}
}
// Example usage:
let input = Data(Array(repeating: UInt8(123), count: 10000))
let compressed = input.compressed(with: .lzfse)
compressed?.data.count // in most cases much less than original input count
let restoredInput = compressed?.decompressed()
input == restoredInput // true
The main entry point is an extension on the Data type. You’ve added a method called compressed(with:) which returns an optional Compressed struct. This method simply calls the static method compress(input:with:) on Compressed.
There’s an example at the end, but it’s currently not working. Time to fix that!
Scroll up to the first block of code you entered and begin the implementation of perform(_:on:using:workingBufferSize:) inserting the following before return nil:
// set the algorithm
let streamAlgorithm: compression_algorithm
switch algorithm {
case .lz4: streamAlgorithm = COMPRESSION_LZ4
case .lz4a: streamAlgorithm = COMPRESSION_LZMA
case .zlib: streamAlgorithm = COMPRESSION_ZLIB
case .lzfse: streamAlgorithm = COMPRESSION_LZFSE
}
// set the stream operation and flags
let streamOperation: compression_stream_operation
let flags: Int32
switch operation {
case .compression:
streamOperation = COMPRESSION_STREAM_ENCODE
flags = Int32(COMPRESSION_STREAM_FINALIZE.rawValue)
case .decompression:
streamOperation = COMPRESSION_STREAM_DECODE
flags = 0
}
This converts your Swift types to the C types required for the compression algorithm.
Next, replace return nil with:
// 1: create a stream
var streamPointer = UnsafeMutablePointer<compression_stream>.allocate(capacity: 1)
defer {
streamPointer.deallocate()
}
// 2: initialize the stream
var stream = streamPointer.pointee
var status = compression_stream_init(&stream, streamOperation, streamAlgorithm)
guard status != COMPRESSION_STATUS_ERROR else {
return nil
}
defer {
compression_stream_destroy(&stream)
}
// 3: set up a destination buffer
let dstSize = workingBufferSize
let dstPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: dstSize)
defer {
dstPointer.deallocate()
}
return nil // To be continued
Here’s what’s happening:
The compiler is doing something special here: It’s using the in-out & marker to take your compression_stream and turn it into an UnsafeMutablePointer<compression_stream>. Alternatively, you could have passed streamPointer. Then you wouldn’t need this special conversion.
- Allocate a
compression_streamand schedule it for deallocation with thedeferblock. - Then, using the
pointeeproperty, you get the stream and pass it to thecompression_stream_initfunction.The compiler is doing something special here: It’s using the in-out
&marker to take yourcompression_streamand turn it into anUnsafeMutablePointer<compression_stream>. Alternatively, you could have passedstreamPointer. Then you wouldn’t need this special conversion. - Finally, you create a destination buffer to act as your working buffer.
Next, finish perform by replacing the final return nil with:
// process the input
return input.withUnsafeBytes { srcRawBufferPointer in
// 1
var output = Data()
// 2
let srcBufferPointer = srcRawBufferPointer.bindMemory(to: UInt8.self)
guard let srcPointer = srcBufferPointer.baseAddress else {
return nil
}
stream.src_ptr = srcPointer
stream.src_size = input.count
stream.dst_ptr = dstPointer
stream.dst_size = dstSize
// 3
while status == COMPRESSION_STATUS_OK {
// process the stream
status = compression_stream_process(&stream, flags)
// collect bytes from the stream and reset
switch status {
case COMPRESSION_STATUS_OK:
// 4
output.append(dstPointer, count: dstSize)
stream.dst_ptr = dstPointer
stream.dst_size = dstSize
case COMPRESSION_STATUS_ERROR:
return nil
case COMPRESSION_STATUS_END:
// 5
output.append(dstPointer, count: stream.dst_ptr - dstPointer)
default:
fatalError()
}
}
return output
}
This is where the work really happens. And here’s what it’s doing:
- Create a
Dataobject which will contain the output — the compressed or decompressed data, depending on what operation this is. - Set up the source and destination buffers with the pointers you allocated and their sizes.
- Here, you keep calling
compression_stream_processas long as it returnsCOMPRESSION_STATUS_OK. - You then copy the destination buffer into
outputthat’s eventually returned from this function. - When the last packet comes in, marked with
COMPRESSION_STATUS_END, you potentially only need to copy part of the destination buffer.
In this example, you can see that the 10,000-element array gets compressed down to 153 bytes. Not too shabby.