Skip to content

Commit b1750ba

Browse files
committed
[concurrency] Implement swift_task_runOnMainActor.
This routine takes a synchronous non-throwing main actor isolated closure without a result. If we are dynamically on the main actor, we just run the closure synchronously. Otherwise, we run a new task on the main actor and call the closure on that. This builds on top of the previous commit by using swift_task_isCurrentExecutorWithFlags in the implementation of this function. To backwards deploy this function on Darwin, I used some tricks from libdispatch to validate that we are on the main queue.
1 parent 5fa02d8 commit b1750ba

File tree

5 files changed

+232
-4
lines changed

5 files changed

+232
-4
lines changed

stdlib/public/Concurrency/Task.swift

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,6 +1164,16 @@ func _taskCreateNullaryContinuationJob(priority: Int, continuation: Builtin.RawU
11641164
@_silgen_name("swift_task_isCurrentExecutor")
11651165
func _taskIsCurrentExecutor(_ executor: Builtin.Executor) -> Bool
11661166

1167+
#if !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY && !SWIFT_CONCURRENCY_EMBEDDED
1168+
1169+
@available(SwiftStdlib 9999, *)
1170+
@_silgen_name("swift_task_isCurrentExecutorWithFlags")
1171+
@usableFromInline
1172+
internal func _taskIsCurrentExecutor(
1173+
executor: Builtin.Executor, flags: UInt64) -> Bool
1174+
1175+
#endif
1176+
11671177
@available(SwiftStdlib 5.1, *)
11681178
@usableFromInline
11691179
@_silgen_name("swift_task_reportUnexpectedExecutor")
@@ -1259,3 +1269,47 @@ internal func _runTaskForBridgedAsyncMethod(@_inheritActorContext _ body: __owne
12591269
#endif
12601270

12611271
#endif
1272+
1273+
#if !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY && !SWIFT_CONCURRENCY_EMBEDDED
1274+
1275+
@available(SwiftStdlib 9999, *)
1276+
@_alwaysEmitIntoClient
1277+
@usableFromInline
1278+
internal func _taskIsOnMainActor() -> Bool {
1279+
// 0 for check mode means do not assert.
1280+
return _taskIsCurrentExecutor(executor: _getMainExecutor(),
1281+
flags: 0)
1282+
}
1283+
1284+
/// SPI that is used by the compiler to implement the hop to main actor if
1285+
/// needed thunk. This thunk is used when passing preconcurrency code a main
1286+
/// actor isolated callback. If when invoked, we are dynamically on the main
1287+
/// actor, we just invoke the function without creating a task. Otherwise, we
1288+
/// create a Task and run operation within it.
1289+
///
1290+
/// NOTE: For this to be safe, we need our function to not have any return value
1291+
/// and that includes an error return value. So we cannot accept throwing
1292+
/// functions here.
1293+
@_alwaysEmitIntoClient
1294+
@available(SwiftStdlib 9999, *)
1295+
public func _taskRunOnMainActor(operation: @escaping @MainActor () -> ()) {
1296+
typealias YesActor = @MainActor () -> ()
1297+
typealias NoActor = () -> ()
1298+
1299+
// First check if we are on the main actor. If so, just call the synchronous
1300+
// function directly.
1301+
if _taskIsOnMainActor() {
1302+
return withoutActuallyEscaping(operation) {
1303+
(_ fn: @escaping YesActor) -> () in
1304+
let rawFn = unsafeBitCast(fn, to: NoActor.self)
1305+
return rawFn()
1306+
}
1307+
}
1308+
1309+
// Otherwise, create a new task on the main actor and run operation there.
1310+
Task { @MainActor in
1311+
operation()
1312+
}
1313+
}
1314+
1315+
#endif
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
#include <stdint.h>
3+
4+
uintptr_t swift_task_getCurrent();
5+
6+
uintptr_t getCurrentTaskShim() { return swift_task_getCurrent(); }

test/Concurrency/Reflection/reflect_task.swift

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// RUN: %empty-directory(%t)
2-
// RUN: %target-build-swift -Xfrontend -disable-availability-checking -parse-stdlib -parse-as-library -lswiftSwiftReflectionTest %s -o %t/reflect_task
2+
// RUN: %target-clang -x c %S/Inputs/reflect_task.c -o %t/reflect_task.c.o -c
3+
// RUN: %target-build-swift -c -Xfrontend -disable-availability-checking -parse-stdlib -parse-as-library -lswiftSwiftReflectionTest %s -o %t/reflect_task.swift.o -module-name reflect_task
4+
// RUN: %target-build-swift %t/reflect_task.swift.o %t/reflect_task.c.o -o %t/reflect_task
35
// RUN: %target-codesign %t/reflect_task
46

57
// RUN: %target-run %target-swift-reflection-test %t/reflect_task | %FileCheck %s --dump-input=fail
@@ -16,12 +18,16 @@ import _Concurrency
1618

1719
import SwiftReflectionTest
1820

19-
@_silgen_name("swift_task_getCurrent")
20-
func _getCurrentAsyncTask() -> UInt
21+
// We do not use swift_task_getCurrent directly since we also can get a
22+
// declaration (if we get unlucky) from _Concurrency with a differing type. So
23+
// we instead just compile a shim in a .c file that actually calls
24+
// swift_task_getCurrent and avoid the collision.
25+
@_silgen_name("getCurrentTaskShim")
26+
func _getCurrentTaskShim() -> UInt
2127

2228
func add(_ a: UInt, _ b: UInt) async -> UInt {
2329
if b == 0 {
24-
reflect(asyncTask: _getCurrentAsyncTask())
30+
reflect(asyncTask: _getCurrentTaskShim())
2531
// CHECK: Reflecting an async task.
2632
// CHECK: Async task {{0x[0-9a-fA-F]*}}
2733

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
#include <dispatch/dispatch.h>
3+
4+
static inline void *getDispatchMain() { return (void *)&_dispatch_main_q; }
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// RUN: %target-run-simple-swift( -Xfrontend -disable-availability-checking -Xfrontend -disable-dynamic-actor-isolation -swift-version 6 -g -import-objc-header %S/Inputs/RunOnMainActor.h %import-libdispatch )
2+
3+
// REQUIRES: executable_test
4+
// REQUIRES: concurrency
5+
// REQUIRES: concurrency_runtime
6+
// REQUIRES: libdispatch
7+
// REQUIRES: asserts
8+
9+
// UNSUPPORTED: freestanding
10+
11+
// For now we do not support back deployment or use os stdlib
12+
// UNSUPPORTED: back_deployment_concurrency
13+
// UNSUPPORTED: use_os_stdlib
14+
15+
import StdlibUnittest
16+
import Dispatch
17+
18+
// MARK: Test runOnMainActor in all modes. We use the one future proof API in
19+
// dispatch: dispatch_assert_queue.
20+
21+
////////////////////////
22+
// MARK: Declarations //
23+
////////////////////////
24+
25+
@_silgen_name("dispatch_assert_queue")
26+
func dispatch_assertQueue(_ ptr: UnsafeRawPointer)
27+
28+
func checkIfOnMainQueue() {
29+
dispatch_assertQueue(getDispatchMain())
30+
}
31+
32+
actor Custom {
33+
}
34+
35+
@globalActor
36+
struct CustomActor {
37+
static var shared: Custom {
38+
return Custom()
39+
}
40+
}
41+
42+
/////////////////
43+
// MARK: Tests //
44+
/////////////////
45+
46+
let tests = TestSuite("RunOnMainActor")
47+
48+
tests.test("checkIfOnMainQueue does not crash on the main queue") { @MainActor () -> () in
49+
// Why do we crash if this is synchronous.
50+
expectCrashLater()
51+
checkIfOnMainQueue()
52+
}
53+
54+
tests.test("checkIfOnMainQueue does not crash on the main queue") { @MainActor () async -> () in
55+
checkIfOnMainQueue()
56+
}
57+
58+
tests.test("checkIfOnMainQueue crashes off the main queue") {
59+
expectCrashLater()
60+
await { @CustomActor in
61+
print("=> checkIfOnMainQueue crashes off the main queue")
62+
checkIfOnMainQueue()
63+
}()
64+
}
65+
66+
tests.test("checkIfOnMainQueue crashes off the main queue 2") { @CustomActor () async -> () in
67+
expectCrashLater()
68+
print("=> checkIfOnMainQueue crashes off the main queue 2")
69+
checkIfOnMainQueue()
70+
}
71+
72+
tests.test("checkIfOnMainQueue crashes using pure dispatch queue") { @CustomActor () async -> () in
73+
expectCrashLater()
74+
75+
let queue = DispatchQueue(label: "")
76+
77+
await withCheckedContinuation { cont in
78+
queue.async {
79+
checkIfOnMainQueue()
80+
cont.resume()
81+
}
82+
}
83+
}
84+
85+
tests.test("RunOnMainActor on MainActor") { @MainActor () async -> () in
86+
// This is main actor isolated.
87+
_taskRunOnMainActor { @MainActor in
88+
print("=> RunOnMainActor on MainActor")
89+
checkIfOnMainQueue()
90+
}
91+
}
92+
93+
tests.test("RunOnMainActor off MainActor") {
94+
await { @CustomActor in
95+
_taskRunOnMainActor { @MainActor in
96+
print("=> RunOnMainActor off MainActor")
97+
checkIfOnMainQueue()
98+
}
99+
}()
100+
}
101+
102+
tests.test("RunOnMainActor off MainActor 2") { @CustomActor () async -> () in
103+
_taskRunOnMainActor {
104+
print("=> RunOnMainActor off MainActor 2")
105+
checkIfOnMainQueue()
106+
}
107+
}
108+
109+
// This succeeds since assert understands that a child queue runs on the main
110+
// queue.
111+
tests.test("checkIfOnMainQueue in SubQueue from MainActor") { @CustomActor () async -> () in
112+
let childQueue = DispatchQueue.init(label: "", qos: .background, attributes: [], autoreleaseFrequency: .inherit, target: DispatchQueue.main)
113+
114+
print("=> checkIfOnMainQueue in SubQueue from MainActor start!")
115+
116+
// We cannot use the checked continuation, since we are not going to come back
117+
// to the custom actor.
118+
await withCheckedContinuation { cont in
119+
childQueue.async {
120+
print("=> checkIfOnMainQueue in SubQueue from MainActor")
121+
checkIfOnMainQueue()
122+
cont.resume()
123+
}
124+
}
125+
}
126+
127+
// This should not fail when backwards deployed since we use the backwards
128+
// compatibility trick.
129+
tests.test("taskRunOnMainActor in SubQueue from MainActor") { @CustomActor () async -> () in
130+
let childQueue = DispatchQueue.init(label: "", qos: .background, attributes: [], autoreleaseFrequency: .inherit, target: DispatchQueue.main)
131+
132+
await withCheckedContinuation { cont in
133+
childQueue.async {
134+
_taskRunOnMainActor { @MainActor () -> () in
135+
print("=> taskRunOnMainActor in SubQueue from MainActor")
136+
checkIfOnMainQueue()
137+
cont.resume()
138+
}
139+
}
140+
}
141+
}
142+
143+
tests.test("taskRunOnMainActor in SubQueue off MainActor") { @CustomActor () async -> () in
144+
let d = DispatchQueue.init(label: "", qos: .background)
145+
let childQueue = DispatchQueue.init(label: "", qos: .background, attributes: [], autoreleaseFrequency: .inherit, target: d)
146+
147+
await withCheckedContinuation { cont in
148+
childQueue.async {
149+
_taskRunOnMainActor { @MainActor in
150+
print("=> taskRunOnMainActor in SubQueue from MainActor")
151+
checkIfOnMainQueue()
152+
cont.resume()
153+
}
154+
}
155+
}
156+
}
157+
158+
await runAllTestsAsync()

0 commit comments

Comments
 (0)