I was recently designing an API with the following constraints:
@MainActor
, so can only be executed on the main actor.V
, but the type doesn't matter right now).Picture this:
@MainActor
func handle<V>(_ value: V) {
Task.detached {
// Not on the main actor.
//
// Do some stuff...
Task { @MainActor in
// Back on the main actor.
value.something()
}
}
}
Going down this path, V
has to be constrained as Sendable
(to avoid warnings when strict concurrency checking is enabled), which is an unfortunate over-requirement. Whilst the reference is being passed between concurrency domains (i.e. "sent"), it's only ever interacted with on the main actor. Therefore I tried using a lightweight box type to "ship" a value from an @MainActor
context to another.
struct MainActorBox<V>: @unchecked Sendable {
@MainActor
init(value: V) {
self.value = value
}
@MainActor
func get() -> V {
value
}
private let value: V
}
The box itself is Sendable
, however the value is effectively hidden away when not on the main actor. This allows the box to be safely transferred between concurrency domains without warnings.
@MainActor
func handle<V>(_ value: V) {
let box = MainActorBox(value: value)
Task.detached {
// Not on the main actor.
//
// Do some stuff...
Task { @MainActor in
// Back on the main actor.
box.get().something()
}
}
}
It would be awesome if the compiler could figure this out without having to wrap the type, but I acknowledge that this could be quite tricky to do statically (particularly for a non-trivial example).
Another improvement would be able to make this generic to any concurrency domain (rather than just the main actor), however I don't believe that's possible right now (even for custom global actors).