Are `sending` and `Sendable` same or related

The following class from AVCam sample code generates error in Swift 6 Task-isolated 'newDevice' is passed as a 'sending' parameter; Uses in callee may race with later task-isolated uses.

The error happens in the line continuation?.yield(newDevice). I suspect it has to do with AVCaptureDevice not being Sendable. But the error talks about keyword sending which I have come across multiple times but never saw its definition.

@preconcurrency import AVFoundation

/// An object that provides an asynchronous stream capture devices that represent the system-preferred camera.
class SystemPreferredCameraObserver: NSObject {
    
    private let systemPreferredKeyPath = "systemPreferredCamera"
    
    let changes: AsyncStream<AVCaptureDevice?>
    private var continuation: AsyncStream<AVCaptureDevice?>.Continuation?

    override init() {
        let (changes, continuation) = AsyncStream.makeStream(of: AVCaptureDevice?.self)
        self.changes = changes
        self.continuation = continuation
        
        super.init()
        
        /// Key-value observe the `systemPreferredCamera` class property on `AVCaptureDevice`.
        AVCaptureDevice.self.addObserver(self, forKeyPath: systemPreferredKeyPath, options: [.new], context: nil)
    }

    deinit {
        continuation?.finish()
    }
    
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
        switch keyPath {
        case systemPreferredKeyPath:
            // Update the observer's system-preferred camera value.
            let newDevice = change?[.newKey] as? AVCaptureDevice
            continuation?.yield(newDevice)
        default:
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        }
    }
}

The error goes away if I add this line in the code:

extension AVCaptureDevice: @unchecked @retroactive Sendable {
    
}

I just want to understand what's exactly going on here and if my fix is alright.

1 Like

Continuation's yield method accepts a sending parameter: yield(with:) | Apple Developer Documentation

A sending parameter can either be @Sendable without any additional requirements, or a non-@Sendable that is guaranteed to be released from the call site at compile time.

Example of non-@Sendable value used incorrectly and correctly with the sending keyword:

@MainActor
struct S {
  let ns: NonSendable

  func getNonSendableInvalid() -> sending NonSendable {
    // error: sending 'self.ns' may cause a data race
    // note: main actor-isolated 'self.ns' is returned as a 'sending' result.
    //       Caller uses could race against main actor-isolated uses.
    return ns
  }

  func getNonSendable() -> sending NonSendable {
    return NonSendable() // okay
  }
}

source: swift-evolution/proposals/0430-transferring-parameters-and-results.md at main · swiftlang/swift-evolution · GitHub

3 Likes

You have to be extremely careful with @unchecked @retroactive Sendable. This declares the type you do not control as Sendable everywhere that the extension is visible.

I'm not familiar with this type, but if it turns out that it is not actually Sendable, this can result in lots of subsequent problems with the design.

You should only use this approach if the type is documented to be completely thread-safe, and just missing a Sendable conformance.

I think the biggest question, for me, when addressing this would be to understand what the isolation should be for SystemPreferredCameraObserver. It is a non-isolated class that is using concurrency features, and that's generally something that will get you into trouble.

1 Like

So if I understand it correctly, a sending parameter is either a Sendable or if it is a non-Sendable, then it is not allowed (or assumed that it is not allowed) to be modified by both the threads, i.e. one thread creates it, the other consumes it, and the first thread never touches it again once it has done the send operation.

Yes.

2 Likes

Does the Swift compiler actually checks conformance to sending at compile time?

Yes, using region-based isolation. The proposal has more information about how it works: swift-evolution/proposals/0414-region-based-isolation.md at main · swiftlang/swift-evolution · GitHub