Skip to content

Commit e819a61

Browse files
committed
Add 'Async ABI' section with examples
Resolves #448
1 parent 173de57 commit e819a61

File tree

1 file changed

+167
-0
lines changed

1 file changed

+167
-0
lines changed

design/mvp/Async.md

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ summary of the motivation and animated sketch of the design in action.
2222
* [Waiting](#waiting)
2323
* [Backpressure](#backpressure)
2424
* [Returning](#returning)
25+
* [Async ABI](#async-abi)
26+
* [Async Import ABI](#async-import-abi)
27+
* [Async Export ABI](#async-export-abi)
2528
* [Examples](#examples)
2629
* [Interaction with the start function](#interaction-with-the-start-function)
2730
* [Interaction with multi-threading](#interaction-with-multi-threading)
@@ -483,6 +486,169 @@ A task may not call `task.return` unless it is in the "started" state. Once
483486
finish once it is in the "returned" state. See the [`canon_task_return`]
484487
function in the Canonical ABI explainer for more details.
485488

489+
## Async ABI
490+
491+
At an ABI level, native async in the Component Model defines for every WIT
492+
function an async-oriented core function signature that can be used instead of
493+
or in addition to the existing (Preview-2-defined) synchronous core function
494+
signature. This async-oriented core function signature is intended to be called
495+
or implemented by generated bindings which then map the low-level core async
496+
protocol to the languages' higher-level native concurrency features. Because
497+
the WIT-level `async` attribute is purely a *hint* (as mentioned
498+
[above](#sync-and-async-functions)), *every* WIT function has an async core
499+
function signature; `async` just provides hints to the bindings generator for
500+
which to use by default.
501+
502+
### Async Import ABI
503+
504+
Given an imported WIT function:
505+
```wit
506+
world w {
507+
import foo: func(s: string) -> string;
508+
}
509+
```
510+
the default sync import function signature is:
511+
```wat
512+
;; sync
513+
(func (param $s-ptr i32) (param $s-len i32) (param $out i32))
514+
```
515+
where `$out` must be a 4-byte-aligned pointer into linear memory into which the
516+
8-byte (pointer, length) of the returned string will be stored.
517+
518+
The new async import function signature is:
519+
```wat
520+
;; async
521+
(func (param $in i32) (param $out i32) (result i32))
522+
```
523+
where `$in` must be a 4-byte-aligned pointer into linear memory from which the
524+
8-byte (pointer, length) of the string argument will be loaded and `$out` works
525+
the same as in the synchronous case. What's different, however, is *when* `$in`
526+
and `$out` are read or written. In a synchronous call, they are always read or
527+
written before the call returns. In an asynchronous call, there is a set of
528+
possibilities indicated by the `(result i32)` value:
529+
* If the returned `i32` is `0`, then the call completed synchronously without
530+
blocking and so `$in` has been read and `$out` has been written.
531+
* Otherwise, the high 28 bits of the `i32` are the index of a new `Subtask`
532+
in the current component instance's `waitables` table. The low 4 bits
533+
indicate how far the callee made it before blocking:
534+
* If `1`, the callee didn't even start (due to backpressure), and thus
535+
neither `$in` nor `$out` have been accessed yet.
536+
* If `2`, the callee started by reading `$in`, but blocked before writing
537+
`$out`.
538+
539+
The async signature `(func (param i32 i32) (result i32))` is the same for
540+
almost all WIT function types since the ABI stores everything in linear memory.
541+
However, there are three special cases:
542+
* If the WIT parameter list is empty, `$in` is removed.
543+
* If the WIT parameter list flattens to exactly 1 core value type (`i32` or
544+
otherwise), `$in` uses that core value type and the argument is passed
545+
by value.
546+
* If the WIT result is empty, `$out` is removed.
547+
548+
For example:
549+
| WIT function type | Async ABI |
550+
| ----------------------------------------- | --------------------- |
551+
| `func()` | `(func (result i32))` |
552+
| `func() -> string` | `(func (param $out i32) (result i32))` |
553+
| `func(s: string)` | `(func (param $in i32) (result i32))` |
554+
| `func(x: f32) -> f32` | `(func (param $in f32) (param $out i32) (result i32))` |
555+
| `func(x: list<list<u8>>) -> list<string>` | `(func (param $in i32) (param $out i32) (result i32))` |
556+
557+
`future` and `stream` can appear anywhere in the parameter or result types. For example:
558+
```wit
559+
func(s1: stream<future<string>>, s2: list<stream<string>>) -> result<stream<string>, stream<error>>
560+
```
561+
In *both* the sync and async ABIs, a `future` or `stream` in the WIT-level type
562+
translates to a single `i32` in the ABI. This `i32` is an index into the
563+
component instance's `waitables` table. For example, for the WIT function type:
564+
```wit
565+
func(f: future<string>) -> future<u32>
566+
```
567+
the synchronous ABI has signature:
568+
```wat
569+
(func (param $f i32) (result i32))
570+
```
571+
and the asynchronous ABI has the signature:
572+
```wat
573+
(func (param $in i32) (param $out i32) (result i32))
574+
```
575+
where, according to the above rules, `$in` is the index of a future in the
576+
`waitables` table (not a pointer to one) while `$out` is a pointer to a linear
577+
memory location that will receive an `i32` index.
578+
579+
For the runtime semantics of this `i32` index, see `lift_stream`,
580+
`lift_future`, `lower_stream` and `lower_future` in the [Canonical ABI
581+
Explainer]. For a complete description of how async imports work, see
582+
[`canon_lower`] in the Canonical ABI Explainer.
583+
584+
585+
#### Async Export ABI
586+
587+
Given an exported WIT function:
588+
```wit
589+
world w {
590+
export foo: func(s: string) -> string;
591+
}
592+
```
593+
the default sync export function signature is:
594+
```wat
595+
;; sync
596+
(func (param $s-ptr i32) (param $s-len i32) (result $retp i32))
597+
```
598+
where (working around the continued lack of multi-return support throughout
599+
the core wasm toolchain) `$retp` must be a 4-byte-aligned pointer into linear
600+
memory from which the 8-byte (pointer, length) of the string result can be
601+
loaded.
602+
603+
The async export ABI provides two flavors: stackful and stackless.
604+
605+
The async stackful export function signature is:
606+
```wat
607+
;; async, no callback
608+
(func (param $s-ptr i32) (param $s-len i32))
609+
```
610+
The parameters work just like synchronous parameters. There is no core function
611+
result because a callee [returns](#returning) their value by *calling* the
612+
*imported* `task.return` function which has signature:
613+
```wat
614+
;; task.return
615+
(func (param $ret-ptr i32) (result $ret-len i32))
616+
```
617+
The parameters of `task.return` work the same as if the WIT return type was the
618+
WIT parameter type of a synchronous function. For example, if more than 16
619+
core parameters would be needed, a single `i32` pointer into linear memory is
620+
used.
621+
622+
The async stackless export function signature is:
623+
```wat
624+
;; async, callback
625+
(func (param $s-ptr i32) (param $s-len i32) (result i32))
626+
```
627+
The parameters also work just like synchronous parameters. The callee returns
628+
their value by calling `task.return` just like the stackful case. The `(result
629+
i32)` lets the core function return what it wants the runtime to do next:
630+
* If the low 4 bits are `0`, the callee completed (and called `task.return`)
631+
without blocking.
632+
* If the low 4 bits are `1`, the callee wants to yield, allowing other code
633+
to run, but resuming thereafter without waiting on anything else.
634+
* If the low 4 bits are `2`, the callee wants to wait for an event to occur in
635+
the waitable set whose index is stored in the high 28 bits.
636+
* If the low 4 bits are `3`, the callee wants to poll for any events that have
637+
occurred in the waitable set whose index is stored in the high 28 bits.
638+
639+
When an async stackless function is exported, a companion "callback" function
640+
must also be exported with signature:
641+
```wat
642+
(func (param i32 i32 i32) (result i32))
643+
```
644+
The `(result i32)` has the same interpretation as the stackless export function
645+
and the runtime will repeatedly call the callback until a value of `0` is
646+
returned. The `i32` parameters describe what happened that caused the callback
647+
to be called again.
648+
649+
For a complete description of how async exports work, see [`canon_lift`] in the
650+
Canonical ABI Explainer.
651+
486652

487653
## Examples
488654

@@ -784,6 +950,7 @@ comes after:
784950
[ESM-integration]: Explainer.md#ESM-integration
785951

786952
[Canonical ABI Explainer]: CanonicalABI.md
953+
[ABI Options]: CanonicalABI.md#canonical-abi-options
787954
[`canon_lift`]: CanonicalABI.md#canon-lift
788955
[`unpack_callback_result`]: CanonicalABI.md#canon-lift
789956
[`canon_lower`]: CanonicalABI.md#canon-lower

0 commit comments

Comments
 (0)