|
| 1 | +// SPDX-License-Identifier: CC-BY-SA-4.0 |
| 2 | +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk> |
| 3 | += Consumer example: an extension-shaped host boundary |
| 4 | +:toc: macro |
| 5 | +:icons: font |
| 6 | + |
| 7 | +A complete, minimal consumer of AffineScript — the thing |
| 8 | +link:../../../docs/ON-RAMP.adoc[docs/ON-RAMP.adoc] describes in prose. |
| 9 | + |
| 10 | +It is shaped after the first real consumer request |
| 11 | +(`hyperpolymath/affinescript#771`, from `hyperpolymath/blocky-writer#73`): a |
| 12 | +browser extension whose document engine lives behind a two-function boundary, |
| 13 | +compiled to WebAssembly, with an error taxonomy that has to cross that |
| 14 | +boundary intact. |
| 15 | + |
| 16 | +toc::[] |
| 17 | + |
| 18 | +== Run it |
| 19 | + |
| 20 | +[source,console] |
| 21 | +---- |
| 22 | +$ just on-ramp-example # from the repository root |
| 23 | +# or |
| 24 | +$ ./examples/consumers/extension-boundary/build.sh |
| 25 | +---- |
| 26 | + |
| 27 | +Expected output: |
| 28 | + |
| 29 | +[source,console] |
| 30 | +---- |
| 31 | +Compiling boundary.affine -> boundary.wasm |
| 32 | +Compiled …/src/boundary.affine -> …/dist/boundary.wasm (WASM) |
| 33 | +Running host.mjs |
| 34 | +examples/consumers/extension-boundary: OK |
| 35 | +---- |
| 36 | + |
| 37 | +`build.sh` finds the compiler three ways, so it works in a checkout |
| 38 | +(`_build/default/bin/main.exe`), against a release binary on `PATH` |
| 39 | +(`affinescript`), or with `dune exec`. That is deliberate: a consumer's build |
| 40 | +script should not care which of the three you have. |
| 41 | + |
| 42 | +== The four files, and why each exists |
| 43 | + |
| 44 | +[cols="1,3",options="header"] |
| 45 | +|=== |
| 46 | +| File | What it is |
| 47 | + |
| 48 | +| `src/boundary.affine` |
| 49 | +| The consumer's own module. It declares the host functions it calls with |
| 50 | + `extern fn` (the whole binding story — no compiler change, no IDL, no |
| 51 | + adapter package), wraps them in typed helpers, and never touches a string |
| 52 | + it did not receive from the host. |
| 53 | + |
| 54 | +| `host.mjs` |
| 55 | +| The other half: an import object that satisfies those declarations, a |
| 56 | + `{code, message, context}` payload the host owns, and assertions for the |
| 57 | + contract (success path, failure path, message round-trip, exported |
| 58 | + surface). |
| 59 | + |
| 60 | +| `build.sh` |
| 61 | +| Compile, then run. The "smallest thing that turns `src/*.affine` into |
| 62 | + something that compiles", in executable form. |
| 63 | + |
| 64 | +| `dist/` |
| 65 | +| Build output (gitignored). `boundary.wasm` is what you would ship in an |
| 66 | + extension bundle next to `host.mjs`'s import object. |
| 67 | +|=== |
| 68 | + |
| 69 | +== The parts worth copying |
| 70 | + |
| 71 | +*Declare the boundary before porting bodies.* The `extern fn` block at the top |
| 72 | +of `src/boundary.affine` fixes every type the guest can see. In a real port |
| 73 | +this is the first thing to write, because the compiler's error messages are |
| 74 | +most useful at the seam. |
| 75 | + |
| 76 | +*Codes, not strings.* `bw_detect_blocks` returns an `Int` status; the message |
| 77 | +lives host-side and is reachable byte-by-byte via |
| 78 | +`bw_error_message_len` / `bw_error_message_byte`. The guest never allocates, |
| 79 | +never encodes, and cannot invent an error shape the host does not know. |
| 80 | + |
| 81 | +*Assert the contract in CI.* `host.mjs` is not a smoke test — it asserts that |
| 82 | +`fill` propagates the exact `BW_*` code, that the message round-trips |
| 83 | +byte-for-byte, and that the guest exports the names it advertised. The same |
| 84 | +script runs in this repository's `build` job, so the on-ramp cannot rot. |
| 85 | + |
| 86 | +*Nothing here is browser-specific.* Node is used only to read the file and to |
| 87 | +have an assertion library. `WebAssembly.instantiate(bytes, imports)` with the |
| 88 | +same `imports` object runs unchanged in a Firefox/Chrome extension background |
| 89 | +frame, in Bun, or in Deno. |
| 90 | + |
| 91 | +== What this example deliberately does not do |
| 92 | + |
| 93 | +* *No byte transfer through linear memory.* Parameters are `i32`, so real byte |
| 94 | + work is a `(pointer, length)` pair into the guest's exported `memory`; see |
| 95 | + `tests/codegen/test_net_send.mjs` for a worked host-side write, and |
| 96 | + `tests/codegen/test_file_roundtrip.mjs` for a round-trip. Kept out here so |
| 97 | + the boundary pattern is the only thing in view. |
| 98 | +* *No closures.* Passing a callback to the host uses the |
| 99 | + `[fnId @+0, envPtr @+4]` + `__indirect_function_table` ABI; a complete |
| 100 | + dispatcher is in `tests/codegen/test_closure_indirect_dispatch.mjs` and in |
| 101 | + `packages/affine-vscode/mod.js`. |
| 102 | +* *No bundler, no manifest.* There is no `deno.json`, no `package.json`, no |
| 103 | + `affine.toml`. A consumer's build file is this shell script, because the |
| 104 | + compiler takes a source file and `-o`. |
0 commit comments