Skip to content

Commit 22f3899

Browse files
committed
CABI: redefine reentrance rules in terms of component instance flag
1 parent 22a4da6 commit 22f3899

4 files changed

Lines changed: 230 additions & 269 deletions

File tree

design/mvp/Concurrency.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ emojis. For an even higher-level introduction, see [these][wasmio-2024]
1616
* [Thread Built-ins](#thread-built-ins)
1717
* [Thread-Local Storage](#thread-local-storage)
1818
* [Blocking](#blocking)
19+
* [Reentrance](#reentrance)
1920
* [Waitables and Waitable Sets](#waitables-and-waitable-sets)
2021
* [Streams and Futures](#streams-and-futures)
2122
* [Stream Readiness](#stream-readiness)
@@ -313,6 +314,7 @@ of the new subtask created for the import call. Thus, one reason for
313314
associating every thread with a "containing task" is to ensure that there is
314315
always a well-defined async call stack.
315316

317+
TODO: maybe move this to 'Reentrance' section
316318
A semantically-observable use of the async call stack is to distinguish between
317319
hazardous **recursive reentrance**, in which a component instance is reentered
318320
when one of its tasks is already on the callstack, from business-as-usual
@@ -503,6 +505,10 @@ once the reason for blocking is addressed.
503505
The [Canonical ABI explainer] defines the above behavior more precisely; search
504506
for `may_block` to see all the relevant points.
505507

508+
### Reentrance
509+
510+
TODO
511+
506512
### Waitables and Waitable Sets
507513

508514
When an `async`-typed function is called with the async ABI and the call
@@ -1278,7 +1284,7 @@ comes after:
12781284
* add an `async` effect on `resource` type definitions allowing a resource
12791285
type to block during its destructor
12801286
* `recursive` function type attribute: allow a function to opt in to
1281-
recursive [reentrance], extending the ABI to link the inner and
1287+
recursive [#reentrance], extending the ABI to link the inner and
12821288
outer activations
12831289
* add a `strict-callback` option that adds extra trapping conditions to
12841290
provide the semantic guarantees needed for engines to statically avoid
@@ -1356,7 +1362,6 @@ comes after:
13561362
[Binary Format]: Binary.md
13571363
[WIT]: WIT.md
13581364
[Blast Zone]: FutureFeatures.md#blast-zones
1359-
[Reentrance]: Explainer.md#component-invariants
13601365
[`start`]: Explainer.md#start-definitions
13611366

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

design/mvp/Explainer.md

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1735,10 +1735,10 @@ propagated once received.
17351735
If `waitable-set.wait` is called from a synchronous- or `async callback`-lifted
17361736
export, no other threads that were implicitly created by a separate
17371737
synchronous- or `async callback`-lifted export call can start or progress in
1738-
the current component instance until `waitable-set.wait` returns (thereby
1739-
ensuring non-reentrance of the core wasm code). However, explicitly-created
1740-
threads and threads implicitly created by non-`callback` `async`-lifted
1741-
("stackful async") exports may start or progress at any time.
1738+
the current component instance until `waitable-set.wait` returns (preserving
1739+
[component invariant] #2). However, explicitly-created threads and threads
1740+
implicitly created by non-`callback` `async`-lifted ("stackful async") exports
1741+
may start or progress at any time.
17421742

17431743
A `subtask` event notifies the supertask that its subtask is now in the given
17441744
state (the meanings of which are described by the [concurrency explainer]).
@@ -2201,12 +2201,12 @@ thread to be resumed (as with `thread.yield-to`). If `cancellable` is set,
22012201
`thread.yield` returns whether the current task was [cancelled] by the caller;
22022202
otherwise, `thread.yield` always returns `false`.
22032203

2204-
If `thread.yield` is called from a synchronous- or `async callback`-lifted
2205-
export, it returns immediately without blocking (instead of trapping, as with
2206-
other possibly-blocking operations like `waitable-set.wait`). This is because,
2207-
unlike other built-ins, `thread.yield` may be scattered liberally throughout
2208-
code that might show up in the transitive call tree of a synchronous function
2209-
call.
2204+
If `thread.yield` is called from a non-`async`-typed function that has not yet
2205+
returned a value, it returns immediately without blocking (instead of trapping,
2206+
as with other possibly-blocking operations like `waitable-set.wait`). This is
2207+
because, unlike other built-ins, `thread.yield` may be scattered liberally
2208+
throughout code that might show up in the transitive call tree of a synchronous
2209+
function call.
22102210

22112211
For details, see [Thread Built-ins] in the concurrency explainer and
22122212
[`canon_thread_yield`] in the Canonical ABI explainer.
@@ -2908,8 +2908,8 @@ definition. Thus, component functions form a "membrane" around the collection
29082908
of core module instances contained by a component instance, allowing the
29092909
Component Model to establish invariants that increase optimizability and
29102910
composability in ways not otherwise possible in the shared-everything setting
2911-
of Core WebAssembly. The Component Model proposes establishing the following
2912-
two runtime invariants:
2911+
of Core WebAssembly. The Component Model establishes the following runtime
2912+
invariants:
29132913
1. Components define a "lockdown" state that prevents continued execution
29142914
after a trap. This both prevents continued execution with corrupt state and
29152915
also allows more-aggressive compiler optimizations (e.g., store reordering).
@@ -2919,13 +2919,16 @@ two runtime invariants:
29192919
implicitly checked at every execution step by component functions. Thus,
29202920
after a trap, it's no longer possible to observe the internal state of a
29212921
component instance.
2922-
2. The Component Model disallows reentrance by trapping if a callee's
2923-
component-instance is already on the stack when the call starts.
2924-
(For details, see [`call_might_be_recursive`](CanonicalABI.md#component-instance-state)
2925-
in the Canonical ABI explainer.) This default prevents obscure
2926-
composition-time bugs and also enables more-efficient non-reentrant
2927-
runtime glue code. This rule will be relaxed by an opt-in
2928-
function type attribute in the [future](Concurrency.md#todo).
2922+
2. The Component Model's [reentrance] rules allow a producer toolchain to
2923+
implement both synchronous and `async` (🔀) exported functions using a
2924+
single, fixed-size shadow stack in linear memory that is pushed and popped in
2925+
LIFO order. (Internal use of cooperative threads (🧵) or the "stackful" ABI
2926+
option (🚟) may however require the use of multiple shadow stacks.)
2927+
3. Until the Component Model provides a mechanism for guest code to reliably
2928+
avoid recursive deadlocks (in particular, due to async backpressure),
2929+
recursive [reentrance] rules trap in cases which might otherwise lead to
2930+
recursive deadlock. (This limitation is intended to be relaxed in the
2931+
[future](Concurrency.md#todo).)
29292932

29302933

29312934
## JavaScript Embedding
@@ -3327,6 +3330,7 @@ For some use-case-focused, worked examples, see:
33273330
[Resolved]: Concurrency.md#cancellation
33283331
[Cancellation]: Concurrency.md#cancellation
33293332
[Cancelled]: Concurrency.md#cancellation
3333+
[Reentrance]: Concurrency.md#reentrance
33303334

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

design/mvp/canonical-abi/definitions.py

Lines changed: 32 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ class ComponentInstance:
188188
parent: Optional[ComponentInstance]
189189
handles: Table[ResourceHandle | Waitable | WaitableSet | ErrorContext]
190190
threads: Table[Thread]
191+
may_enter: bool
191192
may_leave: bool
192193
backpressure: int
193194
exclusive: Optional[Task]
@@ -199,6 +200,7 @@ def __init__(self, store, parent = None):
199200
self.parent = parent
200201
self.handles = Table()
201202
self.threads = Table()
203+
self.may_enter = True
202204
self.may_leave = True
203205
self.backpressure = 0
204206
self.exclusive = None
@@ -212,27 +214,19 @@ def reflexive_ancestors(self) -> set[ComponentInstance]:
212214
inst = inst.parent
213215
return s
214216

215-
def is_reflexive_ancestor_of(self, other):
216-
while other is not None:
217-
if self is other:
218-
return True
219-
other = other.parent
220-
return False
221-
222-
class Supertask:
223-
inst: Optional[ComponentInstance]
224-
supertask: Optional[Supertask]
225-
226-
def call_might_be_recursive(caller: Supertask, callee_inst: ComponentInstance):
227-
if caller.inst is None:
228-
while caller is not None:
229-
if caller.inst and caller.inst.reflexive_ancestors() & callee_inst.reflexive_ancestors():
230-
return True
231-
caller = caller.supertask
232-
return False
233-
else:
234-
return (caller.inst.is_reflexive_ancestor_of(callee_inst) or
235-
callee_inst.is_reflexive_ancestor_of(caller.inst))
217+
def flip_may_enter_to(self, new_value: bool, caller: Optional[ComponentInstance]):
218+
inst = self
219+
if caller is None:
220+
while inst is not None:
221+
assert(inst.may_enter != new_value)
222+
inst.may_enter = new_value
223+
inst = inst.parent
224+
else:
225+
already_entered = caller.reflexive_ancestors()
226+
while inst is not None and inst not in already_entered:
227+
trap_if(inst.may_enter == new_value)
228+
inst.may_enter = new_value
229+
inst = inst.parent
236230

237231
## Concurrency Primitives
238232

@@ -411,9 +405,9 @@ def yield_to(self, cancellable, other: Thread) -> Cancelled:
411405
OnStart = Callable[[], list[any]]
412406
OnResolve = Callable[[Optional[list[any]]], None]
413407
OnCancel = Callable[[], None]
414-
FuncInst = Callable[[Supertask, OnStart, OnResolve], OnCancel]
408+
FuncInst = Callable[[OnStart, OnResolve, Optional[ComponentInstance]], OnCancel]
415409

416-
class Task(Supertask):
410+
class Task:
417411
class State(Enum):
418412
INITIAL = 1
419413
STARTED = 2
@@ -425,19 +419,17 @@ class State(Enum):
425419
opts: CanonicalOptions
426420
inst: ComponentInstance
427421
ft: FuncType
428-
supertask: Supertask
429422
on_start: OnStart
430423
on_resolve: OnResolve
431424
num_borrows: int
432425
waiting_to_enter: Optional[Thread]
433426
threads: list[Thread]
434427

435-
def __init__(self, opts, inst, ft, supertask, on_start, on_resolve):
428+
def __init__(self, opts, inst, ft, on_start, on_resolve):
436429
self.state = Task.State.INITIAL
437430
self.opts = opts
438431
self.inst = inst
439432
self.ft = ft
440-
self.supertask = supertask
441433
self.on_start = on_start
442434
self.on_resolve = on_resolve
443435
self.num_borrows = 0
@@ -536,29 +528,34 @@ def cancel(self):
536528

537529
class Store:
538530
waiting: list[Thread]
531+
nesting_depth: int
539532

540533
def __init__(self):
541534
self.waiting = []
535+
self.nesting_depth = 0
542536

543537
def lift(self, callee, opts: CanonicalOptions, ft: FuncType, inst: ComponentInstance) -> FuncInst:
544-
def func_inst(caller: Supertask, on_start, on_resolve) -> OnCancel:
545-
trap_if(call_might_be_recursive(caller, inst))
546-
task = Task(opts, inst, ft, caller, on_start, on_resolve)
538+
def func_inst(on_start, on_resolve, caller) -> OnCancel:
539+
self.nesting_depth += 1
540+
inst.flip_may_enter_to(False, caller)
541+
task = Task(opts, inst, ft, on_start, on_resolve)
547542
Thread(task, lambda: canon_lift(callee)).resume()
543+
inst.flip_may_enter_to(True, caller)
544+
self.nesting_depth -= 1
548545
return task.request_cancellation
549546
return func_inst
550547

551-
def invoke(self, f: FuncInst, caller: Optional[Supertask], on_start, on_resolve) -> OnCancel:
552-
host_caller = Supertask()
553-
host_caller.inst = None
554-
host_caller.supertask = caller
555-
return f(host_caller, on_start, on_resolve)
548+
def invoke(self, f: FuncInst, on_start, on_resolve) -> OnCancel:
549+
return f(on_start, on_resolve, caller = None)
556550

557551
def tick(self):
552+
assert(self.nesting_depth == 0)
558553
random.shuffle(self.waiting)
559554
for thread in self.waiting:
560555
if thread.ready():
556+
thread.task.inst.flip_may_enter_to(False, caller = None)
561557
thread.resume()
558+
thread.task.inst.flip_may_enter_to(True, caller = None)
562559
return
563560

564561
## Lifting and Lowering Context
@@ -2186,7 +2183,7 @@ def on_resolve(result):
21862183
nonlocal flat_results
21872184
flat_results = lower_flat_values(cx, max_flat_results, result, ft.result_type(), flat_args)
21882185

2189-
subtask.on_cancel = callee(task, on_start, on_resolve)
2186+
subtask.on_cancel = callee(on_start, on_resolve, caller = inst)
21902187
assert(ft.async_ or subtask.state == Subtask.State.RETURNED)
21912188

21922189
if not opts.async_:

0 commit comments

Comments
 (0)