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

Commit eef1bf1

Browse files
committed
Host-defined resources: every piece works, the composition crashes
dev/resources/hres.{wit,wat} is a guest that mints a host resource, reads it back through a borrow, and drops it -- the shape every wasi:io interface has. dev/cljwit/spike/host_resource.clj drives it, and the JVM crashes. Committed for how far it gets, which printing from inside each upcall narrows precisely: add_resource, both add_funcs and instantiate succeed mint runs, host_to_any returns no error succeeds peek runs, any_to_host returns the right rep succeeds the destructor upcall fires with that rep succeeds wasmtime_component_func_call crashes in error::source, after the destructor and before it returns So every piece works and the composition does not. Leading suspect: ownership of the `any` that host_to_any produces -- the spike writes it into the result val *and* deletes the host_t it came from, and nothing here has established which of those wasmtime expects. Not a bb task. A task that aborts the JVM is worse than none, so it runs by hand and the docstring says so. Two shapes worth keeping from the attempt: definterface needs ^void for a destructor callback, or findVirtual fails method resolution against a Void/TYPE MethodType; and a resource destructor is a second upcall shape, void (*)(void*, wasmtime_context_t*, uint32_t), distinct from an import's. Claude-Session: https://claude.ai/code/session_01XF5Hfq4Ca2N2XYEzWQQuHt
1 parent bef8de5 commit eef1bf1

4 files changed

Lines changed: 236 additions & 0 deletions

File tree

‎dev/cljwit/spike/host_resource.clj‎

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
(ns cljwit.spike.host-resource
2+
"Can the host *define* a resource the guest imports?
3+
4+
Every `wasi:io` interface hands out a handle, so this is the shape `0017` F
5+
says is inside the host-import unit rather than after it. A map keyed
6+
`iface#func` cannot express a resource, and minting one needs a second
7+
upcall shape — a destructor `void (*)(void *, wasmtime_context_t *, u32)`.
8+
9+
**This does not work yet, and is committed for how far it gets.** Run it
10+
with
11+
12+
clojure -M:dev -m cljwit.spike.host-resource target/hres.component.wasm
13+
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."
30+
(:require [clojure.java.io :as io])
31+
(:import [java.lang.foreign Arena Linker SymbolLookup MemorySegment
32+
FunctionDescriptor ValueLayout]
33+
[java.lang.invoke MethodHandle MethodHandles MethodType]))
34+
35+
(set! *warn-on-reflection* true)
36+
37+
(def ^:private ^java.lang.foreign.AddressLayout ADDR ValueLayout/ADDRESS)
38+
(def ^:private ^java.lang.foreign.ValueLayout$OfByte I8 ValueLayout/JAVA_BYTE)
39+
(def ^:private ^java.lang.foreign.ValueLayout$OfInt I32 ValueLayout/JAVA_INT)
40+
(def ^:private ^java.lang.foreign.ValueLayout$OfLong I64 ValueLayout/JAVA_LONG)
41+
(def ^:private VAL 32)
42+
(def ^:private UNION 8)
43+
(def ^:private KIND-U32 6)
44+
(def ^:private KIND-RESOURCE 21)
45+
(def ^:private TY 7) ; the u32 tag that distinguishes this host type
46+
47+
(definterface Import
48+
(^java.lang.foreign.MemorySegment
49+
call [^java.lang.foreign.MemorySegment data ^java.lang.foreign.MemorySegment ctx
50+
^java.lang.foreign.MemorySegment ty
51+
^java.lang.foreign.MemorySegment args ^long nargs
52+
^java.lang.foreign.MemorySegment res ^long nres]))
53+
54+
(definterface Dtor
55+
(^void call [^java.lang.foreign.MemorySegment data ^java.lang.foreign.MemorySegment ctx
56+
^int rep]))
57+
58+
(defn -main [& _args]
59+
(let [linker (Linker/nativeLinker)
60+
lib (or (System/getenv "CLJWIT_WASMTIME_LIB")
61+
(throw (ex-info "CLJWIT_WASMTIME_LIB unset" {})))]
62+
(with-open [arena (Arena/ofShared)]
63+
(let [lookup (SymbolLookup/libraryLookup ^String lib arena)
64+
fx (fn [nm ret & args]
65+
(let [seg ^MemorySegment (.orElseThrow (.find lookup ^String nm))
66+
ls (into-array java.lang.foreign.MemoryLayout args)]
67+
(.downcallHandle linker seg
68+
^FunctionDescriptor
69+
(if ret (FunctionDescriptor/of ret ls)
70+
(FunctionDescriptor/ofVoid ls))
71+
(into-array java.lang.foreign.Linker$Option []))))
72+
call (fn [^MethodHandle mh & as] (.invokeWithArguments mh ^java.util.List (vec as)))
73+
emsg (fx "wasmtime_error_message" nil ADDR ADDR)
74+
ok! (fn [what v]
75+
(when-not (.equals MemorySegment/NULL ^MemorySegment v)
76+
(let [nm ^MemorySegment (.allocate arena (long 16))]
77+
(call emsg v nm)
78+
(let [n (.get nm I64 (long 0))
79+
p ^MemorySegment (.get nm ADDR (long 8))
80+
b (byte-array n)]
81+
(MemorySegment/copy (.reinterpret p n) I8 0 b 0 (int n))
82+
(throw (ex-info (str what ": " (String. b "UTF-8")) {})))))
83+
v)
84+
cstr (fn [^String t]
85+
(let [b (.getBytes t "UTF-8")
86+
s ^MemorySegment (.allocate arena (long (inc (alength b))))]
87+
(MemorySegment/copy ^bytes b 0 s I8 0 (alength b))
88+
(.set s I8 (long (alength b)) (byte 0))
89+
s))
90+
upcall (fn [obj iface mt desc]
91+
(.upcallStub linker
92+
(.bindTo (.findVirtual (MethodHandles/lookup) iface "call" mt) obj)
93+
desc arena (into-array java.lang.foreign.Linker$Option [])))
94+
IMPORT-MT (MethodType/methodType MemorySegment ^"[Ljava.lang.Class;"
95+
(into-array Class [MemorySegment MemorySegment MemorySegment
96+
MemorySegment Long/TYPE MemorySegment Long/TYPE]))
97+
IMPORT-FD (FunctionDescriptor/of ADDR (into-array java.lang.foreign.MemoryLayout
98+
[ADDR ADDR ADDR ADDR I64 ADDR I64]))
99+
;; reps the host has hidden behind handles, and what happened to them
100+
table (atom {})
101+
dropped (atom [])
102+
next-rep (atom 0)
103+
104+
hnew (fx "wasmtime_component_resource_host_new" ADDR ValueLayout/JAVA_BOOLEAN I32 I32)
105+
hrep (fx "wasmtime_component_resource_host_rep" I32 ADDR)
106+
hdel (fx "wasmtime_component_resource_host_delete" nil ADDR)
107+
h2a (fx "wasmtime_component_resource_host_to_any" ADDR ADDR ADDR ADDR)
108+
a2h (fx "wasmtime_component_resource_any_to_host" ADDR ADDR ADDR ADDR)
109+
110+
engine (call (fx "wasm_engine_new" ADDR))
111+
store (call (fx "wasmtime_store_new" ADDR ADDR ADDR ADDR)
112+
engine MemorySegment/NULL MemorySegment/NULL)
113+
ctx (call (fx "wasmtime_store_context" ADDR ADDR) store)
114+
115+
mint (reify Import
116+
(call [_ _d cx _t args _n res _nr]
117+
(let [v (.get ^MemorySegment (.reinterpret ^MemorySegment args (long VAL))
118+
I32 (long UNION))
119+
rep (swap! next-rep inc)
120+
_ (swap! table assoc rep v)
121+
hr ^MemorySegment (call hnew true (int rep) (int TY))
122+
out ^MemorySegment (.allocate arena ^java.lang.foreign.MemoryLayout ADDR)]
123+
(ok! "host_to_any" (call h2a cx hr out))
124+
(call hdel hr)
125+
(let [o ^MemorySegment (.reinterpret ^MemorySegment res (long VAL))]
126+
(.set o I8 (long 0) (byte KIND-RESOURCE))
127+
(.set o ADDR (long UNION) ^MemorySegment (.get out ADDR (long 0))))
128+
MemorySegment/NULL)))
129+
peek (reify Import
130+
(call [_ _d cx _t args _n res _nr]
131+
(let [a ^MemorySegment (.reinterpret ^MemorySegment args (long VAL))
132+
any ^MemorySegment (.get a ADDR (long UNION))
133+
out ^MemorySegment (.allocate arena ^java.lang.foreign.MemoryLayout ADDR)]
134+
(ok! "any_to_host" (call a2h cx any out))
135+
(let [hr ^MemorySegment (.get out ADDR (long 0))
136+
rep (call hrep hr)
137+
o ^MemorySegment (.reinterpret ^MemorySegment res (long VAL))]
138+
(call hdel hr)
139+
(.set o I8 (long 0) (byte KIND-U32))
140+
(.set o I32 (long UNION) (int (get @table rep -1))))
141+
MemorySegment/NULL)))
142+
dtor (reify Dtor
143+
(call [_ _d _cx rep]
144+
(swap! dropped conj [rep (get @table rep)])
145+
(swap! table dissoc rep)
146+
nil))
147+
148+
bs (.readAllBytes (io/input-stream (or (first *command-line-args*)
149+
"target/hres.component.wasm")))
150+
buf ^MemorySegment (.allocate arena (long (alength ^bytes bs)))
151+
_ (MemorySegment/copy ^bytes bs 0 buf I8 0 (alength ^bytes bs))
152+
cout ^MemorySegment (.allocate arena ^java.lang.foreign.MemoryLayout ADDR)
153+
_ (ok! "component_new" (call (fx "wasmtime_component_new" ADDR ADDR ADDR I64 ADDR)
154+
engine buf (long (alength ^bytes bs)) cout))
155+
comp (.get cout ADDR (long 0))
156+
clink (call (fx "wasmtime_component_linker_new" ADDR ADDR) engine)
157+
root (call (fx "wasmtime_component_linker_root" ADDR ADDR) clink)
158+
iout ^MemorySegment (.allocate arena ^java.lang.foreign.MemoryLayout ADDR)
159+
iname "local:hres/host@0.1.0"
160+
_ (ok! "add_instance" (call (fx "wasmtime_component_linker_instance_add_instance" ADDR ADDR ADDR I64 ADDR)
161+
root (cstr iname) (long (count (.getBytes iname "UTF-8"))) iout))
162+
li (.get iout ADDR (long 0))
163+
rty (call (fx "wasmtime_component_resource_type_new_host" ADDR I32) (int TY))
164+
_ (ok! "add_resource"
165+
(call (fx "wasmtime_component_linker_instance_add_resource" ADDR ADDR ADDR I64 ADDR ADDR ADDR ADDR)
166+
li (cstr "token") (long 5) rty
167+
(upcall dtor Dtor
168+
(MethodType/methodType Void/TYPE ^"[Ljava.lang.Class;"
169+
(into-array Class [MemorySegment MemorySegment Integer/TYPE]))
170+
(FunctionDescriptor/ofVoid (into-array java.lang.foreign.MemoryLayout [ADDR ADDR I32])))
171+
MemorySegment/NULL MemorySegment/NULL))
172+
addf (fx "wasmtime_component_linker_instance_add_func" ADDR ADDR ADDR I64 ADDR ADDR ADDR)
173+
_ (ok! "add mint" (call addf li (cstr "mint") (long 4)
174+
(upcall mint Import IMPORT-MT IMPORT-FD)
175+
MemorySegment/NULL MemorySegment/NULL))
176+
_ (ok! "add peek" (call addf li (cstr "peek") (long 4)
177+
(upcall peek Import IMPORT-MT IMPORT-FD)
178+
MemorySegment/NULL MemorySegment/NULL))
179+
inst ^MemorySegment (.allocate arena (long 16))
180+
_ (ok! "instantiate" (call (fx "wasmtime_component_linker_instantiate" ADDR ADDR ADDR ADDR ADDR)
181+
clink ctx comp inst))
182+
eidx (call (fx "wasmtime_component_instance_get_export_index" ADDR ADDR ADDR ADDR ADDR I64)
183+
inst ctx MemorySegment/NULL (cstr "run") (long 3))
184+
f ^MemorySegment (.allocate arena (long 24))
185+
_ (when-not (call (fx "wasmtime_component_instance_get_func" ValueLayout/JAVA_BOOLEAN ADDR ADDR ADDR ADDR)
186+
inst ctx eidx f)
187+
(throw (ex-info "no export run" {})))
188+
args ^MemorySegment (.allocate arena (long VAL))
189+
res ^MemorySegment (.allocate arena (long VAL))]
190+
(.set args I8 (long 0) (byte KIND-U32))
191+
(.set args I32 (long UNION) (int 42))
192+
(ok! "func_call" (call (fx "wasmtime_component_func_call" ADDR ADDR ADDR ADDR I64 ADDR I64)
193+
f ctx args (long 1) res (long 1)))
194+
(println "run(42) =" (.get res I32 (long UNION))
195+
" dropped:" (pr-str @dropped)
196+
" table now:" (pr-str @table))))))

‎dev/resources/hres.wat‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
;; The guest half of dev/resources/hres.wit: mint a host resource, read it
2+
;; back through a borrow, and drop it. The drop is what makes the host's
3+
;; destructor callback observable.
4+
(module
5+
(import "local:hres/host@0.1.0" "mint" (func $mint (param i32) (result i32)))
6+
(import "local:hres/host@0.1.0" "peek" (func $peek (param i32) (result i32)))
7+
(import "local:hres/host@0.1.0" "[resource-drop]token" (func $drop (param i32)))
8+
(memory (export "memory") 1)
9+
(func (export "cabi_realloc") (param i32 i32 i32 i32) (result i32) (i32.const 0))
10+
(func (export "run") (param $v i32) (result i32)
11+
(local $h i32) (local $r i32)
12+
(local.set $h (call $mint (local.get $v)))
13+
(local.set $r (call $peek (local.get $h)))
14+
(call $drop (local.get $h))
15+
(local.get $r)))

‎dev/resources/hres.wit‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// A component that imports a *resource* from the host, which is what every
2+
// wasi:io interface does and what a map keyed `iface#func` cannot express
3+
// (`0017` F).
4+
package local:hres@0.1.0;
5+
interface host {
6+
resource token { }
7+
mint: func(v: u32) -> token;
8+
peek: func(t: borrow<token>) -> u32;
9+
}
10+
world w { import host; export run: func(v: u32) -> u32; }

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,21 @@ 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.
186+
172187
## Alternatives rejected
173188
174189
- **Imports on the artifact, sharing one set of host functions.** Wrong the

0 commit comments

Comments
 (0)