It's important to be clear what unsafe code actually is. We throw around these terms "safe" and "unsafe" to the point where they kind of become like slogans, where "safe" is a synonym for "good" and "unsafe" is a synonym for "bad". Why would anybody choose the bad thing?! We shouldn't lose sight of what these terms actually refer to.
Unsafe constructs are not broken by definition; they're not "please break my program" operations. There is a well-defined way to use them, and they are useful when used correctly; that's obviously the goal of whoever created the construct.
But almost all APIs come with some sort of preconditions (in the English language sense, not the Swift precondition
function sense); they don't accept fully arbitrary inputs. For example, Array's subscript operation accepts an integer index -- but it doesn't support subscripting arbitrary integer values; the values you give it must correspond to occupied positions in the array.
The difference between a safe API and an unsafe API is that safe APIs validate those preconditions which are required for memory safety, while unsafe APIs rely on you to use them correctly (or write your own checks) and do not have any built-in precondition validation. If it is at all possible to use the unsafe construct (and we must assume it is), it is also possible to create a safe version which validates its preconditions.
Here's a concrete example - we often get people looking to load POD types from data buffers (e.g. load a UInt32
from position x
). Currently that is only offered as an unsafe API -- but it is totally possible to use that primitive to write a safe version, by validating those preconditions described in the primitive's documentation which pertain to memory safety:
This function only supports loading trivial types. A trivial type does not contain any reference-counted property within its in-memory stored representation.
The memory to read for the new instance must not extend beyond the buffer pointer’s memory region—that is,
offset + MemoryLayout<T>.size
must be less than or equal to the buffer pointer’scount
.
(AFAIK the other requirement, "The memory at offset
bytes into the buffer must be laid out identically to the in-memory representation of T
.", isn't required for memory safety specifically if we know that T
is a trivial type, but obviously it's worth abiding by otherwise you'll read a bunch of safe, junk values).
extension Array where Element == UInt8 {
func loadUnaligned<T>(from offset: Index = 0, as: T.Type) -> T {
withUnsafeBufferPointer { buffer in
precondition(offset + MemoryLayout<T>.size <= buffer.count)
precondition(_isPOD(T.self))
return UnsafeRawBufferPointer(buffer).loadUnaligned(fromByteOffset: offset, as: T.self)
}
}
}
Yes, we used unsafe constructs (again, constructs which do not validate their preconditions), but those constructs have knowable, sensible preconditions which we can validate ourselves to create a memory-safe construct.