Skip to content

Commit 4769c40

Browse files
authored
Only allow 'async' ABI options for 'async'-typed function imports/exports (#646)
1 parent ba5afb2 commit 4769c40

7 files changed

Lines changed: 118 additions & 126 deletions

File tree

design/mvp/Binary.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,6 @@ label' ::= len:<u32> l:<label> => l (if len = |l|)
216216
valtype ::= i:<typeidx> => i
217217
| pvt:<primvaltype> => pvt
218218
resourcetype ::= 0x3f v:<valtype> f?:<core:funcidx>? => (resource (rep v) (dtor f)?)
219-
| 0x3e v:<valtype> f:<core:funcidx>
220-
cb?:<core:funcidx>? => (resource (rep v) (dtor async f (callback cb)?)) 🚝
221219
functype ::= 0x40 ps:<paramlist> rs:<resultlist> => (func ps rs)
222220
| 0x43 ps:<paramlist> rs:<resultlist> => (func async ps rs)
223221
paramlist ::= lt*:vec(<labelvaltype>) => (param lt)*

design/mvp/CanonicalABI.md

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -777,15 +777,22 @@ class Task(Supertask):
777777
self.threads = []
778778
```
779779

780-
The `Task.needs_exclusive` predicate returns whether the Canonical ABI options
781-
indicate that the core wasm being executed does not expect to be reentered
782-
(e.g., because the code is using a single global linear memory shadow stack).
783-
Concretely, this is assumed to be the case when core wasm is lifted
784-
synchronously or with `async callback`. This predicate is used by the other
785-
`Task` methods to determine whether to acquire/release the component instance's
786-
`exclusive` lock.
780+
The `Task.needs_exclusive` method returns whether an `async`-typed function's
781+
ABI options indicate that the Core WebAssembly code requires serialized
782+
execution (with the common reason being that there is a single, global linear
783+
memory shadow stack). This serialized execution is implemented by
784+
acquiring/releasing the component-instance-wide `exclusive` lock before/after
785+
executing Core WebAssembly code executing on the task's *implicit thread*
786+
(explicit threads created by `thread.new-indirect` ignore the `exclusive` lock).
787+
Specifically, sync- and stackless-async-lifted (`async callback`) functions
788+
require the `exclusive` lock and stackful-async-lifted (`async`) functions
789+
ignore the `exclusive` lock (just like explicit threads). Note that
790+
non-`async`-typed functions' implicit threads also ignore the `exclusive` lock
791+
since they must complete synchronously without blocking and thus don't have to
792+
worry about non-LIFO stack interleaving.
787793
```python
788794
def needs_exclusive(self):
795+
assert(self.ft.async_)
789796
return not self.opts.async_ or self.opts.callback
790797
```
791798

@@ -1283,14 +1290,10 @@ the `rt` field of `ResourceHandle` (above) and thus resource type equality is
12831290
class ResourceType(Type):
12841291
impl: ComponentInstance
12851292
dtor: Optional[Callable]
1286-
dtor_async: bool
1287-
dtor_callback: Optional[Callable]
12881293

1289-
def __init__(self, impl, dtor = None, dtor_async = False, dtor_callback = None):
1294+
def __init__(self, impl, dtor = None):
12901295
self.impl = impl
12911296
self.dtor = dtor
1292-
self.dtor_async = dtor_async
1293-
self.dtor_callback = dtor_callback
12941297
```
12951298

12961299

@@ -3470,7 +3473,9 @@ present, is validated as such:
34703473
* if `realloc` is present then `memory` must be present
34713474
* `post-return` - only allowed on [`canon lift`](#canon-lift), which has rules
34723475
for validation
3473-
* 🔀 `async` - cannot be present with `post-return`
3476+
* 🔀 `async` - is only allowed when used with an `async` function type in
3477+
[`canon lift`](#canon-lift) or [`canon lower`](#canon-lower) and cannot be
3478+
present with `post-return`
34743479
* 🔀,not(🚟) `async` - `callback` must also be present. Note that with the 🚟
34753480
feature (the "stackful" ABI), this restriction is lifted.
34763481
* 🔀 `callback` - the function has type `(func (param i32 i32 i32) (result i32))`
@@ -3598,7 +3603,7 @@ function (specified as a `funcidx` immediate in `canon lift`) until the
35983603
[packed] = call_and_trap_on_throw(callee, flat_args)
35993604
code,si = unpack_callback_result(packed)
36003605
while code != CallbackCode.EXIT:
3601-
assert(inst.exclusive is task)
3606+
assert(task.needs_exclusive() and inst.exclusive is task)
36023607
inst.exclusive = None
36033608
match code:
36043609
case CallbackCode.YIELD:
@@ -3921,22 +3926,22 @@ def canon_resource_drop(rt, i):
39213926
if rt.dtor:
39223927
rt.dtor(h.rep)
39233928
else:
3924-
caller_opts = CanonicalOptions(async_ = False)
3925-
callee_opts = CanonicalOptions(async_ = rt.dtor_async, callback = rt.dtor_callback)
3926-
ft = FuncType([U32Type()],[], async_ = False)
3929+
ft = FuncType([U32Type()], [], async_ = False)
39273930
dtor = rt.dtor or (lambda rep: [])
3928-
callee = inst.store.lift(dtor, ft, callee_opts, rt.impl)
3929-
caller = inst.store.lower(callee, ft, caller_opts, inst)
3931+
opts = CanonicalOptions(async_ = False)
3932+
callee = inst.store.lift(dtor, ft, opts, rt.impl)
3933+
caller = inst.store.lower(callee, ft, opts, inst)
39303934
caller([h.rep])
39313935
else:
39323936
h.borrow_scope.num_borrows -= 1
39333937
return []
39343938
```
3935-
The call to a resource's destructor is defined as a non-`async`-lowered,
3936-
non-`async`-typed function call to a possibly-`async`-lifted callee, passing
3937-
the private `i32` representation as a parameter. Thus, destructors *may* block
3938-
on I/O, but only after they `task.return`, ensuring that `resource.drop` never
3939-
blocks.
3939+
The call to a resource's destructor passes the `i32` representation value that
3940+
was previously supplied to `resource.new`. The call works like a normal
3941+
non-`async` cross-component call, using the same `canon_lift` and `canon_lower`
3942+
rules to, for example, catch reentrance. Because the type, lifting and
3943+
lowering are all non-`async`, the destructor may not block. However, the
3944+
destructor may spawn a cooperative thread that does.
39403945

39413946
Since there are valid reasons to call `resource.drop` in the same component
39423947
instance that defined the resource, which would otherwise trap at the

design/mvp/Concurrency.md

Lines changed: 37 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -74,16 +74,6 @@ the same way that they already bind to various OS's concurrent I/O APIs (such
7474
as `select`, `epoll`, `io_uring`, `kqueue` and Overlapped I/O) making the
7575
Component Model "just another OS" from the language toolchain's perspective.
7676

77-
The new async ABI can be used alongside or instead of the existing Preview 2
78-
"sync ABI" to call or implement *any* WIT function type. When *calling* an
79-
imported function via the async ABI, if the callee [blocks](#blocking), control
80-
flow is returned immediately to the caller, and the callee continues executing
81-
concurrently. When *implementing* an exported function via the async ABI,
82-
multiple concurrent export calls are allowed to be made by the caller.
83-
Critically, both sync-ABI-calls-async-ABI and async-ABI-calls-sync-ABI pairings
84-
have well-defined, composable behavior for both inter-component and
85-
intra-component calls.
86-
8777
In addition to adding a new async *ABI* for use by the language's compiler and
8878
runtime, the Component Model also adds a new `async` [effect type] that can be
8979
added to function types (in both WIT and raw component function type
@@ -103,6 +93,16 @@ invariant is necessary to allow non-`async` component exports to be called in
10393
synchronous contexts (like event listeners, callbacks, getters, setters and
10494
constructors).
10595

96+
The new async ABI can be used alongside or instead of the existing Preview 2
97+
"sync ABI" to call or implement any `async`-typed functions. When *calling* an
98+
imported function via the async ABI, if the `async` callee [blocks](#blocking),
99+
control flow is returned immediately to the caller, and the callee continues
100+
executing concurrently. When *implementing* an `async` function via the async
101+
ABI, multiple concurrent export calls are allowed to be made by the caller.
102+
Critically, both sync-ABI-calls-async-ABI and async-ABI-calls-sync-ABI pairings
103+
have well-defined, composable behavior for both inter-component and
104+
intra-component calls.
105+
106106
Because `async` function exports may be implemented with the *sync* ABI and
107107
then call `async` function imports using the *sync* ABI, traditional sync code
108108
can compile directly to components exporting `async` functions without having
@@ -685,30 +685,31 @@ the "started" state.
685685

686686
### Returning
687687

688-
The way an async export call returns its value is by calling [`task.return`],
689-
passing the core values that are to be lifted as *parameters*.
688+
The way an `async` export returns its value using the async ABI is by calling
689+
[`task.return`], passing the core values that are to be lifted as *parameters*.
690+
When using the async ABI, *any* of the threads contained by a task can call
691+
`task.return`; there is no "main thread" of a task. When the last thread of a
692+
task returns, there is a trap if `task.return` has not been called. Thus, *some*
693+
thread (either the thread created implicitly for the initial export call or some
694+
thread transitively created by that thread) must call `task.return`.
690695

691696
Returning values by calling `task.return` allows a task to continue executing
692-
even after it has passed its initial results to the caller. This can be useful
693-
for various finalization tasks (freeing memory or performing logging, billing
694-
or metrics operations) that don't need to be on the critical path of returning
695-
a value to the caller, but the major use of executing code after `task.return`
696-
is to continue to read and write from streams and futures. For example, a
697-
stream transformer function of type `func(in: stream<T>) -> stream<U>` will
698-
immediately `task.return` a stream created via `stream.new` and then sit in a
699-
loop interleaving `stream.read`s (of the readable end passed for `in`) and
700-
`stream.write`s (of the writable end it `stream.new`ed) before exiting the
701-
task.
702-
703-
*Any* of the threads contained by a task can call `task.return`; there is no
704-
"main thread" of a task. When the last thread of a task returns, there is a
705-
trap if `task.return` has not been called. Thus, *some* thread (either the
706-
thread created implicitly for the initial export call or some thread
707-
transitively created by that thread) must call `task.return`.
697+
even after it has passed its initial results to the caller. This is also
698+
possible even with the sync ABI by using cooperative threads. Continuing
699+
to execute after returning a value can be useful for various finalization tasks
700+
(freeing memory or performing logging, billing or metrics operations) that don't
701+
need to be on the critical path of returning a value to the caller, but the
702+
major use of executing code after `task.return` is to continue to read and write
703+
from streams and futures. For example, a stream transformer function of type
704+
`func(in: stream<T>) -> stream<U>` will immediately `task.return` a stream
705+
created via `stream.new` and then sit in a loop interleaving `stream.read`s (of
706+
the readable end passed for `in`) and `stream.write`s (of the writable end it
707+
`stream.new`ed) before exiting the task.
708708

709709
Once `task.return` is called, the task is in the "returned" state. Calling
710-
`task.return` when not in the "started" state traps. Once in a "returned"
711-
state, non-`async` functions are allowed to block.
710+
`task.return` when not in the "started" state traps. Once in a "returned" state,
711+
non-`async` functions may block using cooperative threads that were created
712+
before the synchronous task's implicit thread returned.
712713

713714
### Borrows
714715

@@ -873,19 +874,12 @@ JS [top-level `await`] or I/O in C++ constructors executing during `start`.
873874

874875
## Async ABI
875876

876-
At an ABI level, native async in the Component Model defines for every WIT
877-
function an async-oriented core function signature that can be used instead of
878-
or in addition to the existing (Preview-2-defined) synchronous core function
879-
signature. This async-oriented core function signature is intended to be called
880-
or implemented by generated bindings which then map the low-level core async
881-
protocol to the languages' higher-level native concurrency features.
882-
883-
Note that *every* WIT-level function type can be lifted and lowered using the
884-
async (or sync) ABI. While calling a non-`async`-typed function import using
885-
the async ABI will never returned that the call "blocked" (as guaranteed by the
886-
Component Model trapping if the callee would have blocked), the async ABI is
887-
still allowed to be used (for the benefit of code generators that only want
888-
to think about one ABI).
877+
At an ABI level, native async in the Component Model defines for every
878+
`async`-typed function a non-blocking core function signature that can be
879+
used instead of or in addition to the existing (Preview-2-defined) synchronous
880+
core function signature. This non-blocking core function signature is intended
881+
to be called or implemented by generated bindings which then map the low-level
882+
core async protocol to the languages' higher-level native concurrency features.
889883

890884
### Async Import ABI
891885

design/mvp/Explainer.md

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -573,9 +573,7 @@ valtype ::= <typeidx>
573573
| <defvaltype>
574574
keytype ::= bool | s8 | u8 | s16 | u16 | s32 | u32 | s64 | u64 | char | string 🗺️
575575
resourcetype ::= (resource (rep i32) (dtor <core:funcidx>)?)
576-
| (resource (rep i32) (dtor async <core:funcidx> (callback <core:funcidx>)?)?) 🚝
577576
| (resource (rep i64) (dtor <core:funcidx>)?) 🐘
578-
| (resource (rep i64) (dtor async <core:funcidx> (callback <core:funcidx>)?)?) 🚝🐘
579577
functype ::= (func async? (param "<label>" <valtype>)* (result <valtype>)?)
580578
componenttype ::= (component <componentdecl>*)
581579
instancetype ::= (instance <instancedecl>*)
@@ -828,9 +826,8 @@ is currently fixed to `i32` or `i64`, but will potentially be relaxed to include
828826
other types. When the last handle to a resource is dropped, the resource's
829827
destructor function specified by the `dtor` immediate will be called (if
830828
present), allowing the implementing component to perform clean-up like freeing
831-
linear memory allocations. Destructors can be declared `async`, with the same
832-
meaning for the `async` and `callback` immediates as described below for `canon
833-
lift`. A destructor for a `resource (rep $T)` must have type `($T) -> ()`.
829+
linear memory allocations. A destructor for a `resource (rep $T)` must have type
830+
`($T) -> ()`.
834831

835832
The `instance` type constructor describes a list of named, typed definitions
836833
that can be imported or exported by a component. Informally, instance types
@@ -1342,13 +1339,10 @@ be deallocated and destructors called. This immediate is always optional but,
13421339
if present, is validated to have parameters matching the callee's return type
13431340
and empty results.
13441341

1345-
🔀 The `async` option specifies that the component wants to make (for imports)
1346-
or support (for exports) multiple concurrent (asynchronous) calls. This option
1347-
can be applied to any component-level function type and changes the derived
1348-
Canonical ABI significantly. See the [concurrency explainer] for more details.
1349-
When a function signature contains a `future` or `stream`, validation of `canon
1350-
lower` requires the `async` option to be set (since a synchronous call to a
1351-
function using these types is highly likely to deadlock).
1342+
🔀 The `async` option may only be used with `async` function types and specifies
1343+
that the component wants to make (for imports) or support (for exports) multiple
1344+
concurrent (asynchronous) calls. This option changes the derived Canonical ABI
1345+
significantly; see the [concurrency explainer] for more details.
13521346

13531347
🔀 The `(callback ...)` option may only be present in `canon lift` when the
13541348
`async` option has also been set and specifies a core function that is

design/mvp/canonical-abi/definitions.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,7 @@ def __init__(self, ft, opts, inst, on_start, on_resolve, supertask):
454454
self.threads = []
455455

456456
def needs_exclusive(self):
457+
assert(self.ft.async_)
457458
return not self.opts.async_ or self.opts.callback
458459

459460
def may_block(self):
@@ -690,14 +691,10 @@ def __init__(self, rt, rep, own, borrow_scope = None):
690691
class ResourceType(Type):
691692
impl: ComponentInstance
692693
dtor: Optional[Callable]
693-
dtor_async: bool
694-
dtor_callback: Optional[Callable]
695694

696-
def __init__(self, impl, dtor = None, dtor_async = False, dtor_callback = None):
695+
def __init__(self, impl, dtor = None):
697696
self.impl = impl
698697
self.dtor = dtor
699-
self.dtor_async = dtor_async
700-
self.dtor_callback = dtor_callback
701698

702699
### Waitable State
703700

@@ -2122,7 +2119,7 @@ def thread_func():
21222119
[packed] = call_and_trap_on_throw(callee, flat_args)
21232120
code,si = unpack_callback_result(packed)
21242121
while code != CallbackCode.EXIT:
2125-
assert(inst.exclusive is task)
2122+
assert(task.needs_exclusive() and inst.exclusive is task)
21262123
inst.exclusive = None
21272124
match code:
21282125
case CallbackCode.YIELD:
@@ -2266,12 +2263,11 @@ def canon_resource_drop(rt, i):
22662263
if rt.dtor:
22672264
rt.dtor(h.rep)
22682265
else:
2269-
caller_opts = CanonicalOptions(async_ = False)
2270-
callee_opts = CanonicalOptions(async_ = rt.dtor_async, callback = rt.dtor_callback)
2271-
ft = FuncType([U32Type()],[], async_ = False)
2266+
ft = FuncType([U32Type()], [], async_ = False)
22722267
dtor = rt.dtor or (lambda rep: [])
2273-
callee = inst.store.lift(dtor, ft, callee_opts, rt.impl)
2274-
caller = inst.store.lower(callee, ft, caller_opts, inst)
2268+
opts = CanonicalOptions(async_ = False)
2269+
callee = inst.store.lift(dtor, ft, opts, rt.impl)
2270+
caller = inst.store.lower(callee, ft, opts, inst)
22752271
caller([h.rep])
22762272
else:
22772273
h.borrow_scope.num_borrows -= 1

0 commit comments

Comments
 (0)