Is it possible to call global actor functionality synchronously from the actor itself? For example:
@globalActor actor Service: Actor {
static let shared = Service()
func calculate() {
// I would like to call `Accountant` functionality synchronously here.
// Something similar to `Service.shared.assumeIsolated` but that doesn't work
}
}
You need to restructure your code a bit, we don't enforcement in place to actually guarantee that actor is the same instance as the one used in the shared property and therefore is "THE" global actor.
In theory, you could create a bunch of let a Service() and let b = Service() so it is not necessary true that "this instance" is exactly the same one as the one used in the static let shared and therefore the compiler is right to say that you could be crossing actor boundaries between some instance of the ServiceActor and something else "on that global actor". It would be a nice thing to add some analysis to detect that noone is doing such trickery, but it's not a thing today, and it's somewhat of a heavy lift for a not very important capability -- after all, you can just put logic into a class and make it "on the global actor" to get what you want.
Instead, the "inside the global actor" is just any type annotated with it, and therefore you can just:
@globalActor actor ServiceActor: Actor {
static let shared = Service()
}
@ServiceActor final class Service {
func calculate() {
Accountant().test()
}
}
@ServiceActor final class Accountant {
init() {}
func test() {}
}
and calls between those two types will not be a crossing isolation boundary.
Following up, is it possible to call @globalActor marked functionality from a pre-concurrency API that we know is running in such task executor?
For example:
@globalActor actor ServiceActor: Actor {
let queue: DispatchSerialQueue
let unownedExecutor: UnownedSerialExecutor
init(identifier: String) {
self.queue = DispatchSerialQueue(label: identifier + ".queue")
self.unownedExecutor = self.queue.asUnownedSerialExecutor()
}
static let shared = ServiceActor(identifier: XPC_ID)
}
At some point I have a pre-concurrency handler which is assured to be called in the previously defined DispatchQueue.
try XPCListener(service: identifier, targetQueue: ServiceActor.shared.queue, options: .inactive) { _ in
// I know here I am in the `ServiceActor` context.
// I would like to do something like ServiceActor.assumeIsolated
handleSession(request: $0)
}
You'd have to copy paste the code that MainActor does. This is a known limitation from when we introduced assumeIsolated.
public static func assumeIsolated<T : Sendable>(
_ operation: @<<YOUR ACTOR>> () throws -> T,
file: StaticString = #fileID, line: UInt = #line
) rethrows -> T {
typealias YesActor = @<<YOUR ACTOR>> () throws -> T
typealias NoActor = () throws -> T
/// This is guaranteed to be fatal if the check fails,
/// as this is our "safe" version of this API.
let executor: Builtin.Executor = unsafe Self.shared.unownedExecutor.executor
guard _taskIsCurrentExecutor(executor) else {
fatalError("Incorrect actor executor assumption; Expected same executor as \(<<YOUR ACTOR>>).", file: file, }
// To do the unsafe cast, we have to pretend it's @escaping.
return try withoutActuallyEscaping(operation) {
(_ fn: @escaping YesActor) throws -> T in
let rawFn = unsafe unsafeBitCast(fn, to: NoActor.self)
return try rawFn()
}
}
Though you may not be able to get the Builtin.Executor and would have to pull some tricks in and just try pulling it off using unsafe raw pointers instead.
It'd be worth filing an issue on github about this - could you please file one on GitHub - swiftlang/swift: The Swift Programming Language and ping me there? I fear macros are still not enough to express this, but it would be good to track the request formally, thank you!