Swift 5 Released
Swift 5 is a major milestone in the evolution of the language. Thanks to ABI stability, the Swift runtime is now included in current and future versions of Apple’s platform operating systems: macOS, iOS, tvOS and watchOS. Swift 5 also introduces new capabilities that are building blocks for future versions, including a reimplementation of String, enforcement of exclusive access to memory during runtime, new data types, and support for dynamically callable types.
The main issue I’ve run into (which doesn’t seem to be part of any of the linked evolution proposals) is that the closure for Data.withUnsafeBytes()
now gives you an UnsafeRawBufferPointer
instead of an UnsafePointer<UInt8>
, which is typically what I need to pass to other APIs. It was not obvious how to fix this because the initializers for UnsafePointer
didn’t seem to apply, nor did the withUnsafePointer()
free function. What I came up with was:
let unsafeBufferPointer = unsafeRawBufferPointer.bindMemory(to: UInt8.self) let unsafePointer = unsafeBufferPointer.baseAddress!
I still don’t understand why baseAddress
is defined as an optional or when it’s safe to force unwrap it.
Update (2019-03-28): Joe Groff:
baseAddress
is optional so that it can hold a{NULL, 0}
state for empty buffers. It is always nonnull ifcount
> 0An empty buffer is still a valid buffer; there are a lot of tradeoffs, but making
baseAddress
nullable seemed like the least bad, since many C APIs that ultimately consume these buffers also acceptNULL
with a zero count.For the
Data
API, the count matches theData
’s count.
My takeway from this is that the Data
API cannot be relied on to get you a UnsafePointer<UInt8>
to pass to C because baseAddress
could be nil
if the Data
is empty. This was never the case in my testing, but the API allows it, so force unwrapping is not a good idea.
I ended up writing an extension to provide a reliable UnsafePointer<UInt8>
:
func mjtWithUnsafePointer<ResultType>(_ body: (UnsafePointer<UInt8>) throws -> ResultType) rethrows -> ResultType { return try withUnsafeBytes { (rawBufferPointer: UnsafeRawBufferPointer) -> ResultType in let unsafeBufferPointer = rawBufferPointer.bindMemory(to: UInt8.self) guard let unsafePointer = unsafeBufferPointer.baseAddress else { var int: UInt8 = 0 return try body(&int) } return try body(unsafePointer) } }
even though the callee likely won’t actually access it given that the size is zero.
Update (2019-03-29): I should also note that the automatic Swift 5 conversion failed and then beachballed for all my projects. It was easy enough to update them manually, though.
5 Comments RSS · Twitter
Force-unwrapping isn't "unsafe", per se. Think of it as a declaration that you want things to explode(!) at that point when the precondition (that the optional != nil) is not met.
As for why the baseAddress must be optional, I am not 100% sure of the reasoning behind that in cases like this. Specifically, cases where the lifetime (and validity) of a pointer could reasonably be assumed for the lifetime of withUnsafeBytes(), or Array's withUnsafeBufferPointer().
@Chris What I mean is, it seems like in a case like Data.withUnsafeBytes()
it should be able to guarantee that the baseAddress
will not be nil. But as far as I can tell this isn’t documented. I don’t want it to explode there, but neither do I want to handle a potential nil at every call site.
The documentation for `baseAddress` says:
> If the `baseAddress` of this buffer is `nil`, the count is zero. However, a buffer can have a count of zero even with a non-`nil` base address.
In other words, if `count` is non-zero, `baseAddress` will never be `nil`. If `count` is zero, `baseAddress` may be `nil`, but isn't always. (This saves you from having to allocate storage for empty buffers just to have a valid pointer to put in `baseAddress`.)
If you know the buffer will never be empty, you can safely use `!`. If it may sometimes be empty, you should check for emptiness and do something appropriate.
@Michael Yeah, I think this is a shortcoming of the Swift standard library right now—the buffer types all have an optional baseAddress, and it leaves us doing mental gymnastics trying to figure out when they could possibly be nil in cases like this.
FWIW, this fact even seemed to trip up the optimizer, as I discovered earlier this year: https://bugs.swift.org/browse/SR-9809. Reviewing the PR that fixed the bug (https://github.com/apple/swift/pull/22338), I found an interesting comment:
"(For the record, they're optional so that they round-trip with C pointer/length pairs, which usually accept NULL/0. But as Andy points out, that's not interesting once we've already checked that we have an in-bound index.)"
So it's all C's fault, as usual. :)
@Brent The documentation for baseAddress
is clear; the issue is with Data
. It doesn’t actually say that the length of the buffer will be equal to the length of the data. But even if it is, I guess we have to assume that, at least for an empty data, the buffer could be empty. So in the general case the Data
API does not give you a buffer that can make a valid UnsafePointer
.
Oddly, the documentation for NSData.bytes
says that if the data is empty you get nil, but it’s typed as a non-optional UnsafeRawPointer
.