The concrete things I'm hoping this proposal would help with are:
1: Replace _unsafeInheritExecutor
to enable copying non-sendable data before leaving the caller's executor:
// before
@_unsafeInheritExecutor
public func foo(value: Nonsendable) async {
let sendable = value.sendableCopy()
await foo(sendableValue: sendable)
}
// after
public func foo(value: Nonsendable, _isolated: (any Actor)? = #isolated) async {
let sendable = value.sendableCopy()
await foo(sendableValue: sendable)
}
The fact that actor isolation is passed as a parameter is awkward here as no argument other than what #isolated
produces can ever be valid. The only idea I have for improving this would be a way to mark a parameter as default-only and not explicitly settable, which could potentially be useful for other expression macros as well. This would be a purely additive thing so it could be done later if people do turn out to be often confused by this.
With the current toolchain this produces warnings when I try to use it:
public class Nonsendable {}
public func foo(value: Nonsendable, _isolated: isolated (any Actor)? /*= #isolated*/) async -> Nonsendable {
value
}
// Non-sendable type 'Nonsendable' returned by implicitly asynchronous call to actor-isolated function cannot cross actor boundary
// Passing argument of non-sendable type 'Nonsendable' into actor-isolated context may introduce data races
@MainActor func fromMainActor() async -> Nonsendable {
await foo(value: Nonsendable(), _isolated: MainActor.shared)
}
// no warnings
func fromIsolated(isolated: isolated (any Actor)?) async -> Nonsendable {
await foo(value: Nonsendable(), _isolated: isolated)
}
// same warnings as fromMainActor
func fromNonisolated() async -> Nonsendable {
await foo(value: Nonsendable(), _isolated: nil)
}
My understanding is that none of these actually cross an actor boundary, since foo
is always inheriting the caller's actor. Is this just a temporary shortcoming of the current implementation, something that requires #isolated
, or something that's actually expected not to work?
2: Creating object instances isolated to the current actor:
Realm objects need to be confined to some sort of isolation context which ensures that the objects will only be used from one thread at a time and allows us to schedule work to be performed in the isolation context from outside of it. Actors satisfy both of these, so we have:
struct Realm { // not Sendable
init<A: Actor>(configuration: Realm.Configuration, actor: A) async throws
}
Since it's non-Sendable, normal sendability checking ensures it doesn't leave the isolation context it's created in (until RBI breaks that?), and the actor instance is stored and used to schedule work inside the isolation context. The problems is this API are that it's a bit clunky (since you have to explicitly pass in the current actor), and more importantly it kinda doesn't work. The actor parameter isn't isolated
because isolated parameters for initializers were never implemented, which is something I discovered while first implementing this and then apparently completely forgot about without ever filing a bug report. The actual implementation immediately dispatches to a function which does have an isolated parameter, and this whole thing produces several sendability warnings.
From reading the proposal I think I should be able to do init(configuration: Realm.Configuration, actor: any Actor = #isolated) async throws
and everything will work exactly as desired.
3: Passing isolation parameters to obj-c async functions
We have a Swift wrapper around a c++/obj-c library, and so some of the Swift async functions end up making called to bridged obj-c functions. In Swift 5.9 these started producing sendability warnings which we made go away by marking the obj-c functions with __attribute__((swift_attr("@_unsafeInheritExecutor")))
(which I somewhat suspect may not be something that intentionally works).
If it's possible to implement it'd be nice to have something along the lines of __attribute__((swift_isolated(1)))
to indicate that the first parameter is an isolated actor parameter.
My other thought on this is that withCheckedContinuation()
should use #isolated
. We switched from it to using obj-c async function bridging because on the backdeployed runtime it failed to inherit the caller's executor and so didn't call the block synchronously on the caller's thread as the documentation claims it does. Switching from using _unsafeInheritExecutor
to an official language feature for it should make it more reliable and less weird?