Skip to content

Commit cc9bb7a

Browse files
committed
Handle multiple awaits and suspend-on-exit for async let tasks.
Change the code generation patterns for `async let` bindings to use an ABI based on the following functions: - `swift_asyncLet_begin`, which starts an `async let` child task, but which additionally now associates the `async let` with a caller-owned buffer to receive the result of the task. This is intended to allow the task to emplace its result in caller-owned memory, allowing the child task to be deallocated after completion without invalidating the result buffer. - `swift_asyncLet_get[_throwing]`, which replaces `swift_asyncLet_wait[_throwing]`. Instead of returning a copy of the value, this entry point concerns itself with populating the local buffer. If the buffer hasn't been populated, then it awaits completion of the task and emplaces the result in the buffer; otherwise, it simply returns. The caller can then read the result out of its owned memory. These entry points are intended to be used before every read from the `async let` binding, after which point the local buffer is guaranteed to contain an initialized value. - `swift_asyncLet_finish`, which replaces `swift_asyncLet_end`. Unlike `_end`, this variant is async and will suspend the parent task after cancelling the child to ensure it finishes before cleaning up. The local buffer will also be deinitialized if necessary. This is intended to be used on exit from an `async let` scope, to handle cleaning up the local buffer if necessary as well as cancelling, awaiting, and deallocating the child task. - `swift_asyncLet_consume[_throwing]`, which combines `get` and `finish`. This will await completion of the task, leaving the result value in the result buffer (or propagating the error, if it throws), while destroying and deallocating the child task. This is intended as an optimization for reading `async let` variables that are read exactly once by their parent task. To avoid an epoch break with existing swiftinterfaces and ABI clients, the old builtins and entry points are kept intact for now, but SILGen now only generates code using the new interface. This new interface fixes several issues with the old async let codegen, including use-after-free crashes if the `async let` was never awaited, and the inability to read from an `async let` variable more than once. rdar://77855176
1 parent d5b0c2a commit cc9bb7a

31 files changed

+1051
-144
lines changed

include/swift/ABI/AsyncLet.h

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,29 @@ class alignas(Alignment_AsyncLet) AsyncLet {
3636
constexpr AsyncLet()
3737
: PrivateData{} {}
3838

39-
// FIXME: not sure how many words we should reserve
4039
void *PrivateData[NumWords_AsyncLet];
4140

4241
// TODO: we could offer a "was awaited on" check here
4342

4443
/// Returns the child task that is associated with this async let.
4544
/// The tasks completion is used to fulfil the value represented by this async let.
4645
AsyncTask *getTask() const;
47-
46+
47+
// The compiler preallocates a large fixed space for the `async let`, with the
48+
// intent that most of it be used for the child task context. The next two
49+
// methods return the address and size of that space.
50+
51+
/// Return a pointer to the unused space within the async let block.
52+
void *getPreallocatedSpace();
53+
54+
/// Return the size of the unused space within the async let block.
55+
static size_t getSizeOfPreallocatedSpace();
56+
57+
/// Was the task allocated out of the parent's allocator?
58+
bool didAllocateFromParentTask();
59+
60+
/// Flag that the task was allocated from the parent's allocator.
61+
void setDidAllocateFromParentTask(bool value = true);
4862
};
4963

5064
} // end namespace swift

include/swift/ABI/MetadataValues.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ enum {
5353
/// The number of words in a task group.
5454
NumWords_TaskGroup = 32,
5555

56-
/// The number of words in an AsyncLet (flags + task pointer)
57-
NumWords_AsyncLet = 8, // TODO: not sure how much is enough, these likely could be pretty small
56+
/// The number of words in an AsyncLet (flags + child task context & allocation)
57+
NumWords_AsyncLet = 80, // 640 bytes ought to be enough for anyone
5858
};
5959

6060
struct InProcess;
@@ -2145,8 +2145,11 @@ enum class TaskOptionRecordKind : uint8_t {
21452145
Executor = 0,
21462146
/// Request a child task to be part of a specific task group.
21472147
TaskGroup = 1,
2148+
/// DEPRECATED. AsyncLetWithBuffer is used instead.
21482149
/// Request a child task for an 'async let'.
21492150
AsyncLet = 2,
2151+
/// Request a child task for an 'async let'.
2152+
AsyncLetWithBuffer = 3,
21502153
};
21512154

21522155
/// Flags for cancellation records.

include/swift/ABI/TaskOptions.h

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,12 @@ class ExecutorTaskOptionRecord : public TaskOptionRecord {
9898
}
9999
};
100100

101+
/// DEPRECATED. AsyncLetWithBufferTaskOptionRecord is used instead.
101102
/// Task option to specify that the created task is for an 'async let'.
102103
class AsyncLetTaskOptionRecord : public TaskOptionRecord {
103104
AsyncLet *asyncLet;
104105

105-
public:
106+
public:
106107
AsyncLetTaskOptionRecord(AsyncLet *asyncLet)
107108
: TaskOptionRecord(TaskOptionRecordKind::AsyncLet),
108109
asyncLet(asyncLet) {}
@@ -116,6 +117,30 @@ class AsyncLetTaskOptionRecord : public TaskOptionRecord {
116117
}
117118
};
118119

120+
class AsyncLetWithBufferTaskOptionRecord : public TaskOptionRecord {
121+
AsyncLet *asyncLet;
122+
void *resultBuffer;
123+
124+
public:
125+
AsyncLetWithBufferTaskOptionRecord(AsyncLet *asyncLet,
126+
void *resultBuffer)
127+
: TaskOptionRecord(TaskOptionRecordKind::AsyncLetWithBuffer),
128+
asyncLet(asyncLet),
129+
resultBuffer(resultBuffer) {}
130+
131+
AsyncLet *getAsyncLet() const {
132+
return asyncLet;
133+
}
134+
135+
void *getResultBuffer() const {
136+
return resultBuffer;
137+
}
138+
139+
static bool classof(const TaskOptionRecord *record) {
140+
return record->getKind() == TaskOptionRecordKind::AsyncLetWithBuffer;
141+
}
142+
};
143+
119144
} // end namespace swift
120145

121146
#endif

include/swift/AST/Builtins.def

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -782,18 +782,43 @@ BUILTIN_MISC_OPERATION_WITH_SILGEN(CancelAsyncTask, "cancelAsyncTask", "", Speci
782782
/// __owned @Sendable @escaping () async throws -> T
783783
/// ) -> Builtin.RawPointer
784784
///
785+
/// DEPRECATED. startAsyncLetWithLocalBuffer is used instead.
786+
///
785787
/// Create, initialize and start a new async-let and associated task.
786788
/// Returns an AsyncLet* that must be passed to endAsyncLet for destruction.
787789
BUILTIN_MISC_OPERATION(StartAsyncLet, "startAsyncLet", "", Special)
788790

789-
/// asyncLetEnd(): (Builtin.RawPointer) -> Void
791+
/// startAsyncLetWithLocalBuffer()<T>: (
792+
/// __owned @Sendable @escaping () async throws -> T,
793+
/// _ resultBuf: Builtin.RawPointer
794+
/// ) -> Builtin.RawPointer
795+
///
796+
/// Create, initialize and start a new async-let and associated task, with a
797+
/// locally-allocated buffer assigned to receive the result if the task
798+
/// completes.
799+
/// Returns an AsyncLet* that must be passed to endAsyncLetLifetime for
800+
/// destruction.
801+
BUILTIN_MISC_OPERATION(StartAsyncLetWithLocalBuffer, "startAsyncLetWithLocalBuffer", "", Special)
802+
803+
/// endAsyncLet(): (Builtin.RawPointer) -> Void
804+
///
805+
/// DEPRECATED. The swift_asyncLet_finish intrinsic and endAsyncLetLifetime
806+
/// builtin are used instead.
790807
///
791808
/// Ends and destroys an async-let.
792809
/// The ClosureLifetimeFixup pass adds a second operand to the builtin to
793810
/// ensure that optimizations keep the stack-allocated closure arguments alive
794811
/// until the endAsyncLet.
795812
BUILTIN_MISC_OPERATION_WITH_SILGEN(EndAsyncLet, "endAsyncLet", "", Special)
796813

814+
/// endAsyncLetLifetime(): (Builtin.RawPointer) -> Void
815+
///
816+
/// Marks the end of an async-let's lifetime.
817+
/// The ClosureLifetimeFixup pass adds a second operand to the builtin to
818+
/// ensure that optimizations keep the stack-allocated closure arguments alive
819+
/// until the endAsyncLet.
820+
BUILTIN_MISC_OPERATION(EndAsyncLetLifetime, "endAsyncLetLifetime", "", Special)
821+
797822
/// createAsyncTask(): (
798823
/// Int, // task-creation flags
799824
/// @escaping () async throws -> T // function

include/swift/Runtime/Concurrency.h

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ bool swift_taskGroup_isCancelled(TaskGroup *group);
240240
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
241241
bool swift_taskGroup_isEmpty(TaskGroup *group);
242242

243+
/// DEPRECATED. swift_asyncLet_begin is used instead.
243244
/// Its Swift signature is
244245
///
245246
/// \code
@@ -255,12 +256,31 @@ void swift_asyncLet_start(AsyncLet *alet,
255256
const Metadata *futureResultType,
256257
void *closureEntryPoint, HeapObject *closureContext);
257258

259+
/// Begin an async let child task.
260+
/// Its Swift signature is
261+
///
262+
/// \code
263+
/// func swift_asyncLet_start<T>(
264+
/// asyncLet: Builtin.RawPointer,
265+
/// options: Builtin.RawPointer?,
266+
/// operation: __owned @Sendable () async throws -> T,
267+
/// resultBuffer: Builtin.RawPointer
268+
/// )
269+
/// \endcode
270+
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
271+
void swift_asyncLet_begin(AsyncLet *alet,
272+
TaskOptionRecord *options,
273+
const Metadata *futureResultType,
274+
void *closureEntryPoint, HeapObject *closureContext,
275+
void *resultBuffer);
276+
258277
/// This matches the ABI of a closure `<T>(Builtin.RawPointer) async -> T`
259278
using AsyncLetWaitSignature =
260279
SWIFT_CC(swiftasync)
261280
void(OpaqueValue *,
262281
SWIFT_ASYNC_CONTEXT AsyncContext *, AsyncTask *, Metadata *);
263282

283+
/// DEPRECATED. swift_asyncLet_get is used instead.
264284
/// Wait for a non-throwing async-let to complete.
265285
///
266286
/// This can be called from any thread. Its Swift signature is
@@ -276,6 +296,7 @@ void swift_asyncLet_wait(OpaqueValue *,
276296
AsyncLet *, TaskContinuationFunction *,
277297
AsyncContext *);
278298

299+
/// DEPRECATED. swift_asyncLet_get_throwing is used instead.
279300
/// Wait for a potentially-throwing async-let to complete.
280301
///
281302
/// This can be called from any thread. Its Swift signature is
@@ -292,6 +313,7 @@ void swift_asyncLet_wait_throwing(OpaqueValue *,
292313
ThrowingTaskFutureWaitContinuationFunction *,
293314
AsyncContext *);
294315

316+
/// DEPRECATED. swift_asyncLet_finish is used instead.
295317
/// Its Swift signature is
296318
///
297319
/// \code
@@ -300,6 +322,134 @@ void swift_asyncLet_wait_throwing(OpaqueValue *,
300322
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
301323
void swift_asyncLet_end(AsyncLet *alet);
302324

325+
/// Get the value of a non-throwing async-let, awaiting the result if necessary.
326+
///
327+
/// This can be called from any thread. Its Swift signature is
328+
///
329+
/// \code
330+
/// func swift_asyncLet_get(
331+
/// _ asyncLet: Builtin.RawPointer,
332+
/// _ resultBuffer: Builtin.RawPointer
333+
/// ) async
334+
/// \endcode
335+
///
336+
/// \c result points at the variable storage for the binding. It is
337+
/// uninitialized until the first call to \c swift_asyncLet_get or
338+
/// \c swift_asyncLet_get_throwing. That first call initializes the storage
339+
/// with the result of the child task. Subsequent calls do nothing and leave
340+
/// the value in place.
341+
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swiftasync)
342+
void swift_asyncLet_get(SWIFT_ASYNC_CONTEXT AsyncContext *,
343+
AsyncLet *,
344+
void *,
345+
TaskContinuationFunction *,
346+
AsyncContext *);
347+
348+
/// Get the value of a throwing async-let, awaiting the result if necessary.
349+
///
350+
/// This can be called from any thread. Its Swift signature is
351+
///
352+
/// \code
353+
/// func swift_asyncLet_get_throwing(
354+
/// _ asyncLet: Builtin.RawPointer,
355+
/// _ resultBuffer: Builtin.RawPointer
356+
/// ) async throws
357+
/// \endcode
358+
///
359+
/// \c result points at the variable storage for the binding. It is
360+
/// uninitialized until the first call to \c swift_asyncLet_get or
361+
/// \c swift_asyncLet_get_throwing. That first call initializes the storage
362+
/// with the result of the child task. Subsequent calls do nothing and leave
363+
/// the value in place. A pointer to the storage inside the child task is
364+
/// returned if the task completes successfully, otherwise the error from the
365+
/// child task is thrown.
366+
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swiftasync)
367+
void swift_asyncLet_get_throwing(SWIFT_ASYNC_CONTEXT AsyncContext *,
368+
AsyncLet *,
369+
void *,
370+
ThrowingTaskFutureWaitContinuationFunction *,
371+
AsyncContext *);
372+
373+
/// Exit the scope of an async-let binding. If the task is still running, it
374+
/// is cancelled, and we await its completion; otherwise, we destroy the
375+
/// value in the variable storage.
376+
///
377+
/// Its Swift signature is
378+
///
379+
/// \code
380+
/// func swift_asyncLet_finish(_ asyncLet: Builtin.RawPointer,
381+
/// _ resultBuffer: Builtin.RawPointer) async
382+
/// \endcode
383+
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swiftasync)
384+
void swift_asyncLet_finish(SWIFT_ASYNC_CONTEXT AsyncContext *,
385+
AsyncLet *,
386+
void *,
387+
TaskContinuationFunction *,
388+
AsyncContext *);
389+
390+
/// Get the value of a non-throwing async-let, awaiting the result if necessary,
391+
/// and then destroy the child task. The result buffer is left initialized after
392+
/// returning.
393+
///
394+
/// This can be called from any thread. Its Swift signature is
395+
///
396+
/// \code
397+
/// func swift_asyncLet_get(
398+
/// _ asyncLet: Builtin.RawPointer,
399+
/// _ resultBuffer: Builtin.RawPointer
400+
/// ) async
401+
/// \endcode
402+
///
403+
/// \c result points at the variable storage for the binding. It is
404+
/// uninitialized until the first call to \c swift_asyncLet_get or
405+
/// \c swift_asyncLet_get_throwing. The child task will be invalidated after
406+
/// this call, so the `async let` can not be gotten or finished afterward.
407+
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swiftasync)
408+
void swift_asyncLet_consume(SWIFT_ASYNC_CONTEXT AsyncContext *,
409+
AsyncLet *,
410+
void *,
411+
TaskContinuationFunction *,
412+
AsyncContext *);
413+
414+
/// Get the value of a throwing async-let, awaiting the result if necessary,
415+
/// and then destroy the child task. The result buffer is left initialized after
416+
/// returning.
417+
///
418+
/// This can be called from any thread. Its Swift signature is
419+
///
420+
/// \code
421+
/// func swift_asyncLet_get_throwing(
422+
/// _ asyncLet: Builtin.RawPointer,
423+
/// _ resultBuffer: Builtin.RawPointer
424+
/// ) async throws
425+
/// \endcode
426+
///
427+
/// \c result points at the variable storage for the binding. It is
428+
/// uninitialized until the first call to \c swift_asyncLet_get or
429+
/// \c swift_asyncLet_get_throwing. That first call initializes the storage
430+
/// with the result of the child task. Subsequent calls do nothing and leave
431+
/// the value in place. The child task will be invalidated after
432+
/// this call, so the `async let` can not be gotten or finished afterward.
433+
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swiftasync)
434+
void swift_asyncLet_consume_throwing(SWIFT_ASYNC_CONTEXT AsyncContext *,
435+
AsyncLet *,
436+
void *,
437+
ThrowingTaskFutureWaitContinuationFunction *,
438+
AsyncContext *);
439+
440+
/// Returns true if the currently executing AsyncTask has a
441+
/// 'TaskGroupTaskStatusRecord' present.
442+
///
443+
/// This can be called from any thread.
444+
///
445+
/// Its Swift signature is
446+
///
447+
/// \code
448+
/// func swift_taskGroup_hasTaskGroupRecord()
449+
/// \endcode
450+
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
451+
bool swift_taskGroup_hasTaskGroupRecord();
452+
303453
/// Add a status record to a task. The record should not be
304454
/// modified while it is registered with a task.
305455
///

include/swift/Runtime/RuntimeFunctions.def

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1674,11 +1674,32 @@ FUNCTION(AsyncLetStart,
16741674
swift_asyncLet_start, SwiftCC,
16751675
ConcurrencyAvailability,
16761676
RETURNS(VoidTy),
1677+
ARGS(SwiftAsyncLetPtrTy, // AsyncLet*
1678+
SwiftTaskOptionRecordPtrTy, // options
1679+
TypeMetadataPtrTy, // futureResultType
1680+
Int8PtrTy, // closureEntry
1681+
OpaquePtrTy // closureContext
1682+
),
1683+
ATTRS(NoUnwind, ArgMemOnly))
1684+
1685+
/// void swift_asyncLet_begin(
1686+
/// AsyncLet *alet,
1687+
/// TaskOptionRecord *options,
1688+
/// const Metadata *futureResultType,
1689+
/// void *closureEntryPoint,
1690+
/// HeapObject *closureContext,
1691+
/// void *resultBuffer
1692+
/// );
1693+
FUNCTION(AsyncLetBegin,
1694+
swift_asyncLet_begin, SwiftCC,
1695+
ConcurrencyAvailability,
1696+
RETURNS(VoidTy),
16771697
ARGS(SwiftAsyncLetPtrTy, // AsyncLet*
16781698
SwiftTaskOptionRecordPtrTy, // options
16791699
TypeMetadataPtrTy, // futureResultType
16801700
Int8PtrTy, // closureEntry
16811701
OpaquePtrTy, // closureContext
1702+
Int8PtrTy
16821703
),
16831704
ATTRS(NoUnwind, ArgMemOnly))
16841705

lib/AST/Builtins.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2790,9 +2790,11 @@ ValueDecl *swift::getBuiltinValueDecl(ASTContext &Context, Identifier Id) {
27902790
return getDefaultActorInitDestroy(Context, Id);
27912791

27922792
case BuiltinValueKind::StartAsyncLet:
2793+
case BuiltinValueKind::StartAsyncLetWithLocalBuffer:
27932794
return getStartAsyncLet(Context, Id);
27942795

27952796
case BuiltinValueKind::EndAsyncLet:
2797+
case BuiltinValueKind::EndAsyncLetLifetime:
27962798
return getEndAsyncLet(Context, Id);
27972799

27982800
case BuiltinValueKind::CreateTaskGroup:

0 commit comments

Comments
 (0)