Upgrading exclusive access warning to be an error in Swift 4.2


(Devin Coughlin) #1

In the recent Swift 4.2 branch, the existing Swift 4.1 warning about ‘overlapping accesses’ is now an error in Swift 4 mode. This means that projects with this warning will fail to build with the Swift 4.2 compiler.

The warning typically arises when a mutating method that modifies a variable is passed a non-escaping closure that reads from the same variable. For example:

var buffer = Data(count: Int(INET6_ADDRSTRLEN))

// Warning: overlapping accesses to 'buffer', but modification
// requires exclusive access; consider copying to a local variable
let ptr = buffer.withUnsafeMutableBytes {
	// Note: conflicting access is here
    inet_ntop(AF_INET, &sin.sin_addr, $0, socklen_t(buffer.count))
}

Here withUnsafeMutableBytes() is a mutating method on buffer and so conceptually modifies the entire variable for the duration of the method. The compiler warns on the call to count, which is a computed property that reads from buffer while the modification is in progress — a violation of Swift’s rules for exclusive access.

The best way to address this warning is by reading the buffer count outside of the closure and copying it to a captured local variable:

var buffer = Data(count: Int(INET6_ADDRSTRLEN))
let count = buffer.count

let ptr = buffer.withUnsafeMutableBytes {
    inet_ntop(AF_INET, &sin.sin_addr, $0, socklen_t(count))
}

Sometimes copying the value outside of the closure doesn’t make sense because you want to observe the mutating effects of the called method inside the closure. For example, the following struct provides a helpful method to push a path component onto a path, execute a closure, and then pop the component.

struct MyFilePath {
     var components: [String] = []

     mutating
     func withAppended(_ path: String, _ closure: () -> Void) {
         components.append(path)
         closure()
         components.removeLast()
    }
}

However, calling this method on a path results in an exclusivity violation when the closure also refers to the path via a capture:

var p = MyFilePath(components: ["usr", "local"])
p.withAppended("bin") {
    // Warning: overlapping accesses to ‘p'  
    print("The path is \(p)")
}

To address this exclusivity violation, you can change the helper method to explicitly provide the mutated path as an inout parameter to the closure:

mutating
func withAppended(_ path: String, _ closure: (inout MyFilePath) -> Void) {
    components.append(path)
    closure(&self)
    components.removeLast()
}

Then callers of the method can safely refer to the mutated path via the closure parameter:

var p = MyFilePath(components: ["usr", "local"])
p.withAppended("bin") {
    print("The path is \($0)")
}

If you want to try out the newly-upgraded error on your own code, you can download a Swift 4.2 snapshot (Xcode, Linux) from swift.org. If you are willing, please post back to this thread with your experiences.


Enabling run-time exclusivity checking in release mode
(BJ Homer) #2

I just tried out Swift 4.2 in the Xcode 10 branch, and ran into an error I didn't expect. This code triggers an exclusive access violation:

protocol Nameable {
    var name: String? { get set }
    var preferredName: String? { get set }
}

struct Foo: Nameable {
    var preferredName: String? = "Fred"
    var name: String? = "Frederick"
}

var x: Nameable = Foo()
x.name = x.preferredName ?? x.name // Error: Overlapping accesses to 'x'

However, both of these appear to be just fine:

struct Foo {
    var preferredName: String? = "Fred"
    var name: String? = "Frederick"
}

var x = Foo()
x.name = x.preferredName ?? x.name
class Foo {
    var preferredName: String? = "Fred"
    var name: String? = "Frederick"
}

let x = Foo()
x.name = x.preferredName ?? x.name

Is it expected that using a protocol instead of a concrete type here would trigger an exclusive access violation?


(BJ Homer) #3

In fact, it's even simpler. No ?? needed.

protocol Nameable {
    var name: String? { get set }
    var preferredName: String? { get }
}

struct Foo: Nameable {
    var preferredName: String? = "Fred"
    var name: String? = nil
}

var x: Nameable = Foo()
x.name = x.preferredName

(Xiaodi Wu) #4

I think that's correct, as Nameable.name and Nameable.preferredName could be computed properties. Therefore, each side of the existentially typed assignment must be treated as accessing all of x instead of just the specific properties.


(BJ Homer) #5

But is there any overlapping access? The preferredName read should be done before the name write begins.


(Michael Bates) #6

That does seem surprising to me. Surely it doesn't give an overlapping access error if you store the right hand side in a variable? I would expect assignment to at least be conceptually equivalent to this…

// ...
let pn = x.preferredName
x.name = pn

(Xiaodi Wu) #7

If you want the read to be done first, you’ll need to assign to a temporary variable. Otherwise, it’s an overlapping access, as I understand it.


(Joe Groff) #8

This is a bug. It's fixed in the master and 4.2 branches but didn't make it into the first Xcode 10 beta.


(Joe Groff) #9

The exclusive access does not begin until after all arguments are evaluated. Lvalues are effectively evaluated in two phases—first, during normal argument/operand evaluation, the abstract lvalue is evaluated, which produces an access path to get to the storage and evaluates subscript indexes but does invoke any getters or setters yet. After all arguments are evaluated normally, but before the mutating operation occurs, the access is begun. This allows things like x.foo = x.bar or zim(&x.zang, x.zung) to work without exclusivity violations, since the rvalue terms get evaluated before the formal access begins.


(Xiaodi Wu) #10

Ah, then my understanding about overlapping access is incorrect! What’s my error?


(Xiaodi Wu) #11

Got it. Thanks!


(BJ Homer) #12

It appears that this is still not fixed in the Xcode 10.0 b2 release. Is that expected?


(Joe Groff) #13

Yes, Xcode 10 beta 2 did not pick up any Swift changes compared to beta 1. It should be fixed in the 4.2 snapshots on swift.org.