Nonsendable type inside of a sendable, noncopyable struct

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 is Sendable.

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:

  1. Am I correct that this is actually safe?
  2. Is there a way to express this in the current type system without @unchecked Sendable?
  3. 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)
swiftc -swift-version 6 SendableNoncopying.swift && ./SendableNonCopying        
1286164

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.

I've created this type in my own project:

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)
    }
}

I think it's safe...

3 Likes

Interesting! I had not considered using consuming functions to sending the nonsendable values in and out. I like that idea.