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

Commit 69b4ff0

Browse files
committed
Reflection honors the delete contracts it had been leaking against
The pinned headers require three deallocations reflection never made: wasmtime_component_type_delete for every wasmtime_component_type (one leaked per instantiate -- one per request, in the intended shape), wasmtime_component_item_delete for every _nth-filled item (leaked per export and import walked, in four walks), and wasmtime_component_resource_type_delete for both the reflected clones and the host-registered types (add_resource borrows: the 47.0.1 c-api takes &ty and copies, checked in crates/c-api/src/component/linker.rs). The one prior delete incident here went the other way -- val_delete on a result aborts the JVM on the second call (0014 E) -- so the new deletes are exercised by a repetition test: 50 describes, 50 instantiates and 20 resource-defining instantiates, all past the second-call threshold where that class of bug surfaces. Claude-Session: https://claude.ai/code/session_01XF5Hfq4Ca2N2XYEzWQQuHt
1 parent 6ab6391 commit 69b4ff0

2 files changed

Lines changed: 93 additions & 41 deletions

File tree

‎src/cljwit/host.clj‎

Lines changed: 66 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@
225225
:rty-new-host (b "wasmtime_component_resource_type_new_host" ADDR I32)
226226
:rty-equal (b "wasmtime_component_resource_type_equal" ValueLayout/JAVA_BOOLEAN ADDR ADDR)
227227
:rty-clone (b "wasmtime_component_resource_type_clone" ADDR ADDR)
228+
:rty-delete (b "wasmtime_component_resource_type_delete" nil ADDR)
228229
:hres-new (b "wasmtime_component_resource_host_new" ADDR ValueLayout/JAVA_BOOLEAN I32 I32)
229230
:hres-rep (b "wasmtime_component_resource_host_rep" I32 ADDR)
230231
:hres-delete (b "wasmtime_component_resource_host_delete" nil ADDR)
@@ -988,16 +989,21 @@
988989
(when (invoke nth-f ty (.-ptr e) (long i) pp lp item)
989990
(let [nm (read-name pp lp)
990991
k (item-kind item)
991-
of (.get item ADDR (long ITEM-OF))]
992-
(cond
993-
(= ITEM-FUNC k)
994-
[{:key (if prefix (str prefix "#" nm) nm)
995-
:path (if prefix [prefix nm] [nm])
996-
:sig (reflect-func api arena of)}]
997-
998-
;; One level: WIT does not nest interfaces.
999-
(and (= ITEM-IFACE k) (nil? prefix))
1000-
(walk of (:iface-nth api) (:iface-count api) nm)))))]
992+
of (.get item ADDR (long ITEM-OF))
993+
r (cond
994+
(= ITEM-FUNC k)
995+
[{:key (if prefix (str prefix "#" nm) nm)
996+
:path (if prefix [prefix nm] [nm])
997+
:sig (reflect-func api arena of)}]
998+
999+
;; One level: WIT does not nest interfaces.
1000+
(and (= ITEM-IFACE k) (nil? prefix))
1001+
(walk of (:iface-nth api) (:iface-count api) nm))]
1002+
;; Every _nth-filled item owns its union pointer
1003+
;; and "must be deallocated" (types/component.h)
1004+
;; — after `of` has been fully consumed above.
1005+
(invoke (:item-delete api) item)
1006+
r)))]
10011007
(into [] (comp (keep one) cat)
10021008
(range (invoke count-f ty (.-ptr e))))))]
10031009
(walk ct (:export-nth api) (:export-count api) nil)))
@@ -1014,20 +1020,25 @@
10141020
(when (invoke (:import-nth api) ct (.-ptr e) (long i) pp lp item)
10151021
(let [nm (read-name pp lp)
10161022
k (kind)
1017-
of (.get item ADDR (long ITEM-OF))]
1018-
(cond
1019-
(= ITEM-FUNC k) [[nm (reflect-func api arena of)]]
1020-
(= ITEM-IFACE k)
1021-
(let [[ip il] (name-pair arena)
1022-
sub ^MemorySegment (.allocate arena (long ITEM))]
1023-
(into []
1024-
(keep (fn [j]
1025-
(when (invoke (:iface-nth api) of (.-ptr e) (long j) ip il sub)
1026-
(when (= ITEM-FUNC (bit-and (long (.get sub I8 (long 0))) 0xFF))
1027-
[(str nm "#" (read-name ip il))
1028-
(reflect-func api arena (.get sub ADDR (long ITEM-OF)))]))))
1029-
(range (invoke (:iface-count api) of (.-ptr e)))))
1030-
:else nil)))))
1023+
of (.get item ADDR (long ITEM-OF))
1024+
r (cond
1025+
(= ITEM-FUNC k) [[nm (reflect-func api arena of)]]
1026+
(= ITEM-IFACE k)
1027+
(let [[ip il] (name-pair arena)
1028+
sub ^MemorySegment (.allocate arena (long ITEM))]
1029+
(into []
1030+
(keep (fn [j]
1031+
(when (invoke (:iface-nth api) of (.-ptr e) (long j) ip il sub)
1032+
(let [f? (= ITEM-FUNC (bit-and (long (.get sub I8 (long 0))) 0xFF))
1033+
r (when f?
1034+
[(str nm "#" (read-name ip il))
1035+
(reflect-func api arena (.get sub ADDR (long ITEM-OF)))])]
1036+
(invoke (:item-delete api) sub)
1037+
r))))
1038+
(range (invoke (:iface-count api) of (.-ptr e)))))
1039+
:else nil)]
1040+
(invoke (:item-delete api) item)
1041+
r))))
10311042
cat)
10321043
(range (invoke (:import-count api) ct (.-ptr e))))))
10331044

@@ -1054,18 +1065,24 @@
10541065
(comp
10551066
(keep (fn [i]
10561067
(when (invoke (:import-nth api) ct (.-ptr e) (long i) pp lp item)
1057-
(let [nm (read-name pp lp)]
1058-
(when (= ITEM-IFACE (bit-and (long (.get item I8 (long 0))) 0xFF))
1059-
(let [of (.get item ADDR (long ITEM-OF))
1060-
[ip il] (name-pair arena)
1061-
sub ^MemorySegment (.allocate arena (long ITEM))]
1062-
(into []
1063-
(keep (fn [j]
1064-
(when (invoke (:iface-nth api) of (.-ptr e) (long j) ip il sub)
1065-
(when (= ITEM-RESOURCE (bit-and (long (.get sub I8 (long 0))) 0xFF))
1066-
[(str nm "#" (read-name ip il))
1067-
(invoke (:rty-clone api) (.get sub ADDR (long ITEM-OF)))]))))
1068-
(range (invoke (:iface-count api) of (.-ptr e))))))))))
1068+
(let [nm (read-name pp lp)
1069+
r (when (= ITEM-IFACE (bit-and (long (.get item I8 (long 0))) 0xFF))
1070+
(let [of (.get item ADDR (long ITEM-OF))
1071+
[ip il] (name-pair arena)
1072+
sub ^MemorySegment (.allocate arena (long ITEM))]
1073+
(into []
1074+
(keep (fn [j]
1075+
(when (invoke (:iface-nth api) of (.-ptr e) (long j) ip il sub)
1076+
(let [res? (= ITEM-RESOURCE (bit-and (long (.get sub I8 (long 0))) 0xFF))
1077+
;; Clone before the item goes: the clone is owned.
1078+
r (when res?
1079+
[(str nm "#" (read-name ip il))
1080+
(invoke (:rty-clone api) (.get sub ADDR (long ITEM-OF)))])]
1081+
(invoke (:item-delete api) sub)
1082+
r))))
1083+
(range (invoke (:iface-count api) of (.-ptr e))))))]
1084+
(invoke (:item-delete api) item)
1085+
r))))
10691086
cat)
10701087
(range (invoke (:import-count api) ct (.-ptr e))))]
10711088
;; Cluster: each entry is {:names [...] :type ptr}.
@@ -1130,7 +1147,12 @@
11301147
(cstr arena rname)
11311148
(long (count (.getBytes ^String rname "UTF-8")))
11321149
rty stub MemorySegment/NULL MemorySegment/NULL))))
1133-
nil))))))))
1150+
;; add_resource borrows the type (c-api takes &ty and copies),
1151+
;; so the host still owns it, and the header requires delete.
1152+
(invoke (:rty-delete api) rty)
1153+
nil))))
1154+
(doseq [cls classes]
1155+
(invoke (:rty-delete api) (:type cls)))))))
11341156

11351157
(defn- define-imports!
11361158
"Registers each supplied Clojure function with the linker. Missing imports
@@ -1234,6 +1256,9 @@
12341256
rt {:ctx ctx :api api :registry registry :pending pending}
12351257
found (reflect-exports api arena e ct)
12361258
sigs (into {} (map (fn [x] [(:key x) (:sig x)])) found)
1259+
;; The component type "must be deallocated" (component.h), and this is
1260+
;; its last use. One leaked per instantiate is one per request.
1261+
_ (invoke (:type-delete api) ct)
12371262
index-of (fn [path]
12381263
(when (empty? path)
12391264
(throw (ex-info "an export with no path" {:cljwit/error :internal})))
@@ -1283,10 +1308,10 @@
12831308
_ (closed! (.-closed e) "engine")
12841309
api (.-api e)]
12851310
(with-open [arena (Arena/ofConfined)]
1286-
(let [ct (invoke (:comp-type api) (.-ptr art))]
1287-
(into {}
1288-
(map (fn [{:keys [key sig]}] [key sig]))
1289-
(reflect-exports api arena e ct))))))
1311+
(let [ct (invoke (:comp-type api) (.-ptr art))
1312+
found (reflect-exports api arena e ct)]
1313+
(invoke (:type-delete api) ct)
1314+
(into {} (map (fn [{:keys [key sig]}] [key sig])) found)))))
12901315

12911316
(defn drop-failures
12921317
"Anything a `:drop` threw, and clears them. `0018` E: a destructor that

‎test/cljwit/host_test.clj‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,33 @@
160160
(host/exports i)))))))))
161161
(finally (.delete ^File c))))))
162162

163+
(deftest reflection-cleanup-survives-repetition
164+
;; type_delete, item_delete and resource_type_delete are required by the
165+
;; pinned headers and fire on every reflection. The val_delete incident
166+
;; (0014 E) surfaced on the *second* call, so this loops well past it.
167+
(if-not lib
168+
(println "CLJWIT_WASMTIME_LIB unset — skipping cleanup-repetition test")
169+
(let [c (build-component!)
170+
f (build-component! "hres")]
171+
(try
172+
(with-open [e (host/engine)]
173+
(with-open [a (host/compile e (io/file c))]
174+
(dotimes [_ 50] (host/describe a))
175+
(dotimes [_ 50] (.close ^java.lang.AutoCloseable (host/instantiate a)))
176+
(is (= {:params [["v" :string]] :result :string}
177+
(get (host/describe a) "echo-string"))
178+
"reflection still answers after the churn"))
179+
(with-open [a2 (host/compile e (io/file f))]
180+
(dotimes [_ 20]
181+
(.close ^java.lang.AutoCloseable
182+
(host/instantiate
183+
a2 {:resources {"local:hres/host@0.1.0#token" {:drop identity}}
184+
:imports {"local:hres/host@0.1.0#mint" (fn [v] {:n v})
185+
"local:hres/host@0.1.0#peek" (fn [t] (:n t))
186+
"local:hres/host@0.1.0#eat" (fn [t] (:n @t))}})))
187+
(is true "20 resource-defining instantiates deleted their types")))
188+
(finally (.delete ^File c) (.delete ^File f))))))
189+
163190
(deftest lifetimes-are-explicit
164191
(if-not lib
165192
(println "CLJWIT_WASMTIME_LIB unset — skipping host lifetime test")

0 commit comments

Comments
 (0)