lecodes-web-runtime 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,156 @@
1
+ // The wasm adapter of the generated web bridges — the runtime/native `js/args.h` of the web target
2
+ // (scripts/gen-bridges.ts, the TS emitter). A generated method turns its JS arguments into what the
3
+ // engine's exported C function takes (numbers as they are, strings and arrays copied into the heap,
4
+ // a fetch buffer id into a heap copy of the bytes) and turns the result back (a heap array into a
5
+ // Float32Array, a C string into a string, a struct into an object). The module is one of the
6
+ // runtime/web/native profiles: every engine shares its heap, so this adapter is the only place that
7
+ // knows about `HEAPF32`, `_malloc` and scratch memory.
8
+ //
9
+ // Scratch: every call runs inside `begin()` / `end(sp)` — a bump allocator over one heap block that
10
+ // is reset when the call returns (an argument copy never outlives the call). A copy that does not
11
+ // fit the block is `_malloc`ed and freed by `end`. Nothing here holds a heap view across a call that
12
+ // can grow the heap: the `HEAP*` getters read the module's CURRENT views every time.
13
+
14
+ /** The exports a creator-web profile provides (runtime/web/native/CMakeLists.txt). */
15
+ export type WasmModule = {
16
+ HEAPU8: Uint8Array; HEAPU16: Uint16Array; HEAP32: Int32Array; HEAPU32: Uint32Array; HEAPF32: Float32Array; HEAPF64: Float64Array
17
+ _malloc(bytes: number): number
18
+ _free(ptr: number): void
19
+ UTF8ToString(ptr: number, maxBytes?: number): string
20
+ stringToUTF8(s: string, ptr: number, maxBytes: number): void
21
+ lengthBytesUTF8(s: string): number
22
+ [fn: `_${string}`]: (...args: any[]) => any
23
+ } & Record<string, any>
24
+
25
+ /** What the host lends the bridges: the fetch store behind the `bytes` alias of the contract. */
26
+ export type WasmHost = {
27
+ getBuffer(systemId: number): ArrayBuffer | Uint8Array | null | undefined
28
+ }
29
+
30
+ const SCRATCH_BYTES = 256 * 1024
31
+
32
+ export class Wasm {
33
+ readonly M: WasmModule
34
+ readonly host: WasmHost
35
+ private readonly base: number
36
+ private sp: number
37
+ private readonly heapAllocs: number[] = []
38
+ private readonly marks: number[] = []
39
+
40
+ constructor(module: WasmModule, host: WasmHost) {
41
+ this.M = module
42
+ this.host = host
43
+ this.base = module._malloc(SCRATCH_BYTES)
44
+ this.sp = this.base
45
+ }
46
+
47
+ // ---- scratch ----------------------------------------------------------------------------------
48
+ /** Open a call's scratch frame; `end(sp)` releases everything allocated after it. */
49
+ begin(): number { this.marks.push(this.heapAllocs.length); return this.sp }
50
+ end(sp: number): void {
51
+ this.sp = sp
52
+ const from = this.marks.pop() ?? 0
53
+ while (this.heapAllocs.length > from) this.M._free(this.heapAllocs.pop()!)
54
+ }
55
+ /** `bytes` of scratch, 8-aligned; falls back to the heap when the block is full. */
56
+ alloc(bytes: number): number {
57
+ const n = (Math.max(bytes, 1) + 7) & ~7
58
+ if (this.sp + n <= this.base + SCRATCH_BYTES) { const p = this.sp; this.sp += n; return p }
59
+ const p = this.M._malloc(n)
60
+ this.heapAllocs.push(p)
61
+ return p
62
+ }
63
+
64
+ // ---- arguments in ---------------------------------------------------------------------------------
65
+ /** A NUL-terminated UTF-8 copy of `s`. */
66
+ str(s: string): number {
67
+ const n = this.M.lengthBytesUTF8(s) + 1
68
+ const p = this.alloc(n)
69
+ this.M.stringToUTF8(s, p, n)
70
+ return p
71
+ }
72
+ /** A heap copy of a typed array (or a plain number list as floats); `elem` = bytes per element. */
73
+ array(values: ArrayLike<number>, elem: 1 | 2 | 4 | 8, float = false): number {
74
+ const length = values.length
75
+ const p = this.alloc(length * elem)
76
+ if (length === 0) return p
77
+ if (ArrayBuffer.isView(values)) {
78
+ const v = values as unknown as ArrayBufferView
79
+ this.M.HEAPU8.set(new Uint8Array(v.buffer, v.byteOffset, v.byteLength), p)
80
+ } else if (float) this.M.HEAPF32.set(values as ArrayLike<number>, p >> 2)
81
+ else if (elem === 4) this.M.HEAPU32.set(values as ArrayLike<number>, p >> 2)
82
+ else if (elem === 2) this.M.HEAPU16.set(values as ArrayLike<number>, p >> 1)
83
+ else this.M.HEAPU8.set(values as ArrayLike<number>, p)
84
+ return p
85
+ }
86
+ f32(values: ArrayLike<number>): number { return this.array(values, 4, true) }
87
+ u8(values: ArrayLike<number>): number { return this.array(values, 1) }
88
+ u16(values: ArrayLike<number>): number { return this.array(values, 2) }
89
+ u32(values: ArrayLike<number>): number { return this.array(values, 4) }
90
+ i32(values: ArrayLike<number>): number { return this.array(values, 4) }
91
+ /** The bytes behind a fetch buffer id, copied into the heap; null when the store has nothing. */
92
+ bytes(systemId: number): { ptr: number, size: number } | null {
93
+ const buf = this.host.getBuffer(systemId)
94
+ if (!buf) return null
95
+ const view = buf instanceof Uint8Array ? buf : new Uint8Array(buf)
96
+ if (view.byteLength === 0) return null
97
+ const p = this.alloc(view.byteLength)
98
+ this.M.HEAPU8.set(view, p)
99
+ return { ptr: p, size: view.byteLength }
100
+ }
101
+
102
+ // ---- results out ------------------------------------------------------------------------------------
103
+ /** Copy `n` heap elements at `ptr` into `into` (min(n, into.length) of them) — an engine-written array. */
104
+ readInto(ptr: number, n: number, into: ArrayBufferView, elem: 1 | 2 | 4): void {
105
+ const count = Math.min(n, into.byteLength / elem)
106
+ if (count <= 0) return
107
+ new Uint8Array(into.buffer, into.byteOffset, count * elem).set(this.M.HEAPU8.subarray(ptr, ptr + count * elem))
108
+ }
109
+ /** A NEW Float32Array of `n` floats read at `ptr`. */
110
+ newF32(ptr: number, n: number): Float32Array {
111
+ const out = new Float32Array(Math.max(n, 0))
112
+ if (n > 0) out.set(this.M.HEAPF32.subarray(ptr >> 2, (ptr >> 2) + n))
113
+ return out
114
+ }
115
+ /** A C string (borrowed pointer) as a JS string; 0 → "". */
116
+ cstr(ptr: number, maxBytes?: number): string { return ptr ? this.M.UTF8ToString(ptr, maxBytes) : "" }
117
+ /** A malloc'd C string the engine hands over: read, then freed. */
118
+ cstrFree(ptr: number): string {
119
+ if (!ptr) return ""
120
+ const s = this.M.UTF8ToString(ptr)
121
+ this.M._free(ptr)
122
+ return s
123
+ }
124
+
125
+ /** Zero `bytes` at `p` (a struct the engine fills, a @pack struct before its fields are written). */
126
+ zero(p: number, bytes: number): void { this.M.HEAPU8.fill(0, p, p + bytes) }
127
+ // ---- struct fields (the struct rule's generated readers / the @pack writers) ---------------------------
128
+ wrU8(p: number, v: number): void { this.M.HEAPU8[p] = v }
129
+ wrU16(p: number, v: number): void { this.M.HEAPU16[p >> 1] = v }
130
+ wrI32(p: number, v: number): void { this.M.HEAP32[p >> 2] = v }
131
+ wrU32(p: number, v: number): void { this.M.HEAPU32[p >> 2] = v }
132
+ wrF32(p: number, v: number): void { this.M.HEAPF32[p >> 2] = v }
133
+ wrF64(p: number, v: number): void { this.M.HEAPF64[p >> 3] = v }
134
+ wrI64(p: number, v: number): void { this.M.HEAPU32[p >> 2] = v >>> 0; this.M.HEAP32[(p >> 2) + 1] = Math.floor(v / 4294967296) }
135
+ wrU64(p: number, v: number): void { this.M.HEAPU32[p >> 2] = v >>> 0; this.M.HEAPU32[(p >> 2) + 1] = Math.floor(v / 4294967296) }
136
+ /** `n` structs of `size` bytes starting at `p`, each read by `read`. */
137
+ rdList<T>(p: number, n: number, size: number, read: (q: number) => T): T[] {
138
+ const out: T[] = []
139
+ for (let i = 0; i < n; i++) out.push(read(p + i * size))
140
+ return out
141
+ }
142
+ rdU8(p: number): number { return this.M.HEAPU8[p]! }
143
+ rdU16(p: number): number { return this.M.HEAPU16[p >> 1]! }
144
+ rdI32(p: number): number { return this.M.HEAP32[p >> 2]! }
145
+ rdU32(p: number): number { return this.M.HEAPU32[p >> 2]! }
146
+ rdF32(p: number): number { return this.M.HEAPF32[p >> 2]! }
147
+ rdF64(p: number): number { return this.M.HEAPF64[p >> 3]! }
148
+ rdI64(p: number): number { return this.M.HEAPU32[p >> 2]! + this.M.HEAP32[(p >> 2) + 1]! * 4294967296 }
149
+ rdU64(p: number): number { return this.M.HEAPU32[p >> 2]! + this.M.HEAPU32[(p >> 2) + 1]! * 4294967296 }
150
+ /** `n` numbers of `elem` bytes at `p` (a `float pt[3]` field). */
151
+ rdNumbers(p: number, n: number, rd: (q: number) => number, elem: number): number[] {
152
+ const out: number[] = []
153
+ for (let i = 0; i < n; i++) out.push(rd.call(this, p + i * elem))
154
+ return out
155
+ }
156
+ }
package/src/jsb.ts ADDED
@@ -0,0 +1,494 @@
1
+ // The JS side of runtime/native's wasm arm (runtime/native/src/js/wasm.cpp): the handle table its
2
+ // `jsb::Value`s index, the imports the arm calls (property access, calls, strings, typed arrays,
3
+ // throws), the wrappers that turn an installed handler table into JS functions, and the
4
+ // FinalizationRegistry behind a class finalizer (the owned `Handle`). One instance per module,
5
+ // installed as `Module.jsb` before `creatorWebInit`. Engine-agnostic: a browser or Bun.
6
+ //
7
+ // Values: a number is its IEEE bits (a NaN written canonical — the engine may canonicalize NaNs,
8
+ // so a tagged word never travels as a JS number); anything else is a NaN with the sign bit set, a
9
+ // TAG in bits 16–19 of the high word and the payload in the low word (wasm-support.h). Handles are
10
+ // refcounted: the arm's `dup` / `free` are `dup` / `release` here; objects and functions are deduped
11
+ // through a WeakMap so the arm's `same` holds; at zero references the slot is dropped and the value
12
+ // is the engine's to collect.
13
+ import type { WasmModule } from "./gen/wasm"
14
+
15
+ const TAG_UNDEFINED = 1, TAG_NULL = 2, TAG_BOOL = 3, TAG_OBJECT = 4, TAG_FUNCTION = 5, TAG_STRING = 6, TAG_ARRAY = 7, TAG_OTHER = 8, TAG_EXCEPTION = 15
16
+ const HI_BASE = 0xFFF00000
17
+ const hiOf = (tag: number) => (HI_BASE | (tag << 16)) >>> 0
18
+ const HI_UNDEFINED = hiOf(TAG_UNDEFINED), HI_NULL = hiOf(TAG_NULL), HI_BOOL = hiOf(TAG_BOOL), HI_EXCEPTION = hiOf(TAG_EXCEPTION)
19
+ const tagOf = (hi: number) => ((hi & 0xFFF00000) >>> 0) === HI_BASE ? (hi >>> 16) & 0xF : 0
20
+
21
+ /** A world the arm evaluates in: its global object (the bridges are installed on it) + evaluator. */
22
+ export type JsbWorld = {
23
+ global: object
24
+ evaluate(code: string, filename: string): unknown
25
+ }
26
+
27
+ export type JsbOptions = {
28
+ /** The boot world (context 1). */
29
+ world: JsbWorld
30
+ /** A fresh world for `_creatorApp.run` (a vm context headless); absent = the boot world is reused. */
31
+ newWorld?: () => JsbWorld
32
+ /** The host tables the generated forwarders call (`Module.jsb.host.<table>.<slot>`). */
33
+ host: Record<string, unknown>
34
+ }
35
+
36
+ const ARG_STACK_BYTES = 64 * 1024
37
+
38
+ /** A settle group: the heap callbacks of one host slot call, released together after the first settle. */
39
+ type Group = { ptrs: number[], done: boolean }
40
+
41
+ export class Jsb {
42
+ readonly M: WasmModule
43
+ readonly host: Record<string, unknown>
44
+ private out = 0
45
+ // the table: index → value (0 unused); refs[i] = references the arm holds; free = released slots
46
+ private table: unknown[] = [undefined]
47
+ private refs: number[] = [0]
48
+ private freeSlots: number[] = []
49
+ private objIndex = new WeakMap<object, number>()
50
+ private pending: unknown = undefined
51
+ private hasPending = false
52
+ private worlds = new Map<number, JsbWorld>()
53
+ private nextWorld = 2
54
+ private readonly newWorld?: () => JsbWorld
55
+ // the argument stack the wrappers write call arguments into (nested calls bump it)
56
+ private argBase = 0
57
+ private argSp = 0
58
+ private readonly scratch = new DataView(new ArrayBuffer(8))
59
+ private readonly classRegistry: FinalizationRegistry<{ ref: WeakRef<object>, cls: number, opaque: number }>
60
+ // Every live class instance as a WeakRef: `gc()` sweeps them so a finalizer lands SYNCHRONOUSLY
61
+ // inside the arm's runGC (QuickJS semantics: `ownedStats` = collect + drain + count), instead of
62
+ // on the FinalizationRegistry's later task — which still covers the objects a natural collection takes.
63
+ private readonly tracked = new Map<object, { ref: WeakRef<object>, cls: number, opaque: number }>()
64
+ private readonly fnRegistry: FinalizationRegistry<number>
65
+ private readonly opaques = new WeakMap<object, { cls: number, opaque: number }>()
66
+
67
+ constructor(M: WasmModule, options: JsbOptions) {
68
+ this.M = M
69
+ this.host = options.host
70
+ this.worlds.set(1, options.world)
71
+ this.newWorld = options.newWorld
72
+ this.argBase = M._malloc(ARG_STACK_BYTES)
73
+ this.argSp = this.argBase
74
+ this.classRegistry = new FinalizationRegistry((token) => {
75
+ if (!this.tracked.delete(token)) return // swept already by gc()
76
+ this.M._jsbFinalize(token.cls, token.opaque)
77
+ })
78
+ this.fnRegistry = new FinalizationRegistry((k) => { this.M._jsbFunctionGone(k) })
79
+ }
80
+
81
+ // ---- the arm's hooks --------------------------------------------------------------------------
82
+ _init(out: number) { this.out = out }
83
+
84
+ // ---- encoding ---------------------------------------------------------------------------------
85
+ /** Encode `v` at heap address `p` (8 bytes). A handle takes ONE reference (the caller owns it);
86
+ * the result is that handle's index, 0 for an immediate (a boolean's payload is NOT an index —
87
+ * returning it once released slot 1 under an unrelated function). */
88
+ private write(p: number, v: unknown): number {
89
+ const M = this.M
90
+ if (typeof v === "number") {
91
+ M.HEAPF64[p >> 3] = v !== v ? NaN : v // the engine writes a canonical NaN for NaN
92
+ return 0
93
+ }
94
+ let lo = 0, hi: number, idx = 0
95
+ if (v === undefined) hi = HI_UNDEFINED
96
+ else if (v === null) hi = HI_NULL
97
+ else if (typeof v === "boolean") { hi = HI_BOOL; lo = v ? 1 : 0 }
98
+ else {
99
+ lo = idx = this.alloc(v)
100
+ hi = hiOf(typeof v === "function" ? TAG_FUNCTION : typeof v === "string" ? TAG_STRING : Array.isArray(v) ? TAG_ARRAY : typeof v === "object" ? TAG_OBJECT : TAG_OTHER)
101
+ }
102
+ M.HEAPU32[p >> 2] = lo
103
+ M.HEAPU32[(p >> 2) + 1] = hi
104
+ return idx
105
+ }
106
+ private writeOut(v: unknown) { this.write(this.out, v) }
107
+ private writeOutException() { const M = this.M; M.HEAPU32[this.out >> 2] = 0; M.HEAPU32[(this.out >> 2) + 1] = HI_EXCEPTION }
108
+ /** Decode a (lo, hi) word pair; a handle's value is BORROWED (the reference stays). */
109
+ private decode(lo: number, hi: number): unknown {
110
+ const tag = tagOf(hi)
111
+ switch (tag) {
112
+ case 0: { this.scratch.setUint32(0, lo, true); this.scratch.setUint32(4, hi, true); return this.scratch.getFloat64(0, true) }
113
+ case TAG_UNDEFINED: case TAG_EXCEPTION: return undefined
114
+ case TAG_NULL: return null
115
+ case TAG_BOOL: return lo !== 0
116
+ default: return this.table[lo]
117
+ }
118
+ }
119
+ private readAt(p: number): unknown {
120
+ const M = this.M
121
+ return this.decode(M.HEAPU32[p >> 2], M.HEAPU32[(p >> 2) + 1])
122
+ }
123
+ /** A `value` parameter of a host slot (the generated forwarders): the JS value whose word is at
124
+ * `p` — the app's own value, not a copy; undefined for a null address. */
125
+ valueAt(p: number): unknown { return p ? this.readAt(p) : undefined }
126
+ /** Read the result slot and TAKE the reference the arm handed over (an OWNED result). */
127
+ private takeOut(): { value: unknown, threw: boolean } {
128
+ const M = this.M
129
+ const lo = M.HEAPU32[this.out >> 2], hi = M.HEAPU32[(this.out >> 2) + 1]
130
+ const tag = tagOf(hi)
131
+ if (tag === TAG_EXCEPTION) return { value: undefined, threw: true }
132
+ const value = this.decode(lo, hi)
133
+ if (tag >= TAG_OBJECT && tag <= TAG_OTHER) this.release(lo)
134
+ return { value, threw: false }
135
+ }
136
+ private args(argc: number, argv: number): unknown[] {
137
+ const out: unknown[] = new Array(argc)
138
+ for (let i = 0; i < argc; i++) out[i] = this.readAt(argv + i * 8)
139
+ return out
140
+ }
141
+
142
+ // ---- the table ----------------------------------------------------------------------------------
143
+ private alloc(v: unknown): number {
144
+ const isObj = typeof v === "object" || typeof v === "function"
145
+ if (isObj) {
146
+ const hit = this.objIndex.get(v as object)
147
+ if (hit !== undefined) { this.refs[hit]++; return hit }
148
+ }
149
+ let i: number
150
+ if (this.freeSlots.length > 0) { i = this.freeSlots.pop()!; this.table[i] = v; this.refs[i] = 1 }
151
+ else { i = this.table.length; this.table.push(v); this.refs.push(1) }
152
+ if (isObj) this.objIndex.set(v as object, i)
153
+ return i
154
+ }
155
+ dup(idx: number) { if (idx > 0) this.refs[idx]++ }
156
+ release(idx: number) {
157
+ if (idx <= 0) return
158
+ if (this.refs[idx] <= 0) { console.warn(`[jsb] release of a dead handle ${idx} (${typeof this.table[idx]})`, new Error().stack?.split("\n").slice(2, 6).join("\n")); return }
159
+ if (--this.refs[idx] > 0) return
160
+ const v = this.table[idx]
161
+ if (typeof v === "object" || typeof v === "function") this.objIndex.delete(v as object)
162
+ this.table[idx] = undefined
163
+ this.freeSlots.push(idx)
164
+ }
165
+ /** Live handles (for tests). */
166
+ liveHandles(): number { return this.table.length - 1 - this.freeSlots.length }
167
+
168
+ // ---- worlds -------------------------------------------------------------------------------------
169
+ global(ctx: number) { this.writeOut(this.worlds.get(ctx)?.global ?? this.worlds.get(1)!.global) }
170
+ newContext(): number {
171
+ if (!this.newWorld) return 1
172
+ const id = this.nextWorld++
173
+ this.worlds.set(id, this.newWorld())
174
+ return id
175
+ }
176
+ freeContext(ctx: number) { if (ctx !== 1) this.worlds.delete(ctx) }
177
+ eval(ctx: number, code: string, filename: string) {
178
+ const w = this.worlds.get(ctx) ?? this.worlds.get(1)!
179
+ try { this.writeOut(w.evaluate(code, filename)) }
180
+ catch (e) { this.pending = e; this.hasPending = true; this.writeOutException() }
181
+ }
182
+
183
+ // ---- objects ----------------------------------------------------------------------------------
184
+ get(lo: number, hi: number, key: string) {
185
+ const o = this.decode(lo, hi) as any
186
+ if (o === undefined || o === null) { this.writeOut(undefined); return }
187
+ try { this.writeOut(o[key]) }
188
+ catch (e) { this.pending = e; this.hasPending = true; this.writeOutException() }
189
+ }
190
+ getIndex(lo: number, hi: number, i: number) {
191
+ const o = this.decode(lo, hi) as any
192
+ if (o === undefined || o === null) { this.writeOut(undefined); return }
193
+ try { this.writeOut(o[i]) }
194
+ catch (e) { this.pending = e; this.hasPending = true; this.writeOutException() }
195
+ }
196
+ has(lo: number, hi: number, key: string): boolean {
197
+ const o = this.decode(lo, hi)
198
+ return (typeof o === "object" || typeof o === "function") && o !== null && key in (o as object)
199
+ }
200
+ set(obj: number, key: string, lo: number, hi: number) {
201
+ const o = this.table[obj] as any
202
+ if (o === undefined || o === null) return
203
+ o[key] = this.decode(lo, hi)
204
+ }
205
+ setIndex(obj: number, i: number, lo: number, hi: number) {
206
+ const o = this.table[obj] as any
207
+ if (o === undefined || o === null) return
208
+ o[i] = this.decode(lo, hi)
209
+ }
210
+ keys(lo: number, hi: number, countOut: number): number {
211
+ const M = this.M
212
+ const o = this.decode(lo, hi)
213
+ const ks = (typeof o === "object" || typeof o === "function") && o !== null ? Object.keys(o as object) : []
214
+ let total = 1
215
+ for (const k of ks) total += M.lengthBytesUTF8(k) + 1
216
+ const p = M._malloc(total)
217
+ let off = p
218
+ for (const k of ks) { const n = M.lengthBytesUTF8(k) + 1; M.stringToUTF8(k, off, n); off += n }
219
+ M.HEAPU32[countOut >> 2] = ks.length
220
+ return p
221
+ }
222
+ newObject(classId: number, opaque: number) {
223
+ const o: Record<string, unknown> = {}
224
+ if (classId > 0) {
225
+ this.opaques.set(o, { cls: classId, opaque })
226
+ const token = { ref: new WeakRef(o), cls: classId, opaque }
227
+ this.tracked.set(token, token)
228
+ this.classRegistry.register(o, token, token)
229
+ }
230
+ this.writeOut(o)
231
+ }
232
+ newArray() { this.writeOut([]) }
233
+ opaqueOf(idx: number, classId: number): number {
234
+ const o = this.table[idx]
235
+ if (typeof o !== "object" || o === null) return 0
236
+ const e = this.opaques.get(o)
237
+ return e && e.cls === classId ? e.opaque : 0
238
+ }
239
+ json(lo: number, hi: number) {
240
+ let s: unknown
241
+ try { s = JSON.stringify(this.decode(lo, hi)) } catch { s = undefined }
242
+ this.writeOut(s)
243
+ }
244
+ parseJson(text: string) {
245
+ try { this.writeOut(JSON.parse(text)) }
246
+ catch (e) { this.pending = e; this.hasPending = true; this.writeOutException() }
247
+ }
248
+
249
+ // ---- strings + numbers ---------------------------------------------------------------------------
250
+ newString(s: string) { this.writeOut(s) }
251
+ str(lo: number, hi: number, lenOut: number): number {
252
+ const M = this.M
253
+ const v = this.decode(lo, hi)
254
+ let s: string
255
+ try { s = typeof v === "string" ? v : String(v) } catch { return 0 } // a Symbol
256
+ const n = M.lengthBytesUTF8(s)
257
+ const p = M._malloc(n + 1)
258
+ M.stringToUTF8(s, p, n + 1)
259
+ M.HEAPU32[lenOut >> 2] = n
260
+ return p
261
+ }
262
+ logString(lo: number, hi: number): number {
263
+ const M = this.M
264
+ const v = this.decode(lo, hi)
265
+ let s: string
266
+ try { s = String(v) } catch { s = "" }
267
+ if (v instanceof Error && typeof v.stack === "string" && v.stack.length > 0) {
268
+ const stack = v.stack.startsWith(s) ? v.stack.slice(s.length).replace(/^\n/, "") : v.stack
269
+ if (stack.trim().length > 0) s += "\n" + stack.replace(/[\r\n]+$/, "")
270
+ }
271
+ const n = M.lengthBytesUTF8(s)
272
+ const p = M._malloc(n + 1)
273
+ M.stringToUTF8(s, p, n + 1)
274
+ return p
275
+ }
276
+ toNumber(idx: number): number { try { return Number(this.table[idx]) } catch { return NaN } }
277
+ truthy(idx: number): boolean { return !!this.table[idx] }
278
+
279
+ // ---- errors --------------------------------------------------------------------------------------
280
+ newError(message: string) { this.writeOut(new Error(message)) }
281
+ throwNew(kind: number, message: string) {
282
+ this.pending = kind === 1 ? new TypeError(message) : new Error(message)
283
+ this.hasPending = true
284
+ }
285
+ takeException() {
286
+ const e = this.hasPending ? this.pending : undefined
287
+ this.pending = undefined
288
+ this.hasPending = false
289
+ this.writeOut(e)
290
+ }
291
+ private throwPending(): never {
292
+ const e = this.hasPending ? this.pending : new Error("native handler failed")
293
+ this.pending = undefined
294
+ this.hasPending = false
295
+ throw e
296
+ }
297
+
298
+ // ---- calls ---------------------------------------------------------------------------------------
299
+ call(_ctx: number, fn: number, thisLo: number, thisHi: number, argc: number, argv: number) {
300
+ const f = this.table[fn] as (...a: unknown[]) => unknown
301
+ const thisv = this.decode(thisLo, thisHi)
302
+ const args = this.args(argc, argv)
303
+ try { this.writeOut(f.apply(thisv, args)) }
304
+ catch (e) { this.pending = e; this.hasPending = true; this.writeOutException() }
305
+ }
306
+ construct(ctor: number, argc: number, argv: number) {
307
+ const c = this.table[ctor] as new (...a: unknown[]) => unknown
308
+ const args = this.args(argc, argv)
309
+ try { this.writeOut(Reflect.construct(c, args)) }
310
+ catch (e) { this.pending = e; this.hasPending = true; this.writeOutException() }
311
+ }
312
+
313
+ // ---- typed arrays ---------------------------------------------------------------------------------
314
+ typedArray(kind: number, data: number, bytes: number) {
315
+ const src = this.M.HEAPU8.slice(data, data + bytes) // a copy: the array owns its buffer
316
+ const buf = src.buffer as ArrayBuffer
317
+ const ctors = [null, Uint8Array, Uint16Array, Uint32Array, Int32Array, Float32Array, Float64Array, Int16Array, Int8Array, Uint8ClampedArray] as const
318
+ if (kind === 0) { this.writeOut(buf); return }
319
+ const C = ctors[kind]
320
+ this.writeOut(C ? new C(buf) : undefined)
321
+ }
322
+ /** mode 0: a typed array of `elem`-byte elements (size = elements); 1: any typed array's bytes;
323
+ * 2: an ArrayBuffer. A malloc'd copy the arm frees (a 1-byte block for an empty array), 0 when
324
+ * the value is not such an array. */
325
+ arrayIn(idx: number, elem: number, mode: number, sizeOut: number): number {
326
+ const M = this.M
327
+ const v = this.table[idx]
328
+ let bytes: Uint8Array | null = null
329
+ let size = 0
330
+ if (mode === 2) {
331
+ if (!(v instanceof ArrayBuffer)) return 0
332
+ bytes = new Uint8Array(v); size = bytes.byteLength
333
+ } else {
334
+ if (!ArrayBuffer.isView(v) || v instanceof DataView) return 0
335
+ const ta = v as Uint8Array
336
+ const bpe = (ta as unknown as { BYTES_PER_ELEMENT: number }).BYTES_PER_ELEMENT
337
+ if (mode === 0 && bpe !== elem) return 0
338
+ bytes = new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength)
339
+ size = mode === 0 ? ta.length : ta.byteLength
340
+ }
341
+ const p = M._malloc(Math.max(bytes.byteLength, 1))
342
+ if (bytes.byteLength > 0) M.HEAPU8.set(bytes, p)
343
+ M.HEAPU32[sizeOut >> 2] = size
344
+ return p
345
+ }
346
+ arrayOut(idx: number, data: number, bytes: number) {
347
+ const v = this.table[idx]
348
+ let dst: Uint8Array
349
+ if (v instanceof ArrayBuffer) dst = new Uint8Array(v)
350
+ else if (ArrayBuffer.isView(v)) dst = new Uint8Array(v.buffer, v.byteOffset, v.byteLength)
351
+ else return
352
+ const n = Math.min(bytes, dst.byteLength)
353
+ if (n > 0) dst.set(this.M.HEAPU8.subarray(data, data + n))
354
+ }
355
+
356
+ // ---- handler tables + native functions --------------------------------------------------------
357
+ /** Run an installed handler with `args`: the arguments go onto the arg stack, the result comes
358
+ * back through the slot (an exception re-thrown here). */
359
+ private invoke(call: (argc: number, argv: number) => void, args: unknown[]): unknown {
360
+ const argc = args.length
361
+ const sp = this.argSp
362
+ let argv = sp
363
+ let heapArgv = 0
364
+ if (sp + argc * 8 > this.argBase + ARG_STACK_BYTES) { heapArgv = this.M._malloc(Math.max(argc * 8, 8)); argv = heapArgv }
365
+ else this.argSp = sp + argc * 8
366
+ const owned: number[] = []
367
+ for (let i = 0; i < argc; i++) { const idx = this.write(argv + i * 8, args[i]); if (idx) owned.push(idx) }
368
+ let r: { value: unknown, threw: boolean }
369
+ try {
370
+ call(argc, argv)
371
+ r = this.takeOut()
372
+ } finally {
373
+ for (const idx of owned) this.release(idx)
374
+ this.argSp = sp
375
+ if (heapArgv) this.M._free(heapArgv)
376
+ }
377
+ if (r.threw) this.throwPending()
378
+ return r.value
379
+ }
380
+ defineMethod(ctx: number, obj: number, name: string, fnIndex: number, length: number, getter: number) {
381
+ const o = this.table[obj] as object
382
+ if (getter) {
383
+ Object.defineProperty(o, name, { configurable: true, enumerable: false, get: () => this.invoke(() => this.M._jsbInvokeGetter(fnIndex), []) })
384
+ return
385
+ }
386
+ const M = this.M
387
+ const f = (...args: unknown[]) => this.invoke((argc, argv) => M._jsbInvoke(ctx, fnIndex, argc, argv), args)
388
+ Object.defineProperty(f, "name", { value: name })
389
+ Object.defineProperty(f, "length", { value: length })
390
+ Object.defineProperty(o, name, { configurable: true, enumerable: false, writable: true, value: f })
391
+ }
392
+ makeFunction(ctx: number, k: number, name: string, length: number) {
393
+ const M = this.M
394
+ const f = (...args: unknown[]) => this.invoke((argc, argv) => M._jsbInvokeData(ctx, k, argc, argv), args)
395
+ if (name) Object.defineProperty(f, "name", { value: name })
396
+ Object.defineProperty(f, "length", { value: length })
397
+ this.fnRegistry.register(f, k)
398
+ this.writeOut(f)
399
+ }
400
+
401
+ // ---- host callbacks (the generated forwarders) -----------------------------------------------------
402
+ /** A JS function for a JSCallback heap pointer a host slot received: calling it posts the call
403
+ * through the runtime's dispatch queue (the next tick runs it). `group` = the slot's settle
404
+ * group (released after the first settle; a second settle is ignored); null = a long-lived
405
+ * listener the runtime frees itself. A zero pointer (an optional callback the app left out) is
406
+ * a no-op. */
407
+ callback(ptr: number, group: Group | null): (...args: unknown[]) => void {
408
+ if (!ptr) return () => {}
409
+ return (...args: unknown[]) => {
410
+ if (group) {
411
+ if (group.done) { console.warn("[jsb] a settled host callback was called again"); return }
412
+ group.done = true
413
+ }
414
+ const M = this.M
415
+ const argc = args.length
416
+ const n = group ? group.ptrs.length : 0
417
+ const p = M._malloc(Math.max(argc * 8 + n * 4, 8))
418
+ const owned: number[] = []
419
+ for (let i = 0; i < argc; i++) { const idx = this.write(p + i * 8, args[i]); if (idx) owned.push(idx) }
420
+ const gp = p + argc * 8
421
+ for (let i = 0; i < n; i++) M.HEAPU32[(gp >> 2) + i] = group!.ptrs[i]
422
+ M._jsbHostCall(ptr, argc, p, gp, n)
423
+ for (const idx of owned) this.release(idx)
424
+ M._free(p)
425
+ }
426
+ }
427
+ /** A native view's event, host → app (`channel.emit` of a view a TS host mounted): the payload
428
+ * crosses as the value it is — the runtime retains it until its dispatch queue delivers the
429
+ * event. No payload = the listener gets none. */
430
+ viewEvent(viewId: number, event: string, data?: unknown): void {
431
+ const M = this.M
432
+ const e = M.stringToNewUTF8(event)
433
+ if (data === undefined) { M._jsbViewEvent(viewId, e, 0); M._free(e); return }
434
+ const p = M._malloc(8)
435
+ const idx = this.write(p, data)
436
+ M._jsbViewEvent(viewId, e, p)
437
+ if (idx) this.release(idx)
438
+ M._free(p)
439
+ M._free(e)
440
+ }
441
+ /** A settle group for the callbacks of one slot call. */
442
+ group(...ptrs: number[]): Group { return { ptrs: ptrs.filter((x) => x !== 0), done: false } }
443
+ /** A `const char* const*` + count → strings (the generated forwarders' string[] parameters). */
444
+ strings(ptr: number, count: number): string[] {
445
+ const M = this.M
446
+ const out: string[] = new Array(count)
447
+ for (let i = 0; i < count; i++) { const p = M.HEAPU32[(ptr >> 2) + i]; out[i] = p ? M.UTF8ToString(p) : "" }
448
+ return out
449
+ }
450
+ /** A host's byte result → a malloc'd, NUL-terminated copy the runtime frees (0 for none); the
451
+ * byte count goes to `outSize` when the slot has one. */
452
+ bytesOut(r: unknown, outSize: number): number {
453
+ const M = this.M
454
+ let bytes: Uint8Array | null = null
455
+ if (r instanceof Uint8Array) bytes = r
456
+ else if (r instanceof ArrayBuffer) bytes = new Uint8Array(r)
457
+ else if (ArrayBuffer.isView(r)) bytes = new Uint8Array(r.buffer, r.byteOffset, r.byteLength)
458
+ if (!bytes || bytes.byteLength === 0) { if (outSize) M.HEAPU32[outSize >> 2] = 0; return 0 }
459
+ const p = M._malloc(bytes.byteLength + 1)
460
+ M.HEAPU8.set(bytes, p)
461
+ M.HEAPU8[p + bytes.byteLength] = 0
462
+ if (outSize) M.HEAPU32[outSize >> 2] = bytes.byteLength
463
+ return p
464
+ }
465
+
466
+ gc() {
467
+ const g = (globalThis as { gc?: () => void }).gc
468
+ const bun = (globalThis as { Bun?: { gc?: (force: boolean) => void } }).Bun
469
+ // twice: the first full collection leaves what only became unreachable during it (the weak
470
+ // processing of the sweep below is one such source); the second takes it
471
+ for (let i = 0; i < 2; i++) {
472
+ if (bun?.gc) bun.gc(true)
473
+ else if (typeof g === "function") g()
474
+ else return
475
+ this.sweep()
476
+ }
477
+ }
478
+ /** Collected class instances finalize now, not on the registry's later task. */
479
+ private sweep() {
480
+ for (const token of [...this.tracked.keys()] as { ref: WeakRef<object>, cls: number, opaque: number }[]) {
481
+ if (token.ref.deref() !== undefined) continue
482
+ this.tracked.delete(token)
483
+ this.classRegistry.unregister(token)
484
+ this.M._jsbFinalize(token.cls, token.opaque)
485
+ }
486
+ }
487
+ }
488
+
489
+ /** Create the glue for a loaded runtime module and install it as `Module.jsb`. */
490
+ export const installJsb = (M: WasmModule, options: JsbOptions): Jsb => {
491
+ const jsb = new Jsb(M, options)
492
+ ;(M as unknown as { jsb: Jsb }).jsb = jsb
493
+ return jsb
494
+ }
@@ -0,0 +1,23 @@
1
+ // The wasm profiles of runtime/web/native (build-wasm.sh → native/dist): Emscripten ES6 modules whose
2
+ // default export is the module factory. Typed loosely on purpose — the generated bridges type the
3
+ // exports they call through the Wasm adapter (src/gen/wasm.ts).
4
+ declare module "*creator-full.js" {
5
+ const factory: (args?: Record<string, unknown>) => Promise<any>
6
+ export default factory
7
+ }
8
+ declare module "*creator-2d.js" {
9
+ const factory: (args?: Record<string, unknown>) => Promise<any>
10
+ export default factory
11
+ }
12
+ declare module "*creator-ui.js" {
13
+ const factory: (args?: Record<string, unknown>) => Promise<any>
14
+ export default factory
15
+ }
16
+ declare module "*creator-nav.js" {
17
+ const factory: (args?: Record<string, unknown>) => Promise<any>
18
+ export default factory
19
+ }
20
+ declare module "*creator-headless.js" {
21
+ const factory: (args?: Record<string, unknown>) => Promise<any>
22
+ export default factory
23
+ }