ARTICLE AD BOX
There are currently some compiler bugs (example) that produces both false positives and false negatives, which makes this all the more confusing, but ultimately, this is not possible with property wrappers, because setters do not support sending the newValue parameter.
For a mutex to safely work with non-sendable types, it needs to make sure that the value being protected isn't isolated to somewhere else. For example, this is not safe, and does not compile as expected.
// suppose this is declared in 'Atomic' and // 'Value' is a type parameter without constraints func set(_ newValue: Value) { mutex.withLock { $0 = newValue } }Consider this unsafe situation:
actor SomewhereElse { var atomic = Atomic<NonSendable?>(wrappedValue: nil) func doSomeIsolatedStuff() { print(atomic.wrappedValue) } } @MainActor class Foo { var x = NonSendable() func f() async { let somewhereElse = SomewhereElse() await somewhereElse.atomic.set(x) // SomewhereElse can now access x through the mutex // but the MainActor can also access it, without the mutex! } }For set to be safe, the newValue parameter needs to be sending, meaning that the caller promises to never access the parameter again.
func set(_ newValue: sending Value) { mutex.withLock { $0 = newValue } }The newValue parameter of property setters can't be sending, so it is not possible to implement a safe property wrapper that wraps a Mutex that also works with non-sendable values.
The rest of this answer talks about the compiler bugs, which actually makes it possible to create such a property wrapper. But as established above, this will be unsafe from a concurrency perspective.
There is a compiler bug, which makes the above declaration of set with a sending parameter still doesn't compile! Fortunately, a workaround is found - wrapping the parameter in a closure makes the compiler happy,
// this compiles! func set(_ newValue: sending Value) { let block = { newValue } mutex.withLock { $0 = block() } }But actually, it still compiles if I remove sending (bug report)!
// this compiles and is unsafe! func set(_ newValue: Value) { let block = { newValue } mutex.withLock { $0 = block() } }By exploiting this false positive, you can write
@available(iOS 18.0, *) @propertyWrapper public struct Atomic<Value>: Sendable, ~Copyable { private let mutex: Mutex<Value> public init(wrappedValue: @autoclosure () -> sending Value) { self.mutex = Mutex(wrappedValue()) } public var wrappedValue: Value { get { mutex.withLock { $0 } } set { let workAround = { newValue } mutex.withLock { $0 = workAround() } } } }Note that init takes a @autoclosure () -> sending Value, and not a simple sending Value as you'd expect. This is because of another compiler bug.
