Is there a way for a nonsendable type to receive an asynchronous callback?

I've been trying to figure this out and I think the answer is no, but I'd like to check:

Is it possible for a non-Sendable type to perform some detached asynchronous operation, with a completion handler that calls back to some method on the instance?

final class NonSendable {

  func doSomething() {
    Task.detached {
        // <... do some work ...>
        // < -- HERE -- >
        self.otherFunc()
    }
  }

  func otherFunc() {
  }
}

At the line marked HERE, as far as I can tell, there is no way to call otherFunc.

So I was trying to figure what a non-Sendable type even means in Swift, and I found this from the region-based isolation proposal:

As defined above, all non-Sendable values in a Swift program belong to some isolation region. An isolation region is isolated to an actor's isolation domain, a task's isolation domain, or disconnected from any specific isolation domain:

[...]

As the program executes, an isolation region can be passed across isolation boundaries, but an isolation region can never be accessed by multiple isolation domains at once. When a region R1 is merged into another region R2 that is isolated to an actor, R1 becomes protected by that isolation domain and cannot be passed or accessed across isolation boundaries again.

If an isolation region (containing a non-Sendable type) is able to move across isolation domains, then it would make sense why it is not possible to safely invoke a callback from some arbitrary other domain - because you don't know whether the region has moved since your task was launched.

In other words, these kind of functions "pin" your non-sendable type (and its isolation region) to the isolation domain they currently live on, until some point which cannot be reasoned about statically (so indefinitely). That's fine for the use-case I have in mind, but I don't believe it's expressible in the language (is it?). I couldn't find anything like that in the proposal or future directions, but I may have missed it.

There are quite a lot of types which are not Sendable, i.e. they cannot be used concurrently from different isolation domains, but you can create them from any isolation domain and then (maybe directly from creation or once you start using them) safely use them only from that same domain. And that is how they are used, and in theory they can support asynchronous callbacks, but I can't find anything in Swift concurrency to model this.

Have I overlooked something? Is there a solution for these kinds of types either currently or planned?

I'm a bit confused by what the external use looks like here that would guarantee safety absent further machinery: NonSendable can't see inside doSomething how it's being used externally, and so it would have no way to know statically that the usage here is safeβ€”to me it appears fundamentally unsafe.

There might be a version which uses an isolated parameter as well as sending to ensure that the internal task is isolated to whatever the isolation is that called doSomethingβ€”but then self would be forever merged with the surrounding isolation of the caller of doSomething (precisely because it could have done something like spawn an internal async callback on the given isolation). Would that also satisfy the use case here?

Yeah that's how it seemed to me - because non-sendable types can move between isolation domains, there's no safe way to make an asynchronous callback to them.

So if we take it that this is indeed inherently not possible in the current language design, I suppose I could describe what I'm looking for in a solution: a class that can hold some non-sendable state, and which provides a fixed isolation domain that I know is safe to access that state from.

Now, clearly the thing I've just described is an actor. But using an actor in this instance is not what I want because they always define their own isolation domain. I don't want this data to live in a separate isolation domain; I want a kind of actor which can also be assigned (once, so maybe transferred/sendinged) to an existing isolation domain.

If the isolation checker knows you are in the same domain as this new kind of actor, you could call it synchronously, similar to @isolated(any) closures. For example, if you have one of these values stored in a UI context it would be isolated to the main actor, and you could call it synchronously from main actor contexts, and if you had one stored inside an actor, it would adopt the isolation of that actor and be callable synchronously within the parent actor.

And I suppose that becoming a kind of actor - and having a single, known isolation domain - would actually make these types Sendable, which makes life much easier.

1 Like

Gotcha, this makes sense. I think in a couple of threads this has come up as allowing a class which is otherwise non-sendable (due to holding some mutable state) to have a blessed property declared as something like:

public let isolation: isolated any Actor

which would then let each instance know that it's immutably bound to some isolation domain. Perhaps given the precedent for function types this could be spelled as @isolated(any) class.


I believe there's a version that works at the expense of adding boilerplate to every method, and it looks like this:

final class NonSendable {
  func doSomething(_ isolation: isolated any Actor = #isolation) {
    Task {
      _ = isolation
      // <... do some work ...>
      // < -- HERE -- >
      otherFunc("doSomething") // OK, no change of isolation domain
    }
  }

  func otherFunc() {
  }
}

Essentially, if each individual method on NonSendable expresses that it must be called with some known isolation then that isolation can be used for the async callback.

1 Like