Skip to content
This repository was archived by the owner on Aug 12, 2026. It is now read-only.

Commit 99fa006

Browse files
committed
Host-defined resources work: the destructor returns an error, not void
bb spike-hres run(42) = 42 dropped: [[1 42]] table now: {} A guest mints a host resource, reads it back through a borrow, and drops it; the host's destructor fires with the right rep. Every wasi:io interface has this shape. The bug was one line of a typedef: typedef wasmtime_error_t *(*wasmtime_component_resource_destructor_t)( void *, wasmtime_context_t *, uint32_t); I declared the upcall void. wasmtime then read a garbage register as an error pointer and crashed walking its source chain -- *after* the destructor had already run correctly, which is why every probe printed success and the crash looked like it was about ownership of the `any`. Two predictions were falsified on the way there, both about ownership: removing the mint-side host_delete changed nothing, and a null destructor is not allowed at all. My earlier survey printed exactly the line that names the return type and I read only the parameters, because the return type was on the line above and my grep window cut it. That is the third time today the same shape has cost something -- reading a truncated view and treating it as the whole -- after two greps whose [a-z_] class silently excluded digits. definterface also needs the return type spelled: a ^void method against a Void/TYPE MethodType fails findVirtual outright, which is the friendly version of the same mistake. So every shape S1's stop condition names has now been reached at least once. What is left is the cljwit.host API for host resources: a resource is a type, not a function, so the iface#func map has nowhere to put it. Claude-Session: https://claude.ai/code/session_01XF5Hfq4Ca2N2XYEzWQQuHt
1 parent eef1bf1 commit 99fa006

3 files changed

Lines changed: 44 additions & 38 deletions

File tree

‎bb.edn‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,16 @@
127127
(run "cc" "-O2" "-shared" "-fPIC" "-o" so "dev/spike/noop7.c")
128128
(run "clojure" "-M:dev" "-m" "cljwit.spike.ffm-shape")))}
129129

130+
spike-hres {:doc "Can the host define a resource the guest imports? See 0017 F."
131+
:task (do (babashka.fs/create-dirs "target")
132+
(run "wasm-tools" "parse" "dev/resources/hres.wat" "-o" "target/hres.core.wasm")
133+
(run "wasm-tools" "component" "embed" "dev/resources/hres.wit"
134+
"target/hres.core.wasm" "-o" "target/hres.embed.wasm")
135+
(run "wasm-tools" "component" "new" "target/hres.embed.wasm"
136+
"-o" "target/hres.component.wasm")
137+
(run "clojure" "-M:dev" "-m" "cljwit.spike.host-resource"
138+
"target/hres.component.wasm"))}
139+
130140
spike-import {:doc "Can a Wasm guest call back into Clojure? See 0014's falsifier."
131141
:task (do (babashka.fs/create-dirs "target")
132142
(run "wasm-tools" "parse" "dev/resources/imp.wat" "-o" "target/imp.core.wasm")

‎dev/cljwit/spike/host_resource.clj‎

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,15 @@
66
`iface#func` cannot express a resource, and minting one needs a second
77
upcall shape — a destructor `void (*)(void *, wasmtime_context_t *, u32)`.
88
9-
**This does not work yet, and is committed for how far it gets.** Run it
10-
with
9+
The premise check, not the feature:
1110
12-
clojure -M:dev -m cljwit.spike.host-resource target/hres.component.wasm
11+
bb spike-hres
1312
14-
and the JVM crashes inside libwasmtime's `error::source`. It is deliberately
15-
*not* a `bb` task, because a task that aborts the JVM is worse than none.
16-
17-
What is narrowed, by printing from inside each upcall:
18-
19-
* `add_resource`, both `add_func`s and `instantiate` all succeed.
20-
* `mint` runs; `host_to_any` returns no error.
21-
* `peek` runs; `any_to_host` returns no error and the rep comes back.
22-
* the destructor upcall runs, with the right rep.
23-
* the crash is *inside* `wasmtime_component_func_call`, after the
24-
destructor and before it returns.
25-
26-
So every piece works and the composition does not. The leading suspect is
27-
ownership of the `any` that `host_to_any` produces: this code writes it into
28-
the result val and also deletes the `host_t` it came from, and nothing here
29-
has established which of those wasmtime expects."
13+
The one thing that is not guessable: **the destructor returns
14+
`wasmtime_error_t *`, not `void`.** Declaring it void makes wasmtime read a
15+
garbage register as an error pointer and crash walking its source chain,
16+
after the destructor has already run correctly. A null destructor is not
17+
allowed either -- wasmtime calls it."
3018
(:require [clojure.java.io :as io])
3119
(:import [java.lang.foreign Arena Linker SymbolLookup MemorySegment
3220
FunctionDescriptor ValueLayout]
@@ -52,8 +40,11 @@
5240
^java.lang.foreign.MemorySegment res ^long nres]))
5341

5442
(definterface Dtor
55-
(^void call [^java.lang.foreign.MemorySegment data ^java.lang.foreign.MemorySegment ctx
56-
^int rep]))
43+
;; Returns wasmtime_error_t*, not void. Declaring it void makes wasmtime read
44+
;; a garbage register as an error pointer and crash walking it.
45+
(^java.lang.foreign.MemorySegment
46+
call [^java.lang.foreign.MemorySegment data ^java.lang.foreign.MemorySegment ctx
47+
^int rep]))
5748

5849
(defn -main [& _args]
5950
(let [linker (Linker/nativeLinker)
@@ -143,7 +134,7 @@
143134
(call [_ _d _cx rep]
144135
(swap! dropped conj [rep (get @table rep)])
145136
(swap! table dissoc rep)
146-
nil))
137+
MemorySegment/NULL))
147138

148139
bs (.readAllBytes (io/input-stream (or (first *command-line-args*)
149140
"target/hres.component.wasm")))
@@ -165,9 +156,9 @@
165156
(call (fx "wasmtime_component_linker_instance_add_resource" ADDR ADDR ADDR I64 ADDR ADDR ADDR ADDR)
166157
li (cstr "token") (long 5) rty
167158
(upcall dtor Dtor
168-
(MethodType/methodType Void/TYPE ^"[Ljava.lang.Class;"
159+
(MethodType/methodType MemorySegment ^"[Ljava.lang.Class;"
169160
(into-array Class [MemorySegment MemorySegment Integer/TYPE]))
170-
(FunctionDescriptor/ofVoid (into-array java.lang.foreign.MemoryLayout [ADDR ADDR I32])))
161+
(FunctionDescriptor/of ADDR (into-array java.lang.foreign.MemoryLayout [ADDR ADDR I32])))
171162
MemorySegment/NULL MemorySegment/NULL))
172163
addf (fx "wasmtime_component_linker_instance_add_func" ADDR ADDR ADDR I64 ADDR ADDR ADDR)
173164
_ (ok! "add mint" (call addf li (cstr "mint") (long 4)

‎doc/design/0017-host-imports.md‎

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -169,20 +169,25 @@ shape, and handles minted per call. `resource_host_to_any` is how one becomes
169169
a val — and it panics outside a call scope, which is the landmine already
170170
recorded above.
171171
172-
**Tried, 2026-07-30, and it does not work yet.**
173-
`dev/cljwit/spike/host_resource.clj` drives
174-
`dev/resources/hres.{wit,wat}` — a guest that mints a host resource, reads it
175-
through a `borrow`, and drops it. Every individual piece succeeds:
176-
`add_resource`, both `add_func`s and `instantiate`; `mint` with
177-
`host_to_any` returning no error; `peek` with `any_to_host` returning the right
178-
rep; and the destructor upcall firing with that rep. **The crash is inside
179-
`wasmtime_component_func_call` itself, after the destructor and before it
180-
returns**, in `error::source`.
181-
182-
Leading suspect: ownership of the `any` that `host_to_any` produces. The spike
183-
writes it into the result val *and* deletes the `host_t` it came from, and
184-
nothing has established which of those wasmtime expects. The spike is not a
185-
`bb` task, because a task that aborts the JVM is worse than none.
172+
**It works** (`bb spike-hres`): a guest mints a host resource, reads it through
173+
a `borrow`, and drops it, and the host's destructor fires with the right rep.
174+
175+
The one thing that is not guessable, and that cost a JVM crash to find: **the
176+
destructor returns `wasmtime_error_t *`, not `void`** —
177+
178+
```c
179+
typedef wasmtime_error_t *(*wasmtime_component_resource_destructor_t)(
180+
void *, wasmtime_context_t *, uint32_t);
181+
```
182+
183+
Declaring it `void` in the FFM upcall makes wasmtime read a garbage register as
184+
an error pointer and crash walking its source chain, **after** the destructor
185+
has already run correctly — so every printed probe says the callback works. A
186+
null destructor is not allowed either; wasmtime calls it.
187+
188+
`definterface` also needs the return type spelled: a `^void` method against a
189+
`Void/TYPE` `MethodType` fails `findVirtual` outright, which is the *friendly*
190+
version of the same mistake.
186191

187192
## Alternatives rejected
188193

0 commit comments

Comments
 (0)