Wednesday, March 27, 2019

Swift 5 Released

Ted Kremenek:

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 if count > 0

An 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 accept NULL with a zero count.

For the Data API, the count matches the Data’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.

Brent Royal-Gordon

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.

Leave a Comment