swiftasync-awaitconcurrencyisolationswift6

Calling async function from nonisolated code


I have a non-sendable class Foo which has to conform to NSFilePresenter. The NSFilePresenter protocol forces 'nonisolated' @objc functions. I need to call an async load() function and I don't get how to write it in Swift 6.

// @MainActor is not an option here !
class Foo : NSObject {
  private var operationQueue: OperationQueue?
  // some other mutable properties, no way to make Foo conform to Sendable
  
  // this must stay async because I have to wait for a file coordinator, code not shown for brevity

  public func load() async throws -> sending String {
    let loadurl = URL(string: "bla")!

    
    var errorMain: NSError?

    let text = try await withCheckedThrowingContinuation { continuation in
      fileCoordinator.coordinate(readingItemAt: loadurl, options:.withoutChanges, error: &errorMain) { (readurl) in
        do {
          let text = try String(contentsOf: readurl)
          continuation.resume(returning: text)
        } catch {
          continuation.resume(throwing: MyError.internalError("file coordinator read operation failed"))
        }
      }
      
      if errorMain != nil {
        continuation.resume(throwing: MyError.internalError(errorMain!.localizedDescription))
      }
    }
    
    return(text)
  }
}

extension Foo: NSFilePresenter {
  nonisolated var presentedItemURL: URL? {
    return URL(string: "Bla")
  }
  
  nonisolated var presentedItemOperationQueue: OperationQueue {
    if operationQueue == nil {
      operationQueue = OperationQueue()
    }
    return operationQueue! // unsafe, but not the problem here
  }

  nonisolated func presentedItemDidMove(to newURL: URL) {

    // Question: How to load() safely ?
    
    load() // ERROR: 'async' call in a function that does not support concurrency
    
    Task { // ERROR: Passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution of the closure
      await load()
    }

    Task { @MainActor in // ERROR: Task or actor isolated value cannot be sent
      await load()
    }

  }
}

Any suggestions ?


Solution

  • You state that @MainActor is not an option here, without further explanation. So without understanding the rest of your project, I can suggest that it might be that class Foo should really be actor Foo in a fully compliant Swift Concurrency world.

    The overall issue you are having seems to be that you are trying to pass around an instance of Foo on which you wish to call async methods, without supplying any guarantees to the compiler about that class' data race safety.

    A distillation of your code that replicates this compiler problem with only the essential elements would be as follows:

    class Foo: NSObject {
        public func load() async throws -> sending String {
            "foo"
        }
    
        nonisolated func presentedItemDidMove(to newURL: URL) {
            Task { // ❌ Passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution of the closure
                try await load()
            }
        }
    }
    

    If Foo cannot or should not run as @MainActor, the same guarantee can be given either by making it an actor itself or by taking data safety entirely into your own hands and marking it as @unchecked Sendable, which will get the compiler off your back for better and for worse.

    Either of these changes will make the code you have given here compile, however it's impossible to comment on the effect of the changes on the rest of your code.

    actor Foo: NSObject {
        // maintains compiler-level data race safety enforcement
    }
    

    or...

    class Foo: NSObject, @unchecked Sendable {
       // promises the compiler data race safety...
       // ...but removes its ability to enforce this
    }