You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/pages/reference/wasm-abi.md
+28-28Lines changed: 28 additions & 28 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -3,7 +3,7 @@ title: "WASM module ABI"
3
3
description: "The wire format a `.wasm` module must follow to be importable by Edge Python."
4
4
---
5
5
6
-
> **Sealed contract, plugin ABI v1.** Every signature, op code, tag, and error kind here is the public contract for CDN-distributed `.wasm` plugin modules (Path A). New host packages arrive as new `Op` values, never new imports; a future wire-level break would ship as `env_v2.*` without removing v1. Distinct from the `compiler<->host` interface embedders declare (see [host packages](/reference/writing-modules#path-c-host-capability)), embedders aren't bound by the 6-import limit here.
6
+
> **Sealed contract, plugin ABI v1.** Every signature, op code, tag, and error kind here is the public contract for CDN-distributed `.wasm` plugin modules (Path A). New host packages arrive as new `Op` values, never new imports; a future wire-level break would ship as `env_v2.*` without removing v1. Distinct from the `compiler<->host` interface embedders declare (see [host packages](/reference/writing-modules#path-b-host-capability)), embedders aren't bound by the 6-import limit here.
7
7
8
8
A `.wasm` module imported via `from "<url>" import <names>` follows the contract below. Handle-based API: the host owns all values, the guest sees only opaque `u32` handles, and one universal dispatch primitive (`edge_op`) covers every operation. New types, methods, and language features become available to existing modules with no ABI change.
|`out`| Pointer (in **guest** linear memory) where the guest writes ONE handle for the return value. |
23
23
| return |`0` = success, `1` = error (host pulls the error via `edge_take_error` immediately). |
24
24
25
-
`argv` handles are host-owned and live for the call. Handles the guest creates via `edge_encode` or `edge_op` are guest-owned until released, the guest must `edge_release` each before returning, except the one written into `*out`.
25
+
`argv` handles are host-owned and live for the call. Handles the guest creates via `edge_encode` or `edge_op` are guest-owned until released: the guest must `edge_release` each before returning, except the one written into `*out`.
`__edge_alloc` lets the host stage `argv` arrays in guest linear memory before invoking each export.
40
40
41
-
`__edge_abi_version` returns the wire-format version (currently `1`). The host MUST read this once at instantiation and refuse unknown versions, otherwise a v2 host would silently decode garbage from a v1 module. (At v1 every loader targets 1 so the bundled `compiler.wasm` shim does not yet read the symbol; the check becomes load-bearing when v2 ships.)
41
+
`__edge_abi_version` returns the wire-format version (currently `1`). The host MUST read this once at instantiation and refuse unknown versions; otherwise a v2 host would silently decode garbage from a v1 module. (At v1 every loader targets 1 so the bundled `compiler.wasm` shim does not yet read the symbol; the check becomes load-bearing when v2 ships.)
42
42
43
43
The reference `wasm-pdk` crate emits both symbols automatically. `EDGE_ABI_VERSION` lives in the shared `wasm-abi` crate (no_std, zero deps) so host and every PDK read the same value.
44
44
@@ -103,7 +103,7 @@ Stash an error visible after the guest returns `1`, used when an error did not o
103
103
|`NewSet`| 12 | construct set from `argv` items; unhashable item -> error |
`Op::Iter` materialises the receiver into a List handle (set sorted via `vm.sort_set_items`; dict yields keys; str splits to single-char strings); `Op::IterNext` advances it. `NewDict` / `NewList` construct empty composites; `NewTuple` / `NewSet` / `NewFrozenSet` construct from the `argv` items in one call. `TypeOf` returns names matching the Python builtin: `"int"`, `"float"`, `"str"`, `"bytes"`, `"list"`, `"dict"`, `"set"`, `"tuple"`, `"NoneType"`, `"bool"`, `"object"` (user instance), etc. Values `14..u32::MAX` reserved, old hosts return `1` with `kind=Runtime`.
106
+
`Op::Iter` materialises the receiver into a List handle (set sorted via `vm.sort_set_items`; dict yields keys; str splits to single-char strings); `Op::IterNext` advances it. `NewDict` / `NewList` construct empty composites; `NewTuple` / `NewSet` / `NewFrozenSet` construct from the `argv` items in one call. `TypeOf` returns names matching the Python builtin: `"int"`, `"float"`, `"str"`, `"bytes"`, `"list"`, `"dict"`, `"set"`, `"tuple"`, `"NoneType"`, `"bool"`, `"object"` (user instance), etc. Values `14..u32::MAX` reserved; old hosts return `1` with `kind=Runtime`.
107
107
108
108
## Tags (for `edge_encode` / `edge_decode`)
109
109
@@ -142,7 +142,7 @@ extern crate alloc;
142
142
usealloc::string::String;
143
143
usewasm_pdk::*;
144
144
145
-
wasm_pdk::module!(); // expands to #[global_allocator] + #[panic_handler]
145
+
wasm_pdk::module!(); // expands to #[global_allocator] + #[panic_handler]
Not on crates.io, depend from GitHub, pinned to a release tag:
253
+
Not on crates.io — depend from GitHub, pinned to a release tag:
254
254
255
255
```toml
256
256
[dependencies]
257
257
wasm-pdk = { git = "https://github.com/dylan-sutton-chavez/edge-python", tag = "v0.1.0" }
258
258
```
259
259
260
-
Cargo resolves `wasm-abi` and `wasm-pdk-macros` transitively. Pinning to a tag (vs `branch = "main"`) gives reproducible builds and a known wire-ABI version, your module compiled against `wasm-pdk vX.Y.Z` is binary-compatible with the `compiler_lib.wasm` of the same release. Bump `tag` + `cargo update -p wasm-pdk` to upgrade. Use `branch = "main"` only for unreleased iteration.
260
+
Cargo resolves `wasm-abi` and `wasm-pdk-macros` transitively. Pinning to a tag (vs `branch = "main"`) gives reproducible builds and a known wire-ABI version — your module compiled against `wasm-pdk vX.Y.Z` is binary-compatible with the `compiler_lib.wasm` of the same release. Bump `tag` + `cargo update -p wasm-pdk` to upgrade. Use `branch = "main"` only for unreleased iteration.
@@ -336,28 +336,28 @@ For `from "<url>" import <names>` with a `.wasm` URL: the host fetches bytes (ve
336
336
337
337
## Constraints and caveats
338
338
339
-
***Refcounted handles.** Guest releases every handle it creates via `edge_encode` / `edge_op` except the one returned through `*out`. Host releases argv.
340
-
***`edge_decode` is primitives-only.** For `list`, `dict`, `set`, instances, use `edge_op` (e.g. `Call recv "items"`, `GetItem recv idx`).
341
-
***Trailing kwargs slot.** Every plugin call carries one extra `u32` after the user's positional argv: handle `0` when the caller passed no `name=value` arguments, otherwise a `dict` handle holding the pairs. The `#[plugin_fn]` macro folds it into a `Kwargs` parameter if declared (`fn foo(a: Handle, kw: Kwargs)`); otherwise it is silently absorbed and the function sees only its positional args.
342
-
***Invoking a caller-supplied callable.** From the guest, `edge_op Call recv "__call__" argv` invokes `recv` directly, lambdas, builtins, classes, bound methods all route through the same dispatch the language uses. Use this to wire Python hooks like `default`, `object_hook`, `parse_int`.
343
-
***Reentrance supported.** A guest's `edge_op` runs while the VM is paused on the script's `CallExtern`. Method dispatch routes through the same `vm/handlers/builtin_methods/` descriptor table the language uses internally, adding a method there makes it visible to existing modules with no recompile.
344
-
***Error-as-status, not panic.** Returning `1` does NOT abort the host, the host pulls the error and raises it as a typed Python exception.
345
-
***Memory ownership.** Host only reads guest linear memory at well-defined copy points. Guest-internal allocations stay private.
339
+
-**Refcounted handles.** Guest releases every handle it creates via `edge_encode` / `edge_op` except the one returned through `*out`. Host releases argv.
340
+
-**`edge_decode` is primitives-only.** For `list`, `dict`, `set`, instances, use `edge_op` (e.g. `Call recv "items"`, `GetItem recv idx`).
341
+
-**Trailing kwargs slot.** Every plugin call carries one extra `u32` after the user's positional argv: handle `0` when the caller passed no `name=value` arguments, otherwise a `dict` handle holding the pairs. The `#[plugin_fn]` macro folds it into a `Kwargs` parameter if declared (`fn foo(a: Handle, kw: Kwargs)`); otherwise it is silently absorbed and the function sees only its positional args.
342
+
-**Invoking a caller-supplied callable.** From the guest, `edge_op Call recv "__call__" argv` invokes `recv` directly — lambdas, builtins, classes, bound methods all route through the same dispatch the language uses. Use this to wire Python hooks like `default`, `object_hook`, `parse_int`.
343
+
-**Reentrance supported.** A guest's `edge_op` runs while the VM is paused on the script's `CallExtern`. Method dispatch routes through the same `vm/handlers/builtin_methods/` descriptor table the language uses internally — adding a method there makes it visible to existing modules with no recompile.
344
+
-**Error-as-status, not panic.** Returning `1` does NOT abort the host — the host pulls the error and raises it as a typed Python exception.
345
+
-**Memory ownership.** Host only reads guest linear memory at well-defined copy points. Guest-internal allocations stay private.
346
346
347
347
## Author conveniences
348
348
349
349
The `wasm-pdk` crate (Plugin Development Kit), bundled in this repo, publishable independently of `compiler.wasm`, provides:
350
350
351
-
*`#[plugin_fn]`, typed Rust function -> wire-conformant export.
352
-
*`#[plugin_const]`, zero-arg fn -> module constant via the `__const_<name>` export convention; the host calls it once at import and binds the value as a module attribute.
353
-
*`#[plugin_class]` / `#[plugin_methods]` / `#[plugin_ctor]`, expose a Rust struct as a Python class via the `__class_<Name>_<method>` export convention.
354
-
*`module!()`, expands to `#[global_allocator]` + `#[panic_handler]`.
355
-
*`FromValue` / `IntoValue` with primitive impls (`i64`, `i128`, `f64`, `bool`, `String`, `&str`, `Bytes`, `Option<T>`, `Handle`). `i64` rejects out-of-range values with `ValueError`; use `i128` for the full range. `Bytes` maps to Python `bytes` over `tag::RAW`.
*`Args`, trailing variadic positional params as borrowed handles; declare it as the last param before any `Kwargs`.
358
-
*`Kwargs`, thin wrapper around the trailing kwargs handle with `get::<T>(name)` for primitive kwargs and `get_handle(name)` for callables, tuples, dicts.
359
-
*`PluginCell<T>`, single-threaded interior mutability cell for static plugin state.
-`#[plugin_fn]`, typed Rust function -> wire-conformant export.
352
+
-`#[plugin_const]`, zero-arg fn -> module constant via the `__const_<name>` export convention; the host calls it once at import and binds the value as a module attribute.
353
+
-`#[plugin_class]` / `#[plugin_methods]` / `#[plugin_ctor]`, expose a Rust struct as a Python class via the `__class_<Name>_<method>` export convention.
354
+
-`module!()`, expands to `#[global_allocator]` + `#[panic_handler]`.
355
+
-`FromValue` / `IntoValue` with primitive impls (`i64`, `i128`, `f64`, `bool`, `String`, `&str`, `Bytes`, `Option<T>`, `Handle`). `i64` rejects out-of-range values with `ValueError`; use `i128` for the full range. `Bytes` maps to Python `bytes` over `tag::RAW`.
-`Args`, trailing variadic positional params as borrowed handles; declare it as the last param before any `Kwargs`.
358
+
-`Kwargs`, thin wrapper around the trailing kwargs handle with `get::<T>(name)` for primitive kwargs and `get_handle(name)` for callables, tuples, dicts.
359
+
-`PluginCell<T>`, single-threaded interior mutability cell for static plugin state.
0 commit comments