Skip to content

Commit 1bbf0e2

Browse files
committed
Tidy CanonicalABI.md
1 parent 6c5537d commit 1bbf0e2

File tree

2 files changed

+116
-115
lines changed

2 files changed

+116
-115
lines changed

design/mvp/CanonicalABI.md

Lines changed: 99 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -134,16 +134,16 @@ class Store:
134134
thread.resume()
135135
return
136136
```
137-
The `tick` method does not have an analogue in Core WebAssembly and enables
138-
[native async](Async.md) support in the Component Model. The expectation is
139-
that the host will interleave calls to `invoke` with calls to `tick`,
140-
repeatedly calling `tick` until there is no more work to do or the store is
141-
destroyed. The nondeterministic `random.shuffle` indicates that the embedder is
142-
allowed to use any algorithm (involving priorities, fairness, etc) to choose
143-
which thread to schedule next (and hopefully an algorithm more efficient than
144-
the simple polling loop written above). The `Thread.ready` and `Thread.resume`
145-
methods along with how the `pending` list is populated are all defined
146-
[below](#thread-state) as part of the `Thread` class.
137+
The `Store.tick` method does not have an analogue in Core WebAssembly and
138+
enables [native async support](Async.md) in the Component Model. The
139+
expectation is that the host will interleave calls to `invoke` with calls to
140+
`tick`, repeatedly calling `tick` until there is no more work to do or the
141+
store is destroyed. The nondeterministic `random.shuffle` indicates that the
142+
embedder is allowed to use any algorithm (involving priorities, fairness, etc)
143+
to choose which thread to schedule next (and hopefully an algorithm more
144+
efficient than the simple polling loop written above). The `Thread.ready` and
145+
`Thread.resume` methods along with how the `pending` list is populated are all
146+
defined [below](#thread-state) as part of the `Thread` class.
147147

148148
The `FuncInst` passed to `Store.invoke` is defined to take 3 parameters:
149149
* an optional `caller` `Supertask` which is used to maintain the
@@ -452,14 +452,12 @@ threads; at that point `Thread`s and `Task`s will be many-to-one, with a single
452452

453453
`Thread` is implemented using the Python standard library's [`threading`]
454454
module. While a Python [`threading.Thread`] is a preemptively-scheduled [kernel
455-
thread], the `Thread` abstraction defined here is a cooperatively-scheduled
456-
user-space thread that only switches between `Thread`s when one of the `Thread`
457-
methods is called. To implement cooperativity, the code below uses
458-
[`threading.Lock`] to control and serialize execution. If Python had [fibers]
459-
or algebraic effects, those could have been used instead since all that's
460-
needed is the ability to switch stacks. In any case, the use of
461-
`threading.Thread` is encapsulated by the `Thread` class so that the rest of
462-
the Canonical ABI can simply use `suspend`/`resume`.
455+
thread], it is coerced to behave like a cooperatively-scheduled [fiber] by
456+
careful use of [`threading.Lock`]. If Python had built-in fibers (or algebraic
457+
effects), those could have been used instead since all that's needed is the
458+
ability to switch stacks. In any case, the use of `threading.Thread` is
459+
encapsulated by the `Thread` class so that the rest of the Canonical ABI can
460+
simply use `suspend`, `resume`, etc.
463461

464462
Introducing the `Thread` class in chunks, a `Thread` has the following fields
465463
and can be in one of the following 3 states based on these fields:
@@ -607,9 +605,8 @@ consider the call `BLOCKED` or keep going.
607605

608606
A "waitable" is a concurrent activity that can be waited on by the built-ins
609607
`waitable-set.wait` and `waitable-set.poll`. Currently, there are 5 different
610-
kinds of waitables: [subtasks](Async.md#structured-concurrency)
611-
and the 4 combinations of the [readable and writable ends of futures and
612-
streams](Async.md#streams-and-futures).
608+
kinds of waitables: [subtasks] and the 4 combinations of the [readable and
609+
writable ends] of futures and streams.
613610

614611
Waitables deliver "events" which are values of the following `EventTuple` type.
615612
The two `int` "payload" fields of `EventTuple` store core wasm `i32`s and are
@@ -764,15 +761,32 @@ class Task(Call, Supertask):
764761
self.context = ContextLocalStorage()
765762
```
766763

764+
The `thread` field is initialized by `Task.thread_start`, which is called by
765+
`Thread`'s constructor. Symmetrically, when the `Thread`'s root function
766+
call returns, `Task.thread_stop` is called to trap if the `OnResolve` callback
767+
has not been called (by the `Task.return_` and `Task.cancel` methods,
768+
defined below).
769+
```python
770+
def thread_start(self, thread):
771+
assert(self.thread is None and thread.task is self)
772+
self.thread = thread
773+
774+
def thread_stop(self, thread):
775+
assert(thread is self.thread and thread.task is self)
776+
self.thread = None
777+
trap_if(self.state != Task.State.RESOLVED)
778+
assert(self.num_borrows == 0)
779+
```
780+
767781
The `Task.trap_if_on_the_stack` method checks for unintended reentrance,
768-
enforcing a [component invariant]. This guard uses the `supertask` field of
769-
`Task` which points to the task's supertask in the async call tree defined by
770-
[structured concurrency]. Structured concurrency is necessary to distinguish
771-
between the deadlock-hazardous kind of reentrance (where the new task is a
772-
transitive subtask of a task already running in the same component instance)
773-
and the normal kind of async reentrance (where the new task is just a sibling
774-
of any existing tasks running in the component instance). Note that, in the
775-
[future](Async.md#TODO), there will be a way for a function to opt in (via
782+
enforcing a [component invariant]. This guard uses the `Supertask` defined by
783+
the [Embedding](#embedding) interface to walk up the async call tree defined as
784+
part of [structured concurrency]. The async call tree is necessary to
785+
distinguish between the deadlock-hazardous kind of reentrance (where the new
786+
task is a transitive subtask of a task already running in the same component
787+
instance) and the normal kind of async reentrance (where the new task is just a
788+
sibling of any existing tasks running in the component instance). Note that, in
789+
the [future](Async.md#TODO), there will be a way for a function to opt in (via
776790
function type attribute) to the hazardous kind of reentrance, which will nuance
777791
this test.
778792
```python
@@ -800,8 +814,8 @@ indicate that the core wasm being executed does not expect to be reentered
800814
(e.g., because the code is using a single global linear memory shadow stack).
801815
Concretely, this is assumed to be the case when core wasm is lifted
802816
synchronously or with `async callback`. This predicate is used by the other
803-
`Task` methods to determine whether to acquire/release the
804-
component-instance-wide `exclusive` [`asyncio.Lock`].
817+
`Task` methods to determine whether to acquire/release the component instance's
818+
`exclusive` lock.
805819
```python
806820
def needs_exclusive(self):
807821
return self.opts.sync or self.opts.callback
@@ -826,6 +840,7 @@ backpressure is disabled. There are three sources of backpressure:
826840

827841
```python
828842
def enter(self):
843+
assert(self.thread is not None)
829844
def has_backpressure():
830845
return self.inst.backpressure or (self.needs_exclusive() and self.inst.exclusive)
831846
if has_backpressure() or self.inst.num_waiting_to_enter > 0:
@@ -851,6 +866,17 @@ order. Additionally, the above definition ensures the following properties:
851866
backpressure (i.e., disabling backpressure never unleashes an unstoppable
852867
thundering heard of pending tasks).
853868

869+
Symmetrically, the `Task.exit` method is called before a `Task`'s `Thread`
870+
returns to clear the `exclusive` flag set by `Task.enter`, allowing other
871+
`needs_exclusive` tasks to start or make progress:
872+
```python
873+
def exit(self):
874+
assert(self.thread is not None)
875+
if self.needs_exclusive():
876+
assert(self.inst.exclusive)
877+
self.inst.exclusive = False
878+
```
879+
854880
The `Task.request_cancellation` method is called by the host or wasm caller
855881
(via the `Call` interface of `Task`) to signal that they don't need the return
856882
value and that the caller should hurry up and call the `OnResolve` callback. If
@@ -860,7 +886,7 @@ called with `cancellable` set), `request_cancellation` immediately resumes the
860886
thread, giving the thread the chance to handle cancellation promptly (allowing
861887
`subtask.cancel` to complete eagerly without returning `BLOCKED`). Otherwise,
862888
the cancellation request is remembered in the `Task`'s `state` so that it can
863-
be delivered in the future by `Task.wait_until` (defined next).
889+
be delivered in the future by `Task.suspend_until` (defined next).
864890
```python
865891
def request_cancellation(self):
866892
assert(self.state == Task.State.INITIAL)
@@ -890,13 +916,12 @@ pending cancellation set by `Task.request_cancellation`:
890916
return self.thread.suspend_until(ready_func, cancellable)
891917
```
892918

893-
The `Task.wait_until` method is called by `waitable-set.wait` or from the event
894-
loop of an `async callback` function when `CallbackCode.WAIT` is returned.
919+
The `Task.wait_until` method is called by `canon_waitable_set_wait` and from
920+
the event loop in `canon_lift` when `CallbackCode.WAIT` is returned.
895921
`wait_until` waits until a waitable in the given waitable set has a pending
896922
event to deliver *and* the caller-supplied condition is met. While suspended,
897-
the `WaitableSet.num_waiting` counter is kept above `0` so that
898-
`waitable-set.drop` will trap if another task tries to drop the waitable set
899-
being used.
923+
the `num_waiting` counter is kept above `0` so that `waitable-set.drop` will
924+
trap if another task tries to drop the waitable set being used.
900925
```python
901926
def wait_until(self, ready_func, wset, cancellable) -> EventTuple:
902927
wset.num_waiting += 1
@@ -910,11 +935,11 @@ being used.
910935
return event
911936
```
912937

913-
The `Task.poll_until` method is called by `waitable-set.poll` or from the event
914-
loop of an `async callback` function when `CallbackCode.POLL` is returned.
915-
Unlike `wait_until`, `poll_until` does not wait for the given waitable set to
916-
have a pending event, returning `EventCode.NONE` if there is none already.
917-
However, `poll_until` *does* call `suspsend_until` to allow the runtime to
938+
The `Task.poll_until` method is called by `canon_waitable_set_poll` and from
939+
the event loop in `canon_lift` when `CallbackCode.POLL` is returned. Unlike
940+
`wait_until`, `poll_until` does not wait for the given waitable set to have a
941+
pending event, returning `EventCode.NONE` if there is none already. However,
942+
`poll_until` *does* call `suspsend_until` to allow the runtime to
918943
nondeterministically switch to another task (or not).
919944
```python
920945
def poll_until(self, ready_func, wset, cancellable) -> Optional[EventTuple]:
@@ -929,10 +954,9 @@ nondeterministically switch to another task (or not).
929954
return event
930955
```
931956

932-
The `Task.yield_until` method is called by the `yield` built-in or from the
933-
event loop of an `async callback` function when `CallbackCode.YIELD` is
934-
returned. `yield_until` works like `poll_until` if given a fresh empty waitable
935-
set.
957+
The `Task.yield_until` method is called by `canon_yield` and from
958+
the event loop in `canon_lift` when `CallbackCode.YIELD` is returned.
959+
`yield_until` works like `poll_until` if given a fresh empty waitable set.
936960
```python
937961
def yield_until(self, ready_func, cancellable) -> EventTuple:
938962
if not self.suspend_until(ready_func, cancellable):
@@ -941,13 +965,13 @@ set.
941965
return (EventCode.NONE, 0, 0)
942966
```
943967

944-
The `Task.return_` method is called by either `canon_task_return` or
945-
`canon_lift` to return a list of lifted values to the task's caller via the
946-
`OnResolve` callback. There is a dynamic error if the callee has not dropped
947-
all borrowed handles by the time `task.return` is called which means that the
948-
caller can assume that all its lent handles have been returned to it when it
949-
receives the `SUBTASK` `RETURNED` event. Note that the initial `trap_if` allows
950-
a task to return a value even after cancellation has been requested.
968+
The `Task.return_` method is called by `canon_task_return` and `canon_lift` to
969+
return a list of lifted values to the task's caller via the `OnResolve`
970+
callback. There is a dynamic error if the callee has not dropped all borrowed
971+
handles by the time `task.return` is called which means that the caller can
972+
assume that all its lent handles have been returned to it when it receives the
973+
`SUBTASK` `RETURNED` event. Note that the initial `trap_if` allows a task to
974+
return a value even after cancellation has been requested.
951975
```python
952976
def return_(self, result):
953977
trap_if(self.state == Task.State.RESOLVED)
@@ -957,13 +981,14 @@ a task to return a value even after cancellation has been requested.
957981
self.state = Task.State.RESOLVED
958982
```
959983

960-
The `Task.cancel` method is called by `canon_task_cancel` and enforces the same
961-
`num_borrows` condition as `return_`, ensuring that when the caller's
962-
`OnResolve` callback is called, the caller knows all borrows have been
963-
returned. The initial `trap_if` only allows cancellation after cancellation has
964-
been *delivered* to core wasm. In particular, if `request_cancellation` cannot
965-
synchronously deliver cancellation and sets `Task.state` to `PENDING_CANCEL`,
966-
core wasm will still trap if it tries to call `task.cancel`.
984+
Lastly, the `Task.cancel` method is called by `canon_task_cancel` and
985+
enforces the same `num_borrows` condition as `return_`, ensuring that when
986+
the caller's `OnResolve` callback is called, the caller knows all borrows
987+
have been returned. The initial `trap_if` only allows cancellation after
988+
cancellation has been *delivered* to core wasm. In particular, if
989+
`request_cancellation` cannot synchronously deliver cancellation and sets
990+
`Task.state` to `PENDING_CANCEL`, core wasm will still trap if it tries to
991+
call `task.cancel`.
967992
```python
968993
def cancel(self):
969994
trap_if(self.state != Task.State.CANCEL_DELIVERED)
@@ -972,33 +997,6 @@ core wasm will still trap if it tries to call `task.cancel`.
972997
self.state = Task.State.RESOLVED
973998
```
974999

975-
The `Task.exit` method is called before a `Task`'s `Thread` returns to clear
976-
the `exclusive` flag set by `Task.enter`, allowing other `needs_exclusive`
977-
tasks to start or make progress.
978-
```python
979-
def exit(self):
980-
assert(self.thread is not None)
981-
if self.needs_exclusive():
982-
assert(self.inst.exclusive)
983-
self.inst.exclusive = False
984-
```
985-
986-
Lastly, the `Task.thread_start` and `Task.thread_stop` functions are called by
987-
a `Thread` (defined above) to register/unregister itself when it starts/stops.
988-
When a `Task`'s final (and, currently, only) `Thread` returns, the `trap_if`
989-
guards that the task has upheld its contract to call `OnResolve`.
990-
```python
991-
def thread_start(self, thread):
992-
assert(self.thread is None and thread.task is self)
993-
self.thread = thread
994-
995-
def thread_stop(self, thread):
996-
assert(thread is self.thread and thread.task is self)
997-
self.thread = None
998-
trap_if(self.state != Task.State.RESOLVED)
999-
assert(self.num_borrows == 0)
1000-
```
1001-
10021000

10031001
#### Subtask State
10041002

@@ -1198,15 +1196,15 @@ and lowering and defined below.
11981196

11991197
Values of `stream` type are represented in the Canonical ABI as `i32` indices
12001198
into the current component instance's table referring to either the
1201-
[readable or writable end](Async.md#streams-and-futures) of a stream. Reading
1202-
from the readable end of a stream is achieved by calling `stream.read` and
1203-
supplying a `WritableBuffer`. Conversely, writing to the writable end of a
1204-
stream is achieved by calling `stream.write` and supplying a `ReadableBuffer`.
1205-
The runtime waits until both a readable and writable buffer have been supplied
1206-
and then performs a direct copy between the two buffers. This rendezvous-based
1207-
design avoids the need for an intermediate buffer and copy (unlike, e.g., a
1208-
Unix pipe; a Unix pipe would instead be implemented as a resource type owning
1209-
the buffer memory and *two* streams; on going in and one coming out).
1199+
[readable or writable end] of a stream. Reading from the readable end of a
1200+
stream is achieved by calling `stream.read` and supplying a `WritableBuffer`.
1201+
Conversely, writing to the writable end of a stream is achieved by calling
1202+
`stream.write` and supplying a `ReadableBuffer`. The runtime waits until both
1203+
a readable and writable buffer have been supplied and then performs a direct
1204+
copy between the two buffers. This rendezvous-based design avoids the need
1205+
for an intermediate buffer and copy (unlike, e.g., a Unix pipe; a Unix pipe
1206+
would instead be implemented as a resource type owning the buffer memory and
1207+
*two* streams; on going in and one coming out).
12101208

12111209
The result of a `{stream,future}.{read,write}` is communicated to the wasm
12121210
guest via a `CopyResult` code:
@@ -3771,8 +3769,8 @@ def canon_waitable_set_drop(task, i):
37713769
return []
37723770
```
37733771
Note that `WaitableSet.drop` will trap if it is non-empty or there is a
3774-
concurrent `yield`, `waitable-set.wait`, `waitable-set.poll` or `async
3775-
callback` currently using this waitable set.
3772+
concurrent `waitable-set.wait` or `waitable-set.poll` or `async callback`
3773+
currently using this waitable set.
37763774

37773775

37783776
### 🔀 `canon waitable.join`
@@ -4438,7 +4436,9 @@ def canon_thread_available_parallelism():
44384436
[Structured Concurrency]: Async.md#structured-concurrency
44394437
[Backpressure]: Async.md#backpressure
44404438
[Current Task]: Async.md#current-task
4439+
[Subtasks]: Async.md#structured-concurrency
44414440
[Readable and Writable Ends]: Async.md#streams-and-futures
4441+
[Readable or Writable End]: Async.md#streams-and-futures
44424442
[Context-Local Storage]: Async.md#context-local-storage
44434443
[Subtask State Machine]: Async.md#cancellation
44444444
[Lazy Lowering]: https://github.com/WebAssembly/component-model/issues/383
@@ -4473,7 +4473,7 @@ def canon_thread_available_parallelism():
44734473
[Surrogate]: https://unicode.org/faq/utf_bom.html#utf16-2
44744474
[Name Mangling]: https://en.wikipedia.org/wiki/Name_mangling
44754475
[Kernel Thread]: https://en.wikipedia.org/wiki/Thread_(computing)#kernel_thread
4476-
[Fibers]: https://en.wikipedia.org/wiki/Fiber_(computer_science)
4476+
[Fiber]: https://en.wikipedia.org/wiki/Fiber_(computer_science)
44774477
[Asyncify]: https://emscripten.org/docs/porting/asyncify.html
44784478

44794479
[`import_name`]: https://clang.llvm.org/docs/AttributeReference.html#import-name

design/mvp/canonical-abi/definitions.py

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,16 @@ def __init__(self, opts, inst, ft, supertask, on_resolve):
518518
self.thread = None
519519
self.context = ContextLocalStorage()
520520

521+
def thread_start(self, thread):
522+
assert(self.thread is None and thread.task is self)
523+
self.thread = thread
524+
525+
def thread_stop(self, thread):
526+
assert(thread is self.thread and thread.task is self)
527+
self.thread = None
528+
trap_if(self.state != Task.State.RESOLVED)
529+
assert(self.num_borrows == 0)
530+
521531
def trap_if_on_the_stack(self, inst):
522532
c = self.supertask
523533
while c is not None:
@@ -528,6 +538,7 @@ def needs_exclusive(self):
528538
return self.opts.sync or self.opts.callback
529539

530540
def enter(self):
541+
assert(self.thread is not None)
531542
def has_backpressure():
532543
return self.inst.backpressure or (self.needs_exclusive() and self.inst.exclusive)
533544
if has_backpressure() or self.inst.num_waiting_to_enter > 0:
@@ -542,6 +553,12 @@ def has_backpressure():
542553
self.inst.exclusive = True
543554
return True
544555

556+
def exit(self):
557+
assert(self.thread is not None)
558+
if self.needs_exclusive():
559+
assert(self.inst.exclusive)
560+
self.inst.exclusive = False
561+
545562
def request_cancellation(self):
546563
assert(self.state == Task.State.INITIAL)
547564
if self.thread.cancellable and not (self.thread.in_event_loop and self.inst.exclusive):
@@ -597,22 +614,6 @@ def cancel(self):
597614
self.on_resolve(None)
598615
self.state = Task.State.RESOLVED
599616

600-
def exit(self):
601-
assert(self.thread is not None)
602-
if self.needs_exclusive():
603-
assert(self.inst.exclusive)
604-
self.inst.exclusive = False
605-
606-
def thread_start(self, thread):
607-
assert(self.thread is None and thread.task is self)
608-
self.thread = thread
609-
610-
def thread_stop(self, thread):
611-
assert(thread is self.thread and thread.task is self)
612-
self.thread = None
613-
trap_if(self.state != Task.State.RESOLVED)
614-
assert(self.num_borrows == 0)
615-
616617
#### Subtask State
617618

618619
class Subtask(Waitable):

0 commit comments

Comments
 (0)