Silence warnings about safe shared mutable state

I've been trying out the new SWIFT_STRICT_CONCURRENCY flag in Xcode 14 and I've come across the following scenario that I am unsure on how to best resolve. Here's a simplified example:

private var xKey = false

protocol Foo: AnyObject {
    var x: String { get }
}

extension Foo {
    var x: String {
        if let x = objc_getAssociatedObject(self, &xKey) as? String {
            return x
        } else {
            let x = "<more interesting in real life>"
            objc_setAssociatedObject(self, &xKey, x, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            return x
        }
    }
}

At the point of use of &xKey I am getting the following warning:

Reference to var 'xKey' is not concurrency-safe because it involves shared mutable state

What would be the best way to resolve this? Is there any annotation I can use to pinky-swear to the compiler that this is OK?

The only solution I've found so far, which feels heavy, is to define my key like this instead:

private final class Key: @unchecked Sendable {
    var value = false
}

private let xKey = Key()

And then use: &xKey.value.

Just let key = malloc(1)! is fine. No problem of using C call with Objective-C API in a swift app. :grinning:

1 Like

Hello, thank you! This does not resolve the issue however.

Switching to private let xKey = malloc(1) yields this error:

Cannot pass immutable value as inout argument: 'xKey' is a 'let' constant

And switching to var instead of let puts me back at my original position.

Reference to var 'xKey' is not concurrency-safe because it involves shared mutable state

In this case use xKey as is, without &, that's the whole point.

let key = malloc(1)! // this is global / static / or stored in some singleton class for example
...
objc_getAssociatedObject(self, key)

Got it. Yes, that does resolve the issue, thanks for the help!

I do wonder if this could be generalized to a different case that triggers the same warning though:

I am using the "environment/world" pattern described by the pointfree folks.

struct World: @unchecked Sendable {
    var date: () -> Date

    init() {
        date = Date.init
    }
}

var Current = World()

// elsewhere
func foo() {
    print(Current.date()) // Reference to var 'Current' is not concurrency-safe because it involves shared mutable state
}

Current is a var, but it is effectively a let once configured in app-launch so it is safe, but I'm unsure of how to convince the compiler of that. I've been hoping there was some sort of @unchecked-ish annotation to use for vars.

It would help if you provide the fragment that shows the warning / error, otherwise it is not clear what the issue is.

Updated my example, sorry about that!

Will this work?

struct World: @unchecked Sendable {
    var date: () -> Date

    init() {
        date = Date.init
    }
}

class Current {
    static let current = Current()
    var world: World
    private init() {
        // this init will be called on the first Current.current call
        // configure the world appropriately here
        world = World()
    }
}

// elsewhere
func foo() {
    print(Current.current.world.date())
}

I've come up with this which gives me the same interface as before, like being able to just call Current.date(). Still really wish there was some kind of annotation though to just mark a var is unchecked :thinking:.

struct World: @unchecked Sendable {
    var date: () -> Date

    init() {
        date = Date.init
    }
}

@dynamicMemberLookup
final class WorldContainer<W: Sendable>: @unchecked Sendable {
    var world: W

    init(_ world: W) {
        self.world = world
    }

    subscript<V>(dynamicMember keyPath: KeyPath<W, V>) -> V {
        world[keyPath: keyPath]
    }

    subscript<V>(dynamicMember keyPath: WritableKeyPath<W, V>) -> V {
        get {
            world[keyPath: keyPath]
        }
        set {
            world[keyPath: keyPath] = newValue
        }
    }
}

let Current = WorldContainer(World())

func foo() {
    print(Current.date())
}

I see :thinking:

What prevents you from using let for your Current variable? e.g. like so:

let Current = World.makeWorld()
// makeWorld does the relevant configuration logic.

The pattern is used as a DI solution so it needs to remain a var so that you configure it differently while in unit tests. (Explained much better over here.)

I am also interested in figuring out a way around this issue, since I'm using the same World strategy from the fine folks of PointFree.

This is basically the same problem that we had when optionals came: we needed Implicitly Unwrapped Optionals to work around some limitations of Cocoa.Then with try/catch, we also received try!.

Same thing should be applied here

Has anyone found a solution for this? In my case I actually have the situation where I mutate the World during runtime (switching between authenticated and non authenticated api clients) so it's probably even trickier.

In this case it sounds like you'll actually need to serialize access to your World by either sticking inside an actor or annotating it with a global actor.

What I posted here does work but it's not particularly nice. I've since moved on to something adapted from pointfree's new dependency stuff here though

@noremac Could you elaborate? I've just watched the second dependency episode, turned on the concurrency flags for my Package and the warnings go crazy.

I tried adapting your sample to use the MainActor for synchronized access but of course I can't initialize it globally:

struct World: Sendable {
	var date: @Sendable () -> Date

	init() {
		date = { Date.init() }
	}
}

@dynamicMemberLookup
@MainActor
final class WorldContainer<W: Sendable>: Sendable {
	var world: W

	init(_ world: W) {
		self.world = world
	}

	subscript<V>(dynamicMember keyPath: KeyPath<W, V>) -> V {
		world[keyPath: keyPath]
	}

	subscript<V>(dynamicMember keyPath: WritableKeyPath<W, V>) -> V {
		get {
			world[keyPath: keyPath]
		}
		set {
			world[keyPath: keyPath] = newValue
		}
	}
}

let Current = WorldContainer(World()) // Call to main actor-isolated initializer 'init(_:)' in a synchronous nonisolated context

Depending on your needs a quick alternative would be to provide the global via a task local:

struct World {
  @TaskLocal static var current = Self(
    // ...
  )

  // ...
}

This is more limited than the other global-mutable approach, but also much safer.

In tests, you can use the task local to create a scope with certain overrides:

func testWithMockedAPI() async {
  var world = World.current
  world.apiClient = .mock
  try await World.$current.withValue(world) {
    // Anything accessing `World.current` in here
    // will use the mocked API
  }
}

And you can definitely write a few helpers to make this nicer!

Thanks for your input @stephencelis! I tried adapting my code with your new TaskLocal solution but it didn't work for me since I have to switch dependencies at runtime (and for the entire time not just locally scoped).

I now use the following approach syncing access via the MainActor:

@MainActor
struct World: Sendable {
  var apiClient: ApiClient
  var logger: Logger

  static var current = World()

  init() {
    logger = .osLog(debugOnly: true, logLevel: .debug)
    apiClient = .awaitingConfig
  }
}

final class SomeSdk {
  @MainActor
  public func initialize(with config: Configuration) { // can be called multiple times
    if let customLogger = config.logger {
      World.current.logger = customLogger
    }

    World.current.apiClient = .live(endpoint: config.endpoint, token: config.token)
}

This way I can change the dependencies as long as the mutations happen on the MainActor (which was the case anyways).

I found another solution using a property wrapper.

public enum Config {
   @UnsafeGlobal var defaultLogPrefix: String = ""
}

@propertyWrapper
public class UnsafeGlobal<T> : @unchecked Sendable {
   
   public var wrappedValue: T
   
   public init(wrappedValue: T) {
      self.wrappedValue = wrappedValue
   }
   
}

From my understanding this works because the variable in the enum is effectively an instance of UnsafeGlobal and is never modified. What’s modified is inside the UnsafeGlobal instance, which is considered safe because we have marked UnsafeGlobal to be Sendable.

Making a SafeGlobal should be easy using an NSLock. I have not yet tried it though. It would have to be @unchecked Sendable too because the synchronisation would be done manually, but this time it would not be a blatant lie.


UPDATE: It seems to work well with a safe property wrapper too.

public enum Config {
   @SafeGlobal var defaultLogPrefix: String = ""
}

/* We use the same lock for all SafeGlobal instances.
 * Whether you should use one lock per instance instead will depend on your use-case. */
private let safeGlobalLock = NSLock()

@propertyWrapper
public class SafeGlobal<T : Sendable> : @unchecked Sendable {
   
   public var wrappedValue: T {
      get {safeGlobalLock.withLock{ _wrappedValue }}
      set {safeGlobalLock.withLock{ _wrappedValue = newValue }}
   }
   
   public init(wrappedValue: T) {
      self._wrappedValue = wrappedValue
   }
   
   private var _wrappedValue: T
   
}