|
| 1 | +# 0018 — Host-defined resources in `cljwit.host` |
| 2 | + |
| 3 | +**Status:** proposed · 2026-07-30 · the mechanism is `bb spike-hres`; this is |
| 4 | +the API over it, and no code exists |
| 5 | + |
| 6 | +## The question |
| 7 | + |
| 8 | +`0017` A keys imports by `iface#func`. A **resource is a type, not a |
| 9 | +function**, so that map has nowhere to put one — and every `wasi:io` interface |
| 10 | +declares one, so this is not an edge case. |
| 11 | + |
| 12 | +What is settled, measured 2026-07-30: |
| 13 | + |
| 14 | +- **The round trip works** (`bb spike-hres`): a guest mints a host resource, |
| 15 | + reads it through a `borrow`, and drops it; the host's destructor fires with |
| 16 | + the right rep. |
| 17 | +- **A host resource is a `u32` type tag plus a destructor upcall**, and the |
| 18 | + destructor returns `wasmtime_error_t *`, not `void`. Getting that wrong |
| 19 | + crashes the JVM *after* the callback has run correctly. |
| 20 | +- **A handle carries only a `u32` rep.** `resource_host_new(owned, rep, ty)` |
| 21 | + mints one; `any_to_host` + `host_rep` reads it back. There is nowhere to put |
| 22 | + a Clojure value, so the host must keep the mapping. |
| 23 | +- **The marshaller can tell a host resource from a guest one by type.** A |
| 24 | + valtype's union carries `own`/`borrow` → `resource_type_t *`, and |
| 25 | + `resource_type_equal` compares it against the types the host registered. |
| 26 | + |
| 27 | +## The decision |
| 28 | + |
| 29 | +### A. Resources are declared in their own map, keyed by the same WIT name |
| 30 | + |
| 31 | +```clojure |
| 32 | +(host/instantiate |
| 33 | + art {:resources {"local:hres/host@0.1.0#token" {:drop (fn [v] (close! v))}} |
| 34 | + :imports {"local:hres/host@0.1.0#mint" (fn [v] (open-thing v)) |
| 35 | + "local:hres/host@0.1.0#peek" (fn [t] (:n t))}}) |
| 36 | +``` |
| 37 | + |
| 38 | +The identity stays the exact WIT name (`0014` B, `0017` A). What changes is |
| 39 | +that a resource is a different *kind* of thing, so it gets a different key in |
| 40 | +the options map rather than a different value shape under the same one. |
| 41 | + |
| 42 | +### B. A host resource is any Clojure value; the host owns the rep table |
| 43 | + |
| 44 | +The import that returns one returns an ordinary value. `cljwit.host` assigns it |
| 45 | +a `u32` rep, keeps `rep → value` for the instance, and mints the handle. A |
| 46 | +`borrow` parameter of that type arrives as the value, not as a rep or a handle. |
| 47 | + |
| 48 | +The guest's drop removes the entry and calls `:drop` with the value, if given. |
| 49 | + |
| 50 | +### C. The table is per-instance and dies with it |
| 51 | + |
| 52 | +Reps are only meaningful inside one store. The table lives beside the handle |
| 53 | +registry `0016` D already keeps, and `Instance/close` clears it — after |
| 54 | +closing outstanding guest handles, before deleting the store. |
| 55 | + |
| 56 | +### D. `:drop` may not throw |
| 57 | + |
| 58 | +It runs inside wasmtime's destructor trampoline, which is the same place `0017` |
| 59 | +D's rule applies: a JVM exception unwinding through native frames exits the VM. |
| 60 | +Anything thrown is caught and converted, and the instance is marked dead, as an |
| 61 | +import's exception already is. |
| 62 | + |
| 63 | +## Why |
| 64 | + |
| 65 | +**A, because a resource is not a function and pretending otherwise costs |
| 66 | +later.** `0017` F recorded the shape; the alternative — one map whose values |
| 67 | +are sometimes functions and sometimes maps — makes a typo in the value shape |
| 68 | +into a silent misregistration. |
| 69 | + |
| 70 | +**B, because the rep is a `u32` and nothing else fits through it.** Any design |
| 71 | +where the Clojure value crosses the boundary is impossible; the only question |
| 72 | +is who keeps the table, and the host is the only party that sees both sides. |
| 73 | + |
| 74 | +**D, because the failure is the worst one available**, and it is the same |
| 75 | +failure `0017` D already pays a `try` to prevent. |
| 76 | + |
| 77 | +## Alternatives rejected |
| 78 | + |
| 79 | +- **One `:imports` map with a value shape that says "resource".** See A. |
| 80 | +- **Exposing the rep to Clojure.** Smallest, and it makes a capability an |
| 81 | + integer anyone can forge — the same argument `0016` A made against integer |
| 82 | + handles for guest resources. |
| 83 | +- **A global rep table.** Reps are store-scoped; a global one would collide |
| 84 | + between instances and leak. |
| 85 | +- **Letting `:drop` propagate.** It cannot: the frame it would unwind through |
| 86 | + is native. |
| 87 | +- **Requiring the user to mint handles explicitly**, with a `host/resource` |
| 88 | + constructor called inside the import. Honest about what happens, and it |
| 89 | + makes every `wasi:io`-shaped import carry ceremony that the type already |
| 90 | + determines. |
| 91 | + |
| 92 | +## What would falsify this |
| 93 | + |
| 94 | +- **A resource type used by two instances of the same artifact.** The type is |
| 95 | + registered on the *linker*, which `0014` C creates per instantiate, so each |
| 96 | + instance has its own — unverified, and if linkers turn out shareable the |
| 97 | + table's scope is wrong. |
| 98 | +- **`borrow` parameters of a host type arriving as something other than a |
| 99 | + handle to a live rep** — the spike read one, but only inside the call that |
| 100 | + minted it. A borrow held across two guest calls is untested. |
| 101 | +- **The type comparison being too coarse.** `resource_type_equal` is the only |
| 102 | + discriminator; if two distinct host types compare equal, values would be |
| 103 | + handed to the wrong `:drop`. |
| 104 | +- **`own` parameters** — a guest handing a host resource *back*. The spike only |
| 105 | + covers the guest receiving one. |
| 106 | +- **The cost.** A rep table lookup per borrow and a `swap!` per mint, on a call |
| 107 | + that `0013` prices at ~0.4 µs. Unmeasured. |
| 108 | + |
| 109 | +## Resources |
| 110 | + |
| 111 | +- `bb spike-hres` and `dev/resources/hres.{wit,wat}` — the mechanism. |
| 112 | +- `0017` — the import API this extends, and the exception rule D inherits. |
| 113 | +- `0016` — the guest-resource side, and the argument against integer handles. |
0 commit comments