|
38 | 38 | (def ^:private ITEM 24) ; wasmtime_component_item_t, kind at 0, union at 8 |
39 | 39 | (def ^:private ITEM-OF 8) |
40 | 40 | (def ^:private ITEM-FUNC 3) ; WASMTIME_COMPONENT_ITEM_COMPONENT_FUNC |
41 | | -(def ^:private ITEM-IFACE 1) ; ...COMPONENT_INSTANCE — a WIT interface |
| 41 | +(def ^:private ITEM-IFACE 1) |
| 42 | +(def ^:private ITEM-RESOURCE 4) ; ...COMPONENT_INSTANCE — a WIT interface |
42 | 43 | (def ^:private VT-OF 8) ; ...its union: one pointer to the specific type |
43 | 44 | (def ^:private RES-OK 8) ; wasmtime_component_valresult_t {bool; val *} |
44 | 45 | (def ^:private RES-VAL 16) |
|
176 | 177 | (into-array java.lang.foreign.Linker$Option []))) |
177 | 178 | :linker-root (b "wasmtime_component_linker_root" ADDR ADDR) |
178 | 179 | :li-add-inst (b "wasmtime_component_linker_instance_add_instance" ADDR ADDR ADDR I64 ADDR) |
179 | | - :li-add-func (b "wasmtime_component_linker_instance_add_func" ADDR ADDR ADDR I64 ADDR ADDR ADDR)})) |
| 180 | + :li-add-func (b "wasmtime_component_linker_instance_add_func" ADDR ADDR ADDR I64 ADDR ADDR ADDR) |
| 181 | + :li-add-res (b "wasmtime_component_linker_instance_add_resource" ADDR ADDR ADDR I64 ADDR ADDR ADDR ADDR) |
| 182 | + :rty-new-host (b "wasmtime_component_resource_type_new_host" ADDR I32) |
| 183 | + :rty-equal (b "wasmtime_component_resource_type_equal" ValueLayout/JAVA_BOOLEAN ADDR ADDR) |
| 184 | + :rty-clone (b "wasmtime_component_resource_type_clone" ADDR ADDR) |
| 185 | + :hres-new (b "wasmtime_component_resource_host_new" ADDR ValueLayout/JAVA_BOOLEAN I32 I32) |
| 186 | + :hres-rep (b "wasmtime_component_resource_host_rep" I32 ADDR) |
| 187 | + :hres-delete (b "wasmtime_component_resource_host_delete" nil ADDR) |
| 188 | + :hres-to-any (b "wasmtime_component_resource_host_to_any" ADDR ADDR ADDR ADDR) |
| 189 | + :any-to-hres (b "wasmtime_component_resource_any_to_host" ADDR ADDR ADDR ADDR)})) |
180 | 190 |
|
181 | 191 | (defn- cstr [^Arena arena ^String t] |
182 | 192 | (let [b (.getBytes t "UTF-8") |
|
562 | 572 | :variant (mapcat (fn [[_ t]] (unsupported t)) (:cases tree)) |
563 | 573 | [(:kind tree)]))) |
564 | 574 |
|
| 575 | +(definterface ResourceDtor |
| 576 | + ;; Returns wasmtime_error_t*, not void. Declaring it void makes wasmtime read |
| 577 | + ;; a garbage register as an error pointer and crash walking it, *after* the |
| 578 | + ;; destructor has already run correctly (0018). |
| 579 | + (^java.lang.foreign.MemorySegment |
| 580 | + call [^java.lang.foreign.MemorySegment data ^java.lang.foreign.MemorySegment ctx |
| 581 | + ^int rep])) |
| 582 | + |
565 | 583 | (definterface ImportCallback |
566 | 584 | (^java.lang.foreign.MemorySegment |
567 | 585 | call [^java.lang.foreign.MemorySegment data |
|
584 | 602 | [tree] |
585 | 603 | (cond |
586 | 604 | (nil? tree) nil |
587 | | - (keyword? tree) (when-not (SCALAR tree) [tree]) |
| 605 | + (keyword? tree) (when-not (or (SCALAR tree) (#{:own :borrow} tree)) [tree]) |
588 | 606 | :else (case (:kind tree) |
589 | 607 | :list (unimportable (:element tree)) |
590 | 608 | :tuple (mapcat unimportable (:types tree)) |
|
600 | 618 | "One FFM upcall stub for one host import. Lifts the arguments, calls `f`, |
601 | 619 | lowers the result, and converts anything thrown into a wasmtime error -- |
602 | 620 | a JVM exception unwinding through native frames exits the VM (0017 D)." |
603 | | - [api ^Arena arena sig f nm dead] |
604 | | - (let [lifts (mapv (fn [[_ t]] (lift-fn t nil)) (:params sig)) |
| 621 | + [api ^Arena arena sig f nm dead rtab] |
| 622 | + ;; A host resource in an import's signature is one this instance declared, |
| 623 | + ;; so `:own`/`:borrow` here mean the rep table rather than a guest handle. |
| 624 | + ;; One host resource type per instance for now — the tag is fixed at 1. |
| 625 | + (let [hres-lift (fn [^MemorySegment seg] |
| 626 | + (let [any ^MemorySegment (.get seg ADDR (long UNION)) |
| 627 | + out ^MemorySegment (.allocate arena ^java.lang.foreign.MemoryLayout ADDR)] |
| 628 | + (ok! api "any_to_host" (invoke (:any-to-hres api) (:ctx rtab) any out)) |
| 629 | + (let [hr ^MemorySegment (.get out ADDR (long 0)) |
| 630 | + rep (invoke (:hres-rep api) hr)] |
| 631 | + (invoke (:hres-delete api) hr) |
| 632 | + (first (get @(:table rtab) rep))))) |
| 633 | + lifts (mapv (fn [[_ t]] (if (contains? #{:own :borrow} t) |
| 634 | + hres-lift |
| 635 | + (lift-fn t nil))) |
| 636 | + (:params sig)) |
605 | 637 | rt (:result sig) |
606 | | - lower (some-> rt lower-fn) |
| 638 | + lower (cond |
| 639 | + (nil? rt) nil |
| 640 | + (= :own rt) |
| 641 | + (fn [_ ^MemorySegment seg v] |
| 642 | + (let [rep (swap! (:next rtab) inc) |
| 643 | + _ (swap! (:table rtab) assoc rep [v]) |
| 644 | + hr ^MemorySegment (invoke (:hres-new api) true (int rep) (int 1)) |
| 645 | + out ^MemorySegment (.allocate arena ^java.lang.foreign.MemoryLayout ADDR)] |
| 646 | + (ok! api "host_to_any" (invoke (:hres-to-any api) (:ctx rtab) hr out)) |
| 647 | + (invoke (:hres-delete api) hr) |
| 648 | + (.set seg I8 (long 0) (byte RESOURCE)) |
| 649 | + (.set seg ADDR (long UNION) ^MemorySegment (.get out ADDR (long 0))))) |
| 650 | + :else (lower-fn rt)) |
607 | 651 | ;; 0017 C: wasmtime frees what the callback writes, so every byte of a |
608 | 652 | ;; result comes from malloc. Measured: an Arena pointer survives one |
609 | 653 | ;; call and aborts the process at 2000. |
|
873 | 917 | cat) |
874 | 918 | (range (invoke (:import-count api) ct (.-ptr e)))))) |
875 | 919 |
|
| 920 | +(defn- iface-instance |
| 921 | + "wasmtime refuses a second `add_instance` for one interface name, so both |
| 922 | + resources and functions go through one cache." |
| 923 | + [api ^Arena arena root cache iface] |
| 924 | + (or (get @cache iface) |
| 925 | + (let [out ^MemorySegment (.allocate arena ^java.lang.foreign.MemoryLayout ADDR)] |
| 926 | + (ok! api "linker_instance_add_instance" |
| 927 | + (invoke (:li-add-inst api) root (cstr arena iface) |
| 928 | + (long (count (.getBytes ^String iface "UTF-8"))) out)) |
| 929 | + (let [v (.get out ADDR (long 0))] (swap! cache assoc iface v) v)))) |
| 930 | + |
| 931 | +(defn- reflect-import-resources |
| 932 | + "Every `ITEM_RESOURCE` in the import list, clustered by type. One type can |
| 933 | + carry several names — a `use`d resource appears once per interface that |
| 934 | + uses it, and `type headers = fields` appears again — and reflected items |
| 935 | + compare equal when they are the same type (`0018` A)." |
| 936 | + [api ^Arena arena ^Engine e ^MemorySegment ct] |
| 937 | + (let [[pp lp] (name-pair arena) |
| 938 | + item ^MemorySegment (.allocate arena (long ITEM)) |
| 939 | + found (into [] |
| 940 | + (comp |
| 941 | + (keep (fn [i] |
| 942 | + (when (invoke (:import-nth api) ct (.-ptr e) (long i) pp lp item) |
| 943 | + (let [nm (read-name pp lp)] |
| 944 | + (when (= ITEM-IFACE (bit-and (long (.get item I8 (long 0))) 0xFF)) |
| 945 | + (let [of (.get item ADDR (long ITEM-OF)) |
| 946 | + [ip il] (name-pair arena) |
| 947 | + sub ^MemorySegment (.allocate arena (long ITEM))] |
| 948 | + (into [] |
| 949 | + (keep (fn [j] |
| 950 | + (when (invoke (:iface-nth api) of (.-ptr e) (long j) ip il sub) |
| 951 | + (when (= ITEM-RESOURCE (bit-and (long (.get sub I8 (long 0))) 0xFF)) |
| 952 | + [(str nm "#" (read-name ip il)) |
| 953 | + (invoke (:rty-clone api) (.get sub ADDR (long ITEM-OF)))])))) |
| 954 | + (range (invoke (:iface-count api) of (.-ptr e)))))))))) |
| 955 | + cat) |
| 956 | + (range (invoke (:import-count api) ct (.-ptr e))))] |
| 957 | + ;; Cluster: each entry is {:names [...] :type ptr}. |
| 958 | + (reduce (fn [acc [nm ty]] |
| 959 | + (if-let [k (first (keep-indexed |
| 960 | + (fn [i cls] |
| 961 | + (when (invoke (:rty-equal api) (:type cls) ty) i)) |
| 962 | + acc))] |
| 963 | + (update-in acc [k :names] conj nm) |
| 964 | + (conj acc {:names [nm] :type ty}))) |
| 965 | + [] found))) |
| 966 | + |
| 967 | +(defn- define-resources! |
| 968 | + "Registers one host resource type per cluster, under every name in it." |
| 969 | + [api ^Arena arena ^Engine e clink ^MemorySegment ct resources table drop-errs cache] |
| 970 | + (when (seq resources) |
| 971 | + (let [classes (reflect-import-resources api arena e ct) |
| 972 | + known (set (mapcat :names classes)) |
| 973 | + extra (remove known (keys resources))] |
| 974 | + (when (seq extra) |
| 975 | + (throw (ex-info (str "no such resource: " (pr-str (vec extra))) |
| 976 | + {:cljwit/error :no-such-resource |
| 977 | + :cljwit/extra (vec extra) |
| 978 | + :cljwit/resources known}))) |
| 979 | + (let [root (invoke (:linker-root api) clink)] |
| 980 | + (doseq [[i cls] (map-indexed vector classes)] |
| 981 | + (let [named (filterv resources (:names cls))] |
| 982 | + (when (< 1 (count named)) |
| 983 | + (throw (ex-info (str "one type, two keys: " (pr-str named) |
| 984 | + " name the same resource, which has one destructor") |
| 985 | + {:cljwit/error :duplicate-resource :cljwit/names named}))) |
| 986 | + (when-let [nm (first named)] |
| 987 | + (let [drop-fn (:drop (get resources nm)) |
| 988 | + tag (int (inc i)) |
| 989 | + dtor (reify ResourceDtor |
| 990 | + (call [_ _d _cx rep] |
| 991 | + (try |
| 992 | + (let [[v] (get @table rep)] |
| 993 | + (swap! table dissoc rep) |
| 994 | + (when drop-fn (drop-fn v))) |
| 995 | + ;; 0018 E: a guest drop must still succeed. |
| 996 | + (catch Throwable t (swap! drop-errs conj t))) |
| 997 | + MemorySegment/NULL)) |
| 998 | + stub (.upcallStub (Linker/nativeLinker) |
| 999 | + (.bindTo (.findVirtual (MethodHandles/lookup) ResourceDtor "call" |
| 1000 | + (MethodType/methodType |
| 1001 | + MemorySegment ^"[Ljava.lang.Class;" |
| 1002 | + (into-array Class [MemorySegment MemorySegment Integer/TYPE]))) |
| 1003 | + dtor) |
| 1004 | + (FunctionDescriptor/of ADDR (into-array java.lang.foreign.MemoryLayout |
| 1005 | + [ADDR ADDR I32])) |
| 1006 | + arena (into-array java.lang.foreign.Linker$Option [])) |
| 1007 | + rty (invoke (:rty-new-host api) tag)] |
| 1008 | + ;; Every name in the class, one tag: two names under distinct |
| 1009 | + ;; tags fails instantiate with a message naming neither. |
| 1010 | + (doseq [n (:names cls)] |
| 1011 | + (let [[iface rname] (str/split n #"#" 2)] |
| 1012 | + (ok! api "linker_instance_add_resource" |
| 1013 | + (invoke (:li-add-res api) (iface-instance api arena root cache iface) |
| 1014 | + (cstr arena rname) |
| 1015 | + (long (count (.getBytes ^String rname "UTF-8"))) |
| 1016 | + rty stub MemorySegment/NULL MemorySegment/NULL)))) |
| 1017 | + nil)))))))) |
| 1018 | + |
876 | 1019 | (defn- define-imports! |
877 | 1020 | "Registers each supplied Clojure function with the linker. Missing imports |
878 | 1021 | are wasmtime's to report at instantiate, in a message that names both the |
879 | 1022 | interface and the function (`0017` B); what the host checks is the other |
880 | 1023 | direction, because a key nobody needs is a typo." |
881 | | - [api ^Arena arena ^Engine e _ctx clink ^MemorySegment ct imports dead] |
| 1024 | + [api ^Arena arena ^Engine e _ctx clink ^MemorySegment ct imports dead rtab cache] |
882 | 1025 | (when (seq imports) |
883 | 1026 | (let [needed (reflect-imports api arena e ct) |
884 | 1027 | extra (remove (set (keys needed)) (keys imports))] |
|
887 | 1030 | {:cljwit/error :no-such-import |
888 | 1031 | :cljwit/extra (vec extra) |
889 | 1032 | :cljwit/imports (set (keys needed))}))) |
890 | | - (let [root (invoke (:linker-root api) clink) |
891 | | - ifaces (atom {})] |
| 1033 | + (let [root (invoke (:linker-root api) clink)] |
892 | 1034 | (doseq [[nm f] imports] |
893 | 1035 | (let [sig (get needed nm) |
894 | 1036 | bad (distinct (mapcat unimportable |
|
898 | 1040 | {:cljwit/error :unsupported-type |
899 | 1041 | :cljwit/import nm :cljwit/kinds (vec bad)}))) |
900 | 1042 | (let [[iface fname] (if (str/includes? nm "#") (str/split nm #"#" 2) [nil nm]) |
901 | | - li (if (nil? iface) |
902 | | - root |
903 | | - (or (get @ifaces iface) |
904 | | - (let [out ^MemorySegment (.allocate arena ^java.lang.foreign.MemoryLayout ADDR)] |
905 | | - (ok! api "linker_instance_add_instance" |
906 | | - (invoke (:li-add-inst api) root (cstr arena iface) |
907 | | - (long (count (.getBytes ^String iface "UTF-8"))) out)) |
908 | | - (let [v (.get out ADDR (long 0))] |
909 | | - (swap! ifaces assoc iface v) |
910 | | - v)))) |
911 | | - stub (import-stub api arena sig f nm dead)] |
| 1043 | + li (if (nil? iface) root (iface-instance api arena root cache iface)) |
| 1044 | + stub (import-stub api arena sig f nm dead rtab)] |
912 | 1045 | (ok! api "linker_instance_add_func" |
913 | 1046 | (invoke (:li-add-func api) li (cstr arena fname) |
914 | 1047 | (long (count (.getBytes ^String fname "UTF-8"))) |
|
947 | 1080 | "Instantiates a compiled component. Cheap — tens of microseconds — so a |
948 | 1081 | store per request is the intended shape." |
949 | 1082 | (^Instance [^Artifact art] (instantiate art {})) |
950 | | - (^Instance [^Artifact art {:keys [imports wasi]}] |
| 1083 | + (^Instance [^Artifact art {:keys [imports wasi resources]}] |
951 | 1084 | (closed! (.-closed art) "artifact") |
952 | 1085 | (let [^Engine e (.-engine art) |
953 | 1086 | _ (closed! (.-closed e) "engine") |
|
961 | 1094 | ;; instance has to know it is dead rather than let every later call |
962 | 1095 | ;; report a trap with the cause gone. |
963 | 1096 | dead (atom nil) |
964 | | - _ (define-imports! api arena e ctx clink ct imports dead) |
| 1097 | + ;; 0018 B: the rep table, and the failures a `:drop` collected. |
| 1098 | + rtab {:ctx ctx :table (atom {}) :next (atom 0) :errs (atom [])} |
| 1099 | + li-cache (atom {}) |
| 1100 | + _ (define-resources! api arena e clink ct resources |
| 1101 | + (:table rtab) (:errs rtab) li-cache) |
| 1102 | + _ (define-imports! api arena e ctx clink ct imports dead rtab li-cache) |
965 | 1103 | _ (when wasi (enable-wasi! api arena ctx clink wasi)) |
966 | 1104 | inst ^MemorySegment (.allocate arena (long INSTANCE)) |
967 | 1105 | _ (ok! api "linker_instantiate" |
|
0 commit comments