# The low-level contract
This page is the reference for the contract between a plugin component and
its host: the exports each world expects, the memory rules, the surface
buffer protocol and the error types. The Rust SDK implements all of it, so
a plugin written with the SDK needs none of this page. Read it to debug
what the SDK sends, or to read the protocol itself.
Sources, in order of authority:
- The WIT package `standard:plugin@2.0.0`,
[`wit/plugin.wit`](../wit/plugin.wit) in this crate,
is the contract.
- Where the WIT is silent, this page describes what the SDK does and cites
the SDK file and item. A component that behaves the same way works with
the same hosts.
- Where neither states a rule, this page says it is unspecified. Do not
depend on unspecified behaviour.
## Worlds and exports
A plugin is a component that targets one of two worlds.
| `ui-plugin` | `ui` | `activate`, `frame`, `event`, `deactivate` |
| `daemon-plugin` | `daemon`, `companion` | `activate`, `event`, `drive`, `deactivate`, the interface `daemon-events` (`handle-event`), the interface `companion` (`handle-call`) |
`standard-plugin check` refuses a component that lacks an export its world
requires, and warns about an export no host calls
(`src/check.rs` in `standard-plugin-cli`,
`check_component`).
### When the UI exports are called
| `activate(config)` | Once after instantiation, with the configuration document (JSON text). |
| `frame(now-ms, interval-ms, power) -> option<u64>` | When a paced frame is due and at least one of the plugin's surfaces is visible. `now-ms` is the viewer's monotonic clock; `interval-ms` is the paced frame interval. |
| `event(e)` | For everything else: resizes, visibility, input, account changes, call results (the `event` variant in `event-types`). |
| `deactivate()` | Before the instance is dropped. |
`frame` returns the `now-ms` at which the plugin next wants a frame:
`now-ms` itself for the next paced frame, a later instant for a clock, and
`none` to wait for an event. Nothing else wakes an idle viewer. From any
export, `view.request-frame` asks for the next paced frame and
`view.wake-at(at-ms)` asks for a frame at an instant; the earliest request
wins. The SDK's `Frame::request_frame` is `wake_at(now_ms)`, and the
earliest of the plugin's requests is `frame`'s return value
([`src/ui_runtime.rs`](../src/ui_runtime.rs),
`Frame`).
A commit made in `event` is not a frame request. The viewer samples it on
the next frame that something else causes. A plugin that repaints in
`event` also calls `view.request-frame` or `view.wake-at`.
### When the daemon exports are called
| `activate(config)` | Once after instantiation, with the configuration document. |
| `event(e)` | Account events: value changes, live messages, plugin events, account changes, call results. |
| `daemon-events.handle-event(e)` | Machine events, in order: file changes, failed watches, process output and exits, pane changes. |
| `companion.handle-call(method, payload, from)` | A call from another runtime of the plugin (`from.kind` is `ui` or `daemon`). Returns the answer, or `invalid` for an unknown method. |
| `drive(now-ms) -> option<u64>` | After `activate`, after `event`, `handle-event` and `handle-call`, and at the instant the previous call asked for. Returns the next instant, or `none` to wait for an event. |
| `deactivate()` | The WIT does not say when a host calls it. |
The export is named `drive` because a daemon plugin may link wasi-libc,
which defines a `poll` symbol (`plugin.wit`, `daemon-plugin`).
Rules a daemon plugin can rely on:
- `drive` runs after the calls listed above. The WIT does not promise one
`drive` per call, so do all pending work in each `drive`.
- A host may stop an instance without calling `deactivate`. Do not put
work that must happen in `deactivate`.
- The host delivers the plugin event `system.plugin.interest` once when
the plugin starts and again when the set of shown surfaces changes
(`plugin.wit`, `events`). The SDK hears it only when it differs from the
last one it received (`src/daemon/runtime.rs`, `DaemonRuntime::event`).
- WebSocket activity arrives as the plugin event `system.net.websocket`
(`plugin.wit`, `daemon-net`).
The SDK's daemon runtime runs the plugin's async tasks on a small executor
inside `drive` and returns the earliest timer the tasks wait for
(`src/daemon/runtime.rs`, `DaemonRuntime::drive`).
### Threading
A component instance runs on one thread, and the host calls one export at
a time. The SDK's runtimes and instance-local state depend on this
([`src/local.rs`](../src/local.rs),
`instance_local!`; `src/ui_runtime.rs`, the `Sync` impl of `UiRuntime`).
Host calls go out from inside an export.
### Gestures
Some calls work only while the plugin handles a user gesture: a `key`, a
`paste` or a pointer `down` on one of its surfaces, or one of its
`command`s. `surface.open` and `view.focus-pane` need a gesture.
`url.open` needs a gesture or the second after one, once per input. Each
answers `invalid` outside that window (`plugin.wit`, `surface`, `view`,
`url`).
## Memory
- The component exports its linear memory and the canonical ABI realloc
function. The host uses the realloc function to place strings and lists
in the plugin's memory: the `activate` configuration, event payloads and
the results of host calls. The plugin owns those allocations and frees
them. Bindings that `wit-bindgen` generates do this.
- The SDK's components use UTF-8 for strings (the `wit-bindgen` default).
Other string encodings are unspecified.
- The shared buffer is little-endian. The SDK refuses to compile for a
big-endian target ([`src/lib.rs`](../src/lib.rs),
the `compile_error!` for `target_endian = "big"`).
- A UI plugin has no libc. The SDK's `runtime` feature supplies what a
`no_std` component needs: a global allocator that reuses freed blocks, a
panic handler that traps, `cabi_realloc`, and the C symbols `memcmp`,
`bcmp`, `fmodf` and `fmod`
([`src/runtime.rs`](../src/runtime.rs)).
The allocator reuses memory because every resize allocates a new region
and frees the old one; an allocator that never reuses memory reaches the
memory limit after a few resizes.
- `memoryMib` in the manifest limits linear memory; the host caps it
([manifest](manifest.md)). The contract states no other allocation rule.
## The surface buffer
A UI plugin paints each surface into a region of its own linear memory.
The host reads that region directly. The layout comes from `plugin.wit`
(`surface`, `region-layout`); the SDK constants are in
[`src/surface/mod.rs`](../src/surface/mod.rs)
(`HEADER_LEN`, `CELL_LEN`, `PIXEL_LEN`).
### Layout
```text
offset 0 header: 8 little-endian u32 words (32 bytes)
offset 32 slot 0, slot-len bytes
offset 32 + slot-len slot 1, slot-len bytes
total len = 32 + 2 * slot-len
```
| 0 | `seq`: the sequence number (below) |
| 1 | `model`: 0 cells, 1 pixels |
| 2, 3 | `cols`, `rows` |
| 4, 5 | `px-w`, `px-h` |
| 6, 7 | `cell-px-w`, `cell-px-h` |
`slot-len` is `cols * rows * 16` in the cells model and `px-w * px-h * 4`
in the pixels model. Always take `len`, `header-len`, `slot-len`, the
model and the geometry from `surface.layout(id)`. Do not compute them
yourself.
`px-w` and `px-h` are zero while the viewer has no pixel geometry. A
pixels surface then has an empty slot and nothing to paint.
The WIT states no alignment for the region. The SDK allocates it as
`u32` words, so it is 4-byte aligned (`src/surface/mod.rs`, `Binding`).
Align it the same way.
### The cells model
A cell is 16 bytes, four little-endian `u32` words:
| 0..4 | `grapheme` | 0 is empty. Otherwise a Unicode scalar value. Values from `0x8000_0000` up are reserved for a grapheme pool that is not implemented. |
| 4..8 | `fg` | A colour word (below). |
| 8..12 | `bg` | A colour word. The default colour is a transparent background. |
| 12..14 | `attrs` | `u16` bits: bold 1, dim 2, italic 4, underline 8, reverse 16, strikethrough 32. |
| 14..16 | padding | Zero. |
A colour word's top byte is its model
([`src/colour.rs`](../src/colour.rs),
`Colour::encode` and `Colour::decode`):
| `0x00______` | The default: the theme's text colour, or a transparent background. |
| `0x01RRGGBB` | An RGB colour. |
| `0x020000II` | Terminal palette index `II`. |
| `0x030000TT` | Theme slot `TT`: 0 fg, 1 bg, 2 recede-fg, 3 recede-bg, 4 accent. |
`Colour::decode` reads an unknown model byte as the default colour and
ignores bits a model does not use.
The SDK writes cells by these rules
([`src/surface/cells.rs`](../src/surface/cells.rs),
`char_width`, `Grid::put`). The module documentation says they mirror the
viewer:
- A cell holds one scalar: the first scalar of a grapheme cluster.
Combining marks, variation selectors and joined sequences are dropped.
- A character's width is its East Asian width, 1 or 2. Control
characters (C0, DEL, C1), the bidi controls U+061C, U+200E, U+200F,
U+202A to U+202E and U+2066 to U+2069, and zero-width characters are
never written.
- A wide character takes two cells: the character, then a cell whose
grapheme is 0 with the same colours and attributes.
- A wide character that does not fit before the right edge is replaced by
one empty cell.
- A write over the right half of a wide character empties its left half.
A write over the left half leaves the right half empty. Either way the
neighbour cell joins the dirty rectangle, so the viewer reads it again.
### The pixels model
A pixel is 4 bytes, `r, g, b, a`, in rows from top to bottom. `px-w` is
`cols * cell-px-w`. Alpha is straight (unpremultiplied)
([rendering](rendering.md)).
`view.capabilities().pixel-scale` is 1, or 2 while the host has halved
the plugin's pixel resolution. The geometry the plugin receives is then
the smaller one, and the viewer scales the image up (`plugin.wit`, `view`).
### Binding a region
The SDK binds a surface in `Inner::bind` (`src/surface/mod.rs`):
1. Call `surface.layout(id)`. An `id` the manifest does not declare answers
`invalid`.
2. If the layout's model is not the model you paint in, stop. The SDK
returns `Error::WrongModel`.
3. If `cols` or `rows` is 0, the surface has no size yet. Wait for
`event::resize`.
4. Allocate `len` bytes, zeroed.
5. Write header words 1 to 7 from the layout. The SDK writes word 0 with
its current sequence number.
6. Call `surface.attach(id, region)`, where `region` is the allocated bytes.
The canonical ABI passes a `list<u8>` argument as an address and a
length in the plugin's memory, so the host sees the region itself.
`attach` refuses a region that is shorter than `len` or whose header does
not describe the current layout (`plugin.wit`, `attach`).
After `attach` returns, the host reads the region at any time until it is
detached or replaced. Keep it allocated at the same address. Do not reuse
its memory for anything else.
A surface can have a size before the plugin creates it: for example, when
an instance is restarted. The SDK's `Surface::new` calls `layout` at once
and binds when the surface has a size, else on its first resize
(`src/surface/mod.rs`, `Surface::new`).
### Painting and committing
The region holds two slots. The plugin paints one slot (the back slot)
while the host may read the other one.
The SDK's rules (`src/surface/mod.rs`, `Inner::commit`, `Inner::apply_sync`,
`Inner::back`):
1. After a bind, the back slot is slot 0.
2. Paint only the back slot. Record every rectangle you change, in cells
(cells model) or pixels (pixels model).
3. To commit, increment the sequence number (wrapping at 2^32), write it
to header word 0, and call `surface.commit(id, slot, dirty)` with the
back slot and the changed rectangles.
4. When `commit` succeeds, the other slot becomes the back slot.
5. Before the first write to the new back slot, copy into it the
rectangles of the commit that just went out, from the slot that was
just committed. Each slot then holds a whole frame, and drawing stays
incremental.
6. When `commit` fails, the SDK keeps the back slot and adds the
rectangles back to its dirty set for the next commit.
The contract says that the viewer reads only committed slots, only the
dirty rectangles, and only when a frame is due and the surface is visible
(`plugin.wit`, `surface`). It does not say that the viewer reads every
commit. Rule 5 keeps the image correct when the viewer skips one.
Other commit rules:
- An empty `dirty` list changes nothing and requests no frame. The SDK
makes no host call when nothing changed: `Surface::commit` returns
`Ok(false)`.
- The first commit after an attach is fully dirty. The SDK sends one
rectangle that covers the surface.
- The SDK clamps rectangles to the surface. It merges rectangles that
touch, and past 16 it sends their bounding box
([`src/surface/dirty.rs`](../src/surface/dirty.rs),
`MAX_RECTS`). The contract states no limit on the list.
- `slot` is 0 or 1.
- To repaint all of a slot, skip rule 5 and mark the whole surface dirty.
The SDK's `clear()` and `Surface<Pixels>::repaint_all()` do this.
The WIT says the header carries "a sequence number so a half-written slot
is never sampled". It does not say how a host uses word 0. Write it as
the SDK does.
### Resize and model changes
`event::resize(id, geometry)` says a surface changed size or model. The
plugin then binds a new region: `layout`, allocate, write the header,
`attach` (`plugin.wit`, `event`).
- The host keeps reading the old region until the first commit at the new
size, and never reads it after that commit returns. Free the old region
only after that commit (`plugin.wit`, `attach`).
- The SDK keeps one retired region. When several resizes arrive before a
commit, it keeps the first old region and frees the later ones at once
(`src/surface/mod.rs`, `Inner::bind`, the `retired` field).
- The new region starts zeroed and the next commit is fully dirty. Repaint
everything.
- The SDK rebinds every surface of the resized id before the plugin's
`event` sees the resize (`src/ui_runtime.rs`, `UiRuntime::event`;
`src/surface/mod.rs`, `rebind_surfaces`).
- A surface whose manifest lists both models may switch model. `layout`
then reports the other model. The SDK's `AnySurface` detaches the old
surface and binds a new one in the new model under the same id
(`src/surface/mod.rs`, `AnySurface::sync`).
- An instance surface (`<surface>@<owner>`) that resizes to zero is gone.
The SDK's `Instances` drops its surface then (`Instances::event`).
### Detaching
`surface.detach(id)` unbinds the region. The host must stop reading a
region before its memory is freed, so the SDK detaches when a `Surface` is
dropped (`src/surface/mod.rs`, `impl Drop for Surface`).
### Sizing requests
`surface.request-size(id, cols, rows)` asks for a size in cells instead of
the manifest's `height` and `width`. 0 in a dimension is the viewer's
choice, except for the rows of a `machine.after`, `project.after` or
`project.before` instance, where 0 rows hides the instance. The viewer
clamps the request to what the anchor allows and answers with a resize
when the size changes. The request stays in force until the plugin asks
again. An instance id may be asked before the viewer has sized it
(`plugin.wit`, `request-size`; [manifest](manifest.md#surfaces)).
The plugin paints at the size the latest resize reports.
## Instances
`pane.footer`, `pane.header`, `pane.overlay`, `machine.after`,
`project.after` and `project.before` surfaces have one instance per owner,
named `<surface-id>@<owner-id>`. Each instance arrives as its own
`resize` and `visibility`, and the plugin binds a region per instance by
that id. `view.surface-pane`, `view.surface-machine` and
`view.surface-project` name an instance's owner (`plugin.wit`, `view`;
[manifest](manifest.md#pane-machine-and-project-instances)).
## Errors
Every fallible host call returns `call-error` (`plugin.wit`, `types`):
| `grant-denied(grant)` | The manifest does not carry the grant the call needs. The payload names it. |
| `rate-limited` | The per-plugin rate limit for this call kind was reached. |
| `unavailable(reason)` | The backing service is not reachable now. A `calls.call` timeout is `unavailable("call_timeout")`. |
| `invalid(reason)` | The arguments were malformed: an unknown surface id, an oversized value, a key outside the plugin's namespace, a gesture-only call outside a gesture. |
An import outside the plugin's world fails at instantiation, before any
call (`plugin.wit`, the package comment).
The SDK's `Error`
([`src/error.rs`](../src/error.rs)) carries
those four variants and adds three of its own. The host never sends them:
| `Json` | A payload did not serialize or deserialize. |
| `NotAttached` | The surface has no buffer yet: the viewer has not laid it out. |
| `WrongModel` | The layout reports the other model. |
A panic in an SDK plugin traps (`src/runtime.rs`, the panic handler). What
a host does after a trap is outside the contract; [rendering](rendering.md)
and [daemon plugins](daemon-plugins.md) describe what the plugin sees.
## Unspecified
Neither the WIT nor the SDK states these. Do not depend on them:
- How a host uses header word 0 (`seq`).
- The alignment of a region. The SDK uses 4 bytes.
- A limit on the number of dirty rectangles in one commit.
- Whether the viewer reads every commit.
- When a daemon host calls `deactivate`, and whether it calls it at all.
- Whether `drive` runs once per event or once per group of events.
- String encodings other than UTF-8.
- The grapheme pool for scalars from `0x8000_0000` up. It is reserved.