I've been experimenting with noncopyable types and currently have a use case where I want an AsyncIterator (which is nonsendable) inside of a noncopyable type that isSendable.
I think this is fine because the iterator is created once and is only ever iterated inside of the noncopyable type, meaning there is never a copy of it in different isolation domains even though it can move from one isolation domain to another.
I'm currently using @unchecked Sendable to achieve this, and I'm wondering:
Am I correct that this is actually safe?
Is there a way to express this in the current type system without @unchecked Sendable?
Are there any future additions to the type system that may be able to express this? Some way of marking a type as "sendable if owned exclusively by a noncopyable type"?
class Nonsendable {
var state = 3
}
struct SendableButNotCopyable: ~Copyable, @unchecked Sendable {
var nonsendable: Nonsendable
}
let reference = Nonsendable()
let a = SendableButNotCopyable(nonsendable: reference)
let b = SendableButNotCopyable(nonsendable: reference)
await withDiscardingTaskGroup { group in
group.addTask {
for _ in 0..<1_000_000 {
a.nonsendable.state += 1
}
}
group.addTask {
for _ in 0..<1_000_000 {
b.nonsendable.state += 1
}
}
}
print(reference.state)
So no, not safe in general. (This would crash if I'd used an Array or String instead of an Int).
You need to ensure that your nonsendable type is sending into your Sendable one, for this to be safe. (and you'll still need @unchecked Sendable on your type that maintains the invariant that that type remains disconnected).
Thanks! Yeah in my case I am effectively sending the nonsendable type into the noncopyable-but-sendable one.
If we had sending checking that would at least prevent your example from compiling (I think this may be possible now?) but I don’t think you can currently prevent a copyable property of a noncopyable type from being copied out in one isolation domain, and then subsequently copied out again in another (I achieve this because the property is private, but I don’t think there is a way to have the compiler enforce this now.
Maybe we can come up with a way to specify “this value of a nonsendable type is now sendable but noncopyable”, but I’m not sure this is materially better than my @unchecked Sendable wrapper struct, and it also doesn’t prevent from values inside of the nonsendable struct from being extracted in different isolation domains.
public struct Disconnected<Value: ~Copyable>: ~Copyable, @unchecked Sendable {
private nonisolated(unsafe) var value: Value
public init(_ value: consuming sending Value) {
self.value = value
}
public consuming func consume() -> sending Value {
value
}
public mutating func swap(_ other: consuming sending Value) -> sending Value {
let result = self.value
self = Disconnected(other)
return result
}
public mutating func take() -> sending Value where Value: ExpressibleByNilLiteral {
let result = self.value
self = Disconnected(nil)
return result
}
public mutating func withValue<R: ~Copyable, E: Error>(
_ work: (inout sending Value) throws(E) -> sending R
) throws(E) -> sending R {
try work(&self.value)
}
}