Skip to content

Commit 4de9c9a

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

4 files changed

Lines changed: 297 additions & 323 deletions

File tree

design/mvp/Concurrency.md

Lines changed: 42 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)
@@ -56,8 +54,8 @@ concurrency-specific goals and use cases:
5654
* Allow polyfilling in browsers via JavaScript Promise Integration ([JSPI])
5755
* Avoid partitioning interfaces and components into separate ecosystems based
5856
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).
57+
* Allow runtimes to maintain meaningful cross-language call stacks (for the
58+
benefit of debugging, logging, tracing and profiling).
6159
* Consider backpressure and cancellation as part of the design.
6260
* Allow non-reentrant synchronous and event-loop-driven core wasm code that
6361
assumes a single global linear memory stack to not have to worry about
@@ -120,13 +118,14 @@ language's style of concurrency, most `world`s (including `wasi:cli/command`,
120118
functions so that the contained Core WebAssembly code is free to block.
121119
Implementing a non-`async` function will primarily only arise when a component
122120
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.
121+
setters of `wasi:http/types.headers`). In this more exotic virtualization
122+
scenario, a [future extension](#TODO) could allow a parent component that
123+
imports `async` functions to implement its child's non-`async` imports in the
124+
same manner as [JSPI] in the browser.
125+
126+
Thus, overall, `async` in WIT and the Component Model does not behave like a
127+
"color" in the sense described by the popular [What Color Is Your Function?]
128+
essay.
130129

131130
Each time a component export is called, the wasm runtime logically spawns a new
132131
[green thread] (as opposed to a [kernel thread]) to execute the export call
@@ -190,18 +189,23 @@ immediately block.
190189
This backpressure mechanism provides the basis for how the sync and async ABIs
191190
interoperate:
192191
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.
192+
implemented by a component using the sync ABI, the callee first acquires
193+
an "exclusive" on the component instance, and then starts executing. If
194+
the callee blocks, execution is immediately transferred back to the caller
195+
(as required by the async ABI).
196+
2. If another async call attempts to start in this same component instance, the
197+
callee immediately block when acquiring the "exclusive" lock, waiting for the
198+
previous call to return and release the lock.
199199

200200
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`
201+
block, non-`async` functions do not attempt to acquire the "exclusive" lock;
202+
they just barge in. Components exporting a mix of `async` and non-`async`
203203
functions (which again mostly only arises in the more advanced virtualization
204-
scenarios) must thus take care to handle non-`async` reentrance gracefully.
204+
scenarios) must therefore take care to handle the "barges in" case gracefully.
205+
Because this nested non-`async` call will complete synchronously without
206+
blocking, this behavior does not break [component invariant] #3 since a single
207+
global shadow stack can still be (re)used in a LIFO manner to implement the
208+
nested synchronous call in much the same manner as a traditional signal handler.
205209

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

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.
321+
The async call stack is not currently semantically observable to the running
322+
components other than possibly, nondeterministically, as part of the callstack
323+
stored in 📝 `error-context`. But this async call stack is also useful for
324+
other purposes such as providing backtraces when debugging, profiling and
325+
tracing. While particular languages can and do maintain their own async call
326+
stacks in core wasm state, without the Component Model's async call stack,
327+
linkage *between* different languages would be lost at component boundaries,
328+
leading to a loss of overall context in multi-component applications.
331329

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

491489
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.
490+
suspended and execution will transfer to a caller's thread, if there is one, or
491+
if not, the runtime, which may invoke new component exports or
492+
nondeterministically select a cooperative thread that is ready to run and resume
493+
it. Thus, each of these represents **cooperative yield points**.
494+
493495
Additionally, each of these potentially-blocking operations will trap if the
494496
[current task's function type](#current-thread-and-task) does not declare the
495497
`async` effect, since only `async`-typed functions are allowed to block. As an
@@ -1379,9 +1381,9 @@ comes after:
13791381
type to block during instantiation
13801382
* add an `async` effect on `resource` type definitions allowing a resource
13811383
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
1384+
* allow a parent component to perform [JSPI]-like suspension of the sync calls
1385+
of its child components, thereby allowing the parent to implement the child's
1386+
sync imports calls in terms of the parent's `async` imports.
13851387
* add a `strict-callback` option that adds extra trapping conditions to
13861388
provide the semantic guarantees needed for engines to statically avoid
13871389
fiber creation at component-to-component `async` call boundaries
@@ -1391,9 +1393,6 @@ comes after:
13911393
`shared`
13921394

13931395

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

14271426
[AST Explainer]: Explainer.md
14281427
[Canonical Built-in]: Explainer.md#canonical-built-ins
1428+
[Component Invariant]: Explainer.md#component-invariant
14291429
[`context.get`]: Explainer.md#-contextget
14301430
[`context.set`]: Explainer.md#-contextset
14311431
[`backpressure.inc`]: Explainer.md#-backpressureinc-and-backpressuredec
@@ -1463,7 +1463,6 @@ comes after:
14631463
[Binary Format]: Binary.md
14641464
[WIT]: WIT.md
14651465
[Blast Zone]: FutureFeatures.md#blast-zones
1466-
[Reentrance]: Explainer.md#component-invariants
14671466
[`start`]: Explainer.md#start-definitions
14681467

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

design/mvp/Explainer.md

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2893,14 +2893,17 @@ start being rejected some time after after [WASI Preview 3] is released.
28932893

28942894
## Component Invariants
28952895

2896-
As a consequence of the shared-nothing design described above, all calls into
2897-
or out of a component instance necessarily transit through a component function
2898-
definition. Thus, component functions form a "membrane" around the collection
2899-
of core module instances contained by a component instance, allowing the
2900-
Component Model to establish invariants that increase optimizability and
2901-
composability in ways not otherwise possible in the shared-everything setting
2902-
of Core WebAssembly. The Component Model proposes establishing the following
2903-
two runtime invariants:
2896+
Component validation rules only allow a component to import and export
2897+
component-level functions, not Core WebAssembly functions. Because component-
2898+
level functions can only be produced or consumed by Canonical ABI [`lift` and
2899+
`lower` definitions](#canonical-definitions), which essentially define
2900+
[trampolines] executed at runtime on all incoming and outgoing call paths, the
2901+
Component Model can define and enforce (at runtime, with traps) invariants that
2902+
component authors can depend on. This is analogous to the invariants provided by
2903+
a traditional Operating System to user-space code running inside a process.
2904+
2905+
In particular, the Component Model maintains the following invariants:
2906+
29042907
1. Components define a "lockdown" state that prevents continued execution
29052908
after a trap. This both prevents continued execution with corrupt state and
29062909
also allows more-aggressive compiler optimizations (e.g., store reordering).
@@ -2910,13 +2913,25 @@ two runtime invariants:
29102913
implicitly checked at every execution step by component functions. Thus,
29112914
after a trap, it's no longer possible to observe the internal state of a
29122915
component instance.
2913-
2. The Component Model disallows reentrance by trapping if a callee's
2914-
component-instance is already on the stack when the call starts.
2915-
(For details, see [`call_might_be_recursive`](CanonicalABI.md#component-instances)
2916-
in the Canonical ABI explainer.) This default prevents obscure
2917-
composition-time bugs and also enables more-efficient non-reentrant
2918-
runtime glue code. This rule will be relaxed by an opt-in
2919-
function type attribute in the [future](Concurrency.md#todo).
2916+
2917+
2. Once a component instance starts executing core wasm code, that component
2918+
instance cannot be [reentered] via export call or resumption of a separate
2919+
core wasm cooperative thread until *either* execution returns from the base
2920+
of the core wasm call stack *or* a core wasm import is called that the
2921+
Canonical ABI has defined to be a [cooperative yield point]. In particular,
2922+
calls to synchronously-typed imports as well as asynchronous/non-awaiting
2923+
calls to `async`-typed imports are *not* cooperative yield points and so
2924+
generic bindings generators and component authors are *not* forced to safely
2925+
handle reentrance at *every single* import callsite.
2926+
2927+
3. Furthermore (extending 2), even when synchronously calling a cooperatively-
2928+
yielding core wasm import (such as a synchronous/awaiting call to an
2929+
`async`-typed import or `waitable-set.wait`), unless a component opts in (via
2930+
"stackful lift" 🚟 or cooperative threads 🧵), core wasm execution inside the
2931+
component instance is locally *serialized* (via backpressure) so that
2932+
producer toolchains can continue to use a single global linear memory shadow
2933+
stack that is pushed and popped in LIFO order while achieving intra-component
2934+
[event-loop] concurrency via `callback`.
29202935

29212936

29222937
## JavaScript Embedding
@@ -3234,6 +3249,9 @@ For some use-case-focused, worked examples, see:
32343249
[Universal Types]: https://en.wikipedia.org/wiki/System_F
32353250
[Existential Types]: https://en.wikipedia.org/wiki/System_F
32363251
[Unit]: https://en.wikipedia.org/wiki/Unit_type
3252+
[Trampolines]: https://en.wikipedia.org/wiki/Trampoline_(computing)
3253+
[Reentered]: https://en.wikipedia.org/wiki/Reentrancy_(computing)
3254+
[Event-loop]: https://en.wikipedia.org/wiki/Event_loop
32373255

32383256
[Generative]: https://www.researchgate.net/publication/2426300_A_Syntactic_Theory_of_Type_Generativity_and_Sharing
32393257
[Avoidance Problem]: https://counterexamples.org/avoidance.html
@@ -3318,6 +3336,7 @@ For some use-case-focused, worked examples, see:
33183336
[Resolved]: Concurrency.md#cancellation
33193337
[Cancellation]: Concurrency.md#cancellation
33203338
[Cancelled]: Concurrency.md#cancellation
3339+
[Cooperative Yield Point]: Concurrency.md#blocking
33213340

33223341
[Component Model Documentation]: https://component-model.bytecodealliance.org
33233342
[`wizer`]: https://github.com/bytecodealliance/wizer

0 commit comments

Comments
 (0)