Skip to content

Commit aa11b56

Browse files
committed
CABI: redefine reentrance rules in terms of component instance flag
1 parent 91a165e commit aa11b56

5 files changed

Lines changed: 507 additions & 334 deletions

File tree

design/mvp/CanonicalABI.md

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ class ComponentInstance:
123123
parent: Optional[ComponentInstance]
124124
handles: Table[ResourceHandle | Waitable | WaitableSet | ErrorContext]
125125
threads: Table[Thread]
126+
may_enter: bool
126127
may_leave: bool
127128
backpressure: int
128129
num_waiting_to_enter: int
@@ -134,6 +135,7 @@ class ComponentInstance:
134135
self.parent = parent
135136
self.handles = Table()
136137
self.threads = Table()
138+
self.may_enter = True
137139
self.may_leave = True
138140
self.backpressure = 0
139141
self.num_waiting_to_enter = 0
@@ -155,26 +157,37 @@ by the host, `None`, in the `parent` field. Thus, the set of component instances
155157
in a store forms a forest rooted by the component instances that were
156158
instantiated directly by the host.
157159

158-
Based on this, the "reflexive ancestors" of a component instance (i.e., itself
159-
and all parent component instances up to the root component instance) can be
160-
enumerated and tested via these two helper functions:
160+
TODO
161161
```python
162-
def reflexive_ancestors(self) -> set[ComponentInstance]:
162+
def enter_from(self, caller: Optional[ComponentInstance]):
163+
for inst in self.entering(caller):
164+
trap_if(not inst.may_enter)
165+
inst.may_enter = False
166+
167+
def leave_to(self, caller: Optional[ComponentInstance]):
168+
for inst in self.entering(caller):
169+
assert(not inst.may_enter)
170+
inst.may_enter = True
171+
172+
def entering(self, caller: Optional[ComponentInstance]):
173+
if caller:
174+
return self.self_and_ancestors() - caller.self_and_ancestors()
175+
else:
176+
return self.self_and_ancestors()
177+
178+
def self_and_ancestors(self) -> set[ComponentInstance]:
163179
s = set()
164180
inst = self
165181
while inst is not None:
166182
s.add(inst)
167183
inst = inst.parent
168184
return s
169-
170-
def is_reflexive_ancestor_of(self, other):
171-
while other is not None:
172-
if self is other:
173-
return True
174-
other = other.parent
175-
return False
176185
```
177186

187+
Based on this, the "reflexive ancestors" of a component instance (i.e., itself
188+
and all parent component instances up to the root component instance) can be
189+
enumerated and tested via these two helper functions:
190+
...
178191
How the host instantiates and invokes root components is up to the host and not
179192
specified by the Component Model. Exports of previously-instantiated root
180193
components *may* be supplied as the imports of subsequently-instantiated root

design/mvp/Concurrency.md

Lines changed: 64 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33
This document contains a high-level summary of the native concurrency support
44
added as part of [WASI Preview 3], providing background for understanding the
55
definitions in the [WIT], [AST explainer], [binary format] and [Canonical ABI
6-
explainer] documents that are gated by the 🔀 (async) and 🧵 (threading)
7-
emojis. For an even higher-level introduction, see [these][wasmio-2024]
8-
[presentations][wasmio-2025].
6+
explainer] documents that are gated by the 🔀 (async) and 🧵 (threading) emojis.
97

108
* [Goals](#goals)
119
* [Summary](#summary)
@@ -16,6 +14,7 @@ emojis. For an even higher-level introduction, see [these][wasmio-2024]
1614
* [Thread Built-ins](#thread-built-ins)
1715
* [Thread-Local Storage](#thread-local-storage)
1816
* [Blocking](#blocking)
17+
* [Reentrance](#reentrance)
1918
* [Waitables and Waitable Sets](#waitables-and-waitable-sets)
2019
* [Streams and Futures](#streams-and-futures)
2120
* [Stream Readiness](#stream-readiness)
@@ -56,8 +55,8 @@ concurrency-specific goals and use cases:
5655
* Allow polyfilling in browsers via JavaScript Promise Integration ([JSPI])
5756
* Avoid partitioning interfaces and components into separate ecosystems based
5857
on degree of concurrency; don't give components a "[color]".
59-
* Maintain meaningful cross-language call stacks (for the benefit of debugging,
60-
logging and tracing).
58+
* Allow runtimes to maintain meaningful cross-language call stacks (for the
59+
benefit of debugging, logging, tracing and profiling).
6160
* Consider backpressure and cancellation as part of the design.
6261
* Allow non-reentrant synchronous and event-loop-driven core wasm code that
6362
assumes a single global linear memory stack to not have to worry about
@@ -120,13 +119,14 @@ language's style of concurrency, most `world`s (including `wasi:cli/command`,
120119
functions so that the contained Core WebAssembly code is free to block.
121120
Implementing a non-`async` function will primarily only arise when a component
122121
is *virtualizing* the non-`async` *imports* of a `world` (e.g., the getters and
123-
setters of `wasi:http/types.headers`). In this virtualization scenario (once
124-
functions are allowed to be [recursive](#TODO)), the Canonical ABI and/or Core
125-
WebAssembly [stack-switching] proposal will allow a parent component to
126-
implement a child's non-`async` imports in terms of the parent's `async`
127-
imports in the same manner as [JSPI]. Thus, overall, `async` in WIT and the
128-
Component Model does not behave like a "color" in the sense described by the
129-
popular [What Color Is Your Function?] essay.
122+
setters of `wasi:http/types.headers`). In this more exotic virtualization
123+
scenario, a [future extension](#TODO) could allow a parent component that
124+
imports `async` functions to implement its child's non-`async` imports in the
125+
same manner as [JSPI] in the browser.
126+
127+
Thus, overall, `async` in WIT and the Component Model does not behave like a
128+
"color" in the sense described by the popular [What Color Is Your Function?]
129+
essay.
130130

131131
Each time a component export is called, the wasm runtime logically spawns a new
132132
[green thread] (as opposed to a [kernel thread]) to execute the export call
@@ -190,18 +190,23 @@ immediately block.
190190
This backpressure mechanism provides the basis for how the sync and async ABIs
191191
interoperate:
192192
1. If a component calls an import using the async ABI, and the import is
193-
implemented by a component using the sync ABI, and the callee blocks,
194-
execution is immediately transferred back to the caller (as required by the
195-
async ABI) and the callee's component instance is marked "suspended".
196-
2. If another async call attempts to start in a "suspended" component instance,
197-
the Component Model automatically makes the call block, the same way as when
198-
backpressure is active.
193+
implemented by a component using the sync ABI, the callee first acquires
194+
an "exclusive" on the component instance, and then starts executing. If
195+
the callee blocks, execution is immediately transferred back to the caller
196+
(as required by the async ABI).
197+
2. If another async call attempts to start in this same component instance, the
198+
callee immediately block when acquiring the "exclusive" lock, waiting for the
199+
previous call to return and release the lock.
199200

200201
Note that because functions without `async` in their type are not allowed to
201-
block, non-`async` functions do not check for backpressure or suspension; they
202-
always run synchronously. Components exporting a mix of `async` and non-`async`
202+
block, non-`async` functions do not attempt to acquire the "exclusive" lock;
203+
they just barge in. Components exporting a mix of `async` and non-`async`
203204
functions (which again mostly only arises in the more advanced virtualization
204-
scenarios) must thus take care to handle non-`async` reentrance gracefully.
205+
scenarios) must therefore take care to handle the "barges in" case gracefully.
206+
Because this nested non-`async` call will complete synchronously without
207+
blocking, this behavior does not break [component invariant] #3 since a single
208+
global shadow stack can still be (re)used in a LIFO manner to implement the
209+
nested synchronous call in much the same manner as a traditional signal handler.
205210

206211
Lastly, WIT is extended with two new type constructors—`future<T>` and
207212
`stream<T>`—to allow new WIT interfaces to explicitly represent concurrency in
@@ -314,20 +319,14 @@ of the new subtask created for the import call. Thus, one reason for
314319
associating every thread with a "containing task" is to ensure that there is
315320
always a well-defined async call stack.
316321

317-
A semantically-observable use of the async call stack is to distinguish between
318-
hazardous **recursive reentrance**, in which a component instance is reentered
319-
when one of its tasks is already on the callstack, from business-as-usual
320-
**sibling reentrance**, in which a component instance is reentered for the
321-
first time on a particular async call stack. Recursive reentrance currently
322-
always traps, but will be allowed (and indicated to core wasm) in an opt-in
323-
manner in the [future](#TODO).
324-
325-
The async call stack is also useful for non-semantic purposes such as providing
326-
backtraces when debugging, profiling and tracing. While particular languages
327-
can and do maintain their own async call stacks in core wasm state, without the
328-
Component Model's async call stack, linkage *between* different languages would
329-
be lost at component boundaries, leading to a loss of overall context in
330-
multi-component applications.
322+
The async call stack is not currently semantically observable to the running
323+
components other than possibly, nondeterministically, as part of the callstack
324+
stored in 📝 `error-context`. But this async call stack is also useful for
325+
other purposes such as providing backtraces when debugging, profiling and
326+
tracing. While particular languages can and do maintain their own async call
327+
stacks in core wasm state, without the Component Model's async call stack,
328+
linkage *between* different languages would be lost at component boundaries,
329+
leading to a loss of overall context in multi-component applications.
331330

332331
There is an important gap between the Component Model's minimal form of
333332
Structured Concurrency and the Structured Concurrency support that appears in
@@ -489,7 +488,11 @@ all of which are described above or below in more detail:
489488
[`subtask.cancel`](#cancellation) built-in
490489

491490
At each of these points, the [current thread](#current-thread-and-task) will be
492-
suspended and execution will transfer to a caller's thread, if there is one.
491+
suspended and execution will transfer to a caller's thread, if there is one, or
492+
if not, the runtime, which may invoke new component exports or
493+
nondeterministically select a cooperative thread that is ready to run and resume
494+
it. Thus, each of these represents **cooperative yield points**.
495+
493496
Additionally, each of these potentially-blocking operations will trap if the
494497
[current task's function type](#current-thread-and-task) does not declare the
495498
`async` effect, since only `async`-typed functions are allowed to block. As an
@@ -504,6 +507,24 @@ once the reason for blocking is addressed.
504507
The [Canonical ABI explainer] defines the above behavior more precisely; search
505508
for `may_block` to see all the relevant points.
506509

510+
### Reentrance
511+
512+
TODO:
513+
* mention Component Invariant #2
514+
* blocking specifically intended to allow reentrance
515+
* thus, define "reentrance" to only blocked synchronous
516+
* sync-typed, or async-lowered async-typed
517+
* sync-lowered async-typed = blocking
518+
* why useful
519+
* risk: async recursion => deadlock
520+
* specific example: backpressure (below)
521+
* lots of other possibilities
522+
* so general rule: don't create async recursion dependency
523+
* only possible via host or in guest via [donut wrapping]
524+
* no well-defined way to rule out and trap; async call stack promising,
525+
only captures "causality", not "dependence" (good for observability,
526+
bad for catching recursion without false positives)
527+
507528
### Waitables and Waitable Sets
508529

509530
When an `async`-typed function is called with the async ABI and the call
@@ -662,6 +683,7 @@ without calling the component's Core WebAssembly code. By using a counter
662683
instead of a boolean flag, unrelated pieces of code can report backpressure for
663684
distinct limited resources without prior coordination.
664685

686+
TODO: mention component invariant #3 again
665687
In addition to *explicit* backpressure set by wasm code, there is also an
666688
*implicit* source of backpressure used to protect non-reentrant core wasm code.
667689
In particular, when an export uses the sync ABI or the stackless async ABI, a
@@ -1379,21 +1401,20 @@ comes after:
13791401
type to block during instantiation
13801402
* add an `async` effect on `resource` type definitions allowing a resource
13811403
type to block during its destructor
1382-
* `recursive` function type attribute: allow a function to opt in to
1383-
recursive [reentrance], extending the ABI to link the inner and
1384-
outer activations
1404+
* allow a parent component to perform [JSPI]-like suspension of the sync calls
1405+
of its child components, thereby allowing the parent to implement the child's
1406+
sync imports calls in terms of the parent's `async` imports.
13851407
* add a `strict-callback` option that adds extra trapping conditions to
13861408
provide the semantic guarantees needed for engines to statically avoid
13871409
fiber creation at component-to-component `async` call boundaries
1410+
* allow function closures to be passed as first-class values, supporting the
1411+
the "callback" pattern in many pre-existing APIs, including Web APIs
13881412
* allow pipelining multiple `stream.read`/`write` calls
13891413
* allow chaining multiple async calls together ("promise pipelining")
13901414
* integrate with `shared`: define how to lift and lower functions `async` *and*
13911415
`shared`
13921416

13931417

1394-
[wasmio-2024]: https://www.youtube.com/watch?v=y3x4-nQeXxc
1395-
[wasmio-2025]: https://www.youtube.com/watch?v=mkkYNw8gTQg
1396-
13971418
[Color]: https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/
13981419
[What Color Is Your Function?]: https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/
13991420
[Weak Memory Model]: https://people.mpi-sws.org/~rossberg/papers/Watt,%20Rossberg,%20Pichon-Pharabod%20-%20Weakening%20WebAssembly%20[Extended].pdf
@@ -1426,6 +1447,7 @@ comes after:
14261447

14271448
[AST Explainer]: Explainer.md
14281449
[Canonical Built-in]: Explainer.md#canonical-built-ins
1450+
[Component Invariant]: Explainer.md#component-invariant
14291451
[`context.get`]: Explainer.md#-contextget
14301452
[`context.set`]: Explainer.md#-contextset
14311453
[`backpressure.inc`]: Explainer.md#-backpressureinc-and-backpressuredec
@@ -1463,7 +1485,6 @@ comes after:
14631485
[Binary Format]: Binary.md
14641486
[WIT]: WIT.md
14651487
[Blast Zone]: FutureFeatures.md#blast-zones
1466-
[Reentrance]: Explainer.md#component-invariants
14671488
[`start`]: Explainer.md#start-definitions
14681489

14691490
[Store]: https://webassembly.github.io/spec/core/exec/runtime.html#syntax-store

0 commit comments

Comments
 (0)