Skip to content

Commit 439edbc

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 9f8cc21 commit 439edbc

30 files changed

+1019
-145
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
@@ -793,18 +793,43 @@ BUILTIN_MISC_OPERATION_WITH_SILGEN(CancelAsyncTask, "cancelAsyncTask", "", Speci
793793
/// __owned @Sendable @escaping () async throws -> T
794794
/// ) -> Builtin.RawPointer
795795
///
796+
/// DEPRECATED. startAsyncLetWithLocalBuffer is used instead.
797+
///
796798
/// Create, initialize and start a new async-let and associated task.
797799
/// Returns an AsyncLet* that must be passed to endAsyncLet for destruction.
798800
BUILTIN_MISC_OPERATION(StartAsyncLet, "startAsyncLet", "", Special)
799801

800-
/// asyncLetEnd(): (Builtin.RawPointer) -> Void
802+
/// startAsyncLetWithLocalBuffer()<T>: (
803+
/// __owned @Sendable @escaping () async throws -> T,
804+
/// _ resultBuf: Builtin.RawPointer
805+
/// ) -> Builtin.RawPointer
806+
///
807+
/// Create, initialize and start a new async-let and associated task, with a
808+
/// locally-allocated buffer assigned to receive the result if the task
809+
/// completes.
810+
/// Returns an AsyncLet* that must be passed to endAsyncLetLifetime for
811+
/// destruction.
812+
BUILTIN_MISC_OPERATION(StartAsyncLetWithLocalBuffer, "startAsyncLetWithLocalBuffer", "", Special)
813+
814+
/// endAsyncLet(): (Builtin.RawPointer) -> Void
815+
///
816+
/// DEPRECATED. The swift_asyncLet_finish intrinsic and endAsyncLetLifetime
817+
/// builtin are used instead.
801818
///
802819
/// Ends and destroys an async-let.
803820
/// The ClosureLifetimeFixup pass adds a second operand to the builtin to
804821
/// ensure that optimizations keep the stack-allocated closure arguments alive
805822
/// until the endAsyncLet.
806823
BUILTIN_MISC_OPERATION_WITH_SILGEN(EndAsyncLet, "endAsyncLet", "", Special)
807824

825+
/// endAsyncLetLifetime(): (Builtin.RawPointer) -> Void
826+
///
827+
/// Marks the end of an async-let's lifetime.
828+
/// The ClosureLifetimeFixup pass adds a second operand to the builtin to
829+
/// ensure that optimizations keep the stack-allocated closure arguments alive
830+
/// until the endAsyncLet.
831+
BUILTIN_MISC_OPERATION(EndAsyncLetLifetime, "endAsyncLetLifetime", "", Special)
832+
808833
/// createAsyncTask(): (
809834
/// Int, // task-creation flags
810835
/// @escaping () async throws -> T // function

include/swift/Runtime/Concurrency.h

Lines changed: 137 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,121 @@ 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+
303440
/// Returns true if the currently executing AsyncTask has a
304441
/// 'TaskGroupTaskStatusRecord' present.
305442
///

include/swift/Runtime/RuntimeFunctions.def

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1682,11 +1682,32 @@ FUNCTION(AsyncLetStart,
16821682
swift_asyncLet_start, SwiftCC,
16831683
ConcurrencyAvailability,
16841684
RETURNS(VoidTy),
1685+
ARGS(SwiftAsyncLetPtrTy, // AsyncLet*
1686+
SwiftTaskOptionRecordPtrTy, // options
1687+
TypeMetadataPtrTy, // futureResultType
1688+
Int8PtrTy, // closureEntry
1689+
OpaquePtrTy // closureContext
1690+
),
1691+
ATTRS(NoUnwind, ArgMemOnly))
1692+
1693+
/// void swift_asyncLet_begin(
1694+
/// AsyncLet *alet,
1695+
/// TaskOptionRecord *options,
1696+
/// const Metadata *futureResultType,
1697+
/// void *closureEntryPoint,
1698+
/// HeapObject *closureContext,
1699+
/// void *resultBuffer
1700+
/// );
1701+
FUNCTION(AsyncLetBegin,
1702+
swift_asyncLet_begin, SwiftCC,
1703+
ConcurrencyAvailability,
1704+
RETURNS(VoidTy),
16851705
ARGS(SwiftAsyncLetPtrTy, // AsyncLet*
16861706
SwiftTaskOptionRecordPtrTy, // options
16871707
TypeMetadataPtrTy, // futureResultType
16881708
Int8PtrTy, // closureEntry
16891709
OpaquePtrTy, // closureContext
1710+
Int8PtrTy
16901711
),
16911712
ATTRS(NoUnwind, ArgMemOnly))
16921713

lib/AST/Builtins.cpp

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

28142814
case BuiltinValueKind::StartAsyncLet:
2815+
case BuiltinValueKind::StartAsyncLetWithLocalBuffer:
28152816
return getStartAsyncLet(Context, Id);
28162817

28172818
case BuiltinValueKind::EndAsyncLet:
2819+
case BuiltinValueKind::EndAsyncLetLifetime:
28182820
return getEndAsyncLet(Context, Id);
28192821

28202822
case BuiltinValueKind::CreateTaskGroup:

0 commit comments

Comments
 (0)