Skip to content

Commit 6158334

Browse files
authored
Add SPI-based withMainSerialExecutor (#81)
* Add SPI-based `_withMainSerialExecutor` This helper makes it easier to [reliably test Swift concurrency](https://forums.swift.org/t/reliably-testing-code-that-adopts-swift-concurrency/57304/81). * wip
1 parent 5c881bc commit 6158334

File tree

5 files changed

+284
-0
lines changed

5 files changed

+284
-0
lines changed

Package.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ let package = Package(
2323
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "0.8.0"),
2424
],
2525
targets: [
26+
.systemLibrary(name: "_CAsyncSupport"),
2627
.target(
2728
name: "Dependencies",
2829
dependencies: [
30+
"_CAsyncSupport",
2931
.product(name: "CombineSchedulers", package: "combine-schedulers"),
3032
.product(name: "Clocks", package: "swift-clocks"),
3133
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import _CAsyncSupport
2+
3+
@_spi(Concurrency) public func withMainSerialExecutor<T>(
4+
@_implicitSelfCapture operation: () async throws -> T
5+
) async rethrows -> T {
6+
let hook = swift_task_enqueueGlobal_hook
7+
defer { swift_task_enqueueGlobal_hook = hook }
8+
swift_task_enqueueGlobal_hook = { job, original in
9+
MainActor.shared.enqueue(unsafeBitCast(job, to: UnownedJob.self))
10+
}
11+
return try await operation()
12+
}
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift Async Algorithms open source project
4+
//
5+
// Copyright (c) 2022 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
#include <stdint.h>
13+
14+
#if !defined(__has_feature)
15+
#define __has_feature(x) 0
16+
#endif
17+
18+
#if !defined(__has_attribute)
19+
#define __has_attribute(x) 0
20+
#endif
21+
22+
#if !defined(__has_builtin)
23+
#define __has_builtin(builtin) 0
24+
#endif
25+
26+
#if !defined(__has_cpp_attribute)
27+
#define __has_cpp_attribute(attribute) 0
28+
#endif
29+
30+
// TODO: These macro definitions are duplicated in BridgedSwiftObject.h. Move
31+
// them to a single file if we find a location that both Visibility.h and
32+
// BridgedSwiftObject.h can import.
33+
#if __has_feature(nullability)
34+
// Provide macros to temporarily suppress warning about the use of
35+
// _Nullable and _Nonnull.
36+
# define SWIFT_BEGIN_NULLABILITY_ANNOTATIONS \
37+
_Pragma("clang diagnostic push") \
38+
_Pragma("clang diagnostic ignored \"-Wnullability-extension\"")
39+
# define SWIFT_END_NULLABILITY_ANNOTATIONS \
40+
_Pragma("clang diagnostic pop")
41+
42+
#else
43+
// #define _Nullable and _Nonnull to nothing if we're not being built
44+
// with a compiler that supports them.
45+
# define _Nullable
46+
# define _Nonnull
47+
# define SWIFT_BEGIN_NULLABILITY_ANNOTATIONS
48+
# define SWIFT_END_NULLABILITY_ANNOTATIONS
49+
#endif
50+
51+
#define SWIFT_MACRO_CONCAT(A, B) A ## B
52+
#define SWIFT_MACRO_IF_0(IF_TRUE, IF_FALSE) IF_FALSE
53+
#define SWIFT_MACRO_IF_1(IF_TRUE, IF_FALSE) IF_TRUE
54+
#define SWIFT_MACRO_IF(COND, IF_TRUE, IF_FALSE) \
55+
SWIFT_MACRO_CONCAT(SWIFT_MACRO_IF_, COND)(IF_TRUE, IF_FALSE)
56+
57+
#if __has_attribute(pure)
58+
#define SWIFT_READONLY __attribute__((__pure__))
59+
#else
60+
#define SWIFT_READONLY
61+
#endif
62+
63+
#if __has_attribute(const)
64+
#define SWIFT_READNONE __attribute__((__const__))
65+
#else
66+
#define SWIFT_READNONE
67+
#endif
68+
69+
#if __has_attribute(always_inline)
70+
#define SWIFT_ALWAYS_INLINE __attribute__((always_inline))
71+
#else
72+
#define SWIFT_ALWAYS_INLINE
73+
#endif
74+
75+
#if __has_attribute(noinline)
76+
#define SWIFT_NOINLINE __attribute__((__noinline__))
77+
#else
78+
#define SWIFT_NOINLINE
79+
#endif
80+
81+
#if __has_attribute(noreturn)
82+
#define SWIFT_NORETURN __attribute__((__noreturn__))
83+
#else
84+
#define SWIFT_NORETURN
85+
#endif
86+
87+
#if __has_attribute(used)
88+
#define SWIFT_USED __attribute__((__used__))
89+
#else
90+
#define SWIFT_USED
91+
#endif
92+
93+
#if __has_attribute(unavailable)
94+
#define SWIFT_ATTRIBUTE_UNAVAILABLE __attribute__((__unavailable__))
95+
#else
96+
#define SWIFT_ATTRIBUTE_UNAVAILABLE
97+
#endif
98+
99+
#if (__has_attribute(weak_import))
100+
#define SWIFT_WEAK_IMPORT __attribute__((weak_import))
101+
#else
102+
#define SWIFT_WEAK_IMPORT
103+
#endif
104+
105+
// Define the appropriate attributes for sharing symbols across
106+
// image (executable / shared-library) boundaries.
107+
//
108+
// SWIFT_ATTRIBUTE_FOR_EXPORTS will be placed on declarations that
109+
// are known to be exported from the current image. Typically, they
110+
// are placed on header declarations and then inherited by the actual
111+
// definitions.
112+
//
113+
// SWIFT_ATTRIBUTE_FOR_IMPORTS will be placed on declarations that
114+
// are known to be exported from a different image. This never
115+
// includes a definition.
116+
//
117+
// Getting the right attribute on a declaratioon can be pretty awkward,
118+
// but it's necessary under the C translation model. All of this
119+
// ceremony is familiar to Windows programmers; C/C++ programmers
120+
// everywhere else usually don't bother, but since we have to get it
121+
// right for Windows, we have everything set up to get it right on
122+
// other targets as well, and doing so lets the compiler use more
123+
// efficient symbol access patterns.
124+
#if defined(__MACH__) || defined(__wasi__)
125+
126+
// On Mach-O and WebAssembly, we use non-hidden visibility. We just use
127+
// default visibility on both imports and exports, both because these
128+
// targets don't support protected visibility but because they don't
129+
// need it: symbols are not interposable outside the current image
130+
// by default.
131+
# define SWIFT_ATTRIBUTE_FOR_EXPORTS __attribute__((__visibility__("default")))
132+
# define SWIFT_ATTRIBUTE_FOR_IMPORTS __attribute__((__visibility__("default")))
133+
134+
#elif defined(__ELF__)
135+
136+
// On ELF, we use non-hidden visibility. For exports, we must use
137+
// protected visibility to tell the compiler and linker that the symbols
138+
// can't be interposed outside the current image. For imports, we must
139+
// use default visibility because protected visibility guarantees that
140+
// the symbol is defined in the current library, which isn't true for
141+
// an import.
142+
//
143+
// The compiler does assume that the runtime and standard library can
144+
// refer to each other's symbols as DSO-local, so it's important that
145+
// we get this right or we can get linker errors.
146+
# define SWIFT_ATTRIBUTE_FOR_EXPORTS __attribute__((__visibility__("protected")))
147+
# define SWIFT_ATTRIBUTE_FOR_IMPORTS __attribute__((__visibility__("default")))
148+
149+
#elif defined(__CYGWIN__)
150+
151+
// For now, we ignore all this on Cygwin.
152+
# define SWIFT_ATTRIBUTE_FOR_EXPORTS
153+
# define SWIFT_ATTRIBUTE_FOR_IMPORTS
154+
155+
// FIXME: this #else should be some sort of #elif Windows
156+
#else // !__MACH__ && !__ELF__
157+
158+
// On PE/COFF, we use dllimport and dllexport.
159+
# define SWIFT_ATTRIBUTE_FOR_EXPORTS __declspec(dllexport)
160+
# define SWIFT_ATTRIBUTE_FOR_IMPORTS __declspec(dllimport)
161+
162+
#endif
163+
164+
// CMake conventionally passes -DlibraryName_EXPORTS when building
165+
// code that goes into libraryName. This isn't the best macro name,
166+
// but it's conventional. We do have to pass it explicitly in a few
167+
// places in the build system for a variety of reasons.
168+
//
169+
// Unfortunately, defined(D) is a special function you can use in
170+
// preprocessor conditions, not a macro you can use anywhere, so we
171+
// need to manually check for all the libraries we know about so that
172+
// we can use them in our condition below.s
173+
#if defined(swiftCore_EXPORTS)
174+
#define SWIFT_IMAGE_EXPORTS_swiftCore 1
175+
#else
176+
#define SWIFT_IMAGE_EXPORTS_swiftCore 0
177+
#endif
178+
#if defined(swift_Concurrency_EXPORTS)
179+
#define SWIFT_IMAGE_EXPORTS_swift_Concurrency 1
180+
#else
181+
#define SWIFT_IMAGE_EXPORTS_swift_Concurrency 0
182+
#endif
183+
#if defined(swift_Distributed_EXPORTS)
184+
#define SWIFT_IMAGE_EXPORTS_swift_Distributed 1
185+
#else
186+
#define SWIFT_IMAGE_EXPORTS_swift_Distributed 0
187+
#endif
188+
#if defined(swift_Differentiation_EXPORTS)
189+
#define SWIFT_IMAGE_EXPORTS_swift_Differentiation 1
190+
#else
191+
#define SWIFT_IMAGE_EXPORTS_swift_Differentiation 0
192+
#endif
193+
194+
#define SWIFT_EXPORT_FROM_ATTRIBUTE(LIBRARY) \
195+
SWIFT_MACRO_IF(SWIFT_IMAGE_EXPORTS_##LIBRARY, \
196+
SWIFT_ATTRIBUTE_FOR_EXPORTS, \
197+
SWIFT_ATTRIBUTE_FOR_IMPORTS)
198+
199+
// SWIFT_EXPORT_FROM(LIBRARY) declares something to be a C-linkage
200+
// entity exported by the given library.
201+
//
202+
// SWIFT_RUNTIME_EXPORT is just SWIFT_EXPORT_FROM(swiftCore).
203+
//
204+
// TODO: use this in shims headers in overlays.
205+
#if defined(__cplusplus)
206+
#define SWIFT_EXPORT_FROM(LIBRARY) extern "C" SWIFT_EXPORT_FROM_ATTRIBUTE(LIBRARY)
207+
#define SWIFT_EXPORT extern "C"
208+
#else
209+
#define SWIFT_EXPORT extern
210+
#define SWIFT_EXPORT_FROM(LIBRARY) SWIFT_EXPORT_FROM_ATTRIBUTE(LIBRARY)
211+
#endif
212+
#define SWIFT_RUNTIME_EXPORT SWIFT_EXPORT_FROM(swiftCore)
213+
214+
// Define mappings for calling conventions.
215+
216+
// Annotation for specifying a calling convention of
217+
// a runtime function. It should be used with declarations
218+
// of runtime functions like this:
219+
// void runtime_function_name() SWIFT_CC(swift)
220+
#define SWIFT_CC(CC) SWIFT_CC_##CC
221+
222+
// SWIFT_CC(c) is the C calling convention.
223+
#define SWIFT_CC_c
224+
225+
// SWIFT_CC(swift) is the Swift calling convention.
226+
// FIXME: the next comment is false.
227+
// Functions outside the stdlib or runtime that include this file may be built
228+
// with a compiler that doesn't support swiftcall; don't define these macros
229+
// in that case so any incorrect usage is caught.
230+
#if __has_attribute(swiftcall)
231+
#define SWIFT_CC_swift __attribute__((swiftcall))
232+
#define SWIFT_CONTEXT __attribute__((swift_context))
233+
#define SWIFT_ERROR_RESULT __attribute__((swift_error_result))
234+
#define SWIFT_INDIRECT_RESULT __attribute__((swift_indirect_result))
235+
#else
236+
#define SWIFT_CC_swift
237+
#define SWIFT_CONTEXT
238+
#define SWIFT_ERROR_RESULT
239+
#define SWIFT_INDIRECT_RESULT
240+
#endif
241+
242+
typedef struct _Job* JobRef;
243+
244+
typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobal_original)(JobRef _Nonnull job);
245+
SWIFT_EXPORT_FROM(swift_Concurrency)
246+
SWIFT_CC(swift) void (* _Nullable swift_task_enqueueGlobal_hook)(
247+
JobRef _Nonnull job, swift_task_enqueueGlobal_original _Nonnull original);
248+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module _CAsyncSupport [system] {
2+
header "_CAsyncSupport.h"
3+
export *
4+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
@_spi(Concurrency) import Dependencies
2+
import XCTest
3+
4+
final class MainSerialExecutorTests: XCTestCase {
5+
func testSerializedExecution() async {
6+
let xs = LockIsolated<[Int]>([])
7+
await withMainSerialExecutor {
8+
await withTaskGroup(of: Void.self) { group in
9+
for x in 1...1000 {
10+
group.addTask {
11+
xs.withValue { $0.append(x) }
12+
}
13+
}
14+
}
15+
}
16+
xs.withValue { XCTAssertEqual(Array(1...1000), $0) }
17+
}
18+
}

0 commit comments

Comments
 (0)