From 48f9af36d33bf2b5fecb7fc929001497ac2200ee Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Mon, 16 Feb 2026 11:59:32 -0500 Subject: [PATCH 1/2] Fix WASM memory view invalidation when memory grows When Emscripten's WASM memory grows (due to -sALLOW_MEMORY_GROWTH), all existing TypedArray views become detached because the underlying ArrayBuffer is replaced. This caused bugs where reading from views after FFI calls returned undefined values if memory had grown. The fix introduces RefreshableTypedArray, a wrapper that lazily recreates the TypedArray view when HEAPU8.buffer changes. This is a simple reference comparison that only triggers view recreation when actually needed. Affected call sites: - runtime.ts: executePendingJobs - reads ctxPtrOut after QTS_ExecutePendingJob - context.ts: newPromise - reads resolve/reject handles after QTS_NewPromiseCapability - context.ts: getLength - reads uint32Out after QTS_GetLength - context.ts: getOwnPropertyNames - reads outPtr and uint32Out after QTS_GetOwnPropertyNames Also fixed: getOwnPropertyNames was using HEAP8.buffer instead of HEAPU8.buffer Co-Authored-By: Claude Opus 4.5 --- .../quickjs-emscripten-core/src/context.ts | 10 ++--- .../quickjs-emscripten-core/src/memory.ts | 41 ++++++++++++++++--- .../quickjs-emscripten-core/src/runtime.ts | 2 +- 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/packages/quickjs-emscripten-core/src/context.ts b/packages/quickjs-emscripten-core/src/context.ts index 9bc7c8a0d..a4e0323d8 100644 --- a/packages/quickjs-emscripten-core/src/context.ts +++ b/packages/quickjs-emscripten-core/src/context.ts @@ -487,7 +487,7 @@ export class QuickJSContext mutablePointerArray.value.ptr, ) const promiseHandle = this.memory.heapValueHandle(promisePtr) - const [resolveHandle, rejectHandle] = Array.from(mutablePointerArray.value.typedArray).map( + const [resolveHandle, rejectHandle] = Array.from(mutablePointerArray.value.typedArray.value).map( (jsvaluePtr) => this.memory.heapValueHandle(jsvaluePtr as any), ) return new QuickJSDeferredPromise({ @@ -994,7 +994,7 @@ export class QuickJSContext if (status < 0) { return undefined } - return this.uint32Out.value.typedArray[0] + return this.uint32Out.value.typedArray.value[0] } /** @@ -1052,9 +1052,9 @@ export class QuickJSContext if (errorPtr) { return this.fail(this.memory.heapValueHandle(errorPtr)) } - const len = this.uint32Out.value.typedArray[0] - const ptr = outPtr.value.typedArray[0] - const pointerArray = new Uint32Array(this.module.HEAP8.buffer, ptr, len) + const len = this.uint32Out.value.typedArray.value[0] + const ptr = outPtr.value.typedArray.value[0] + const pointerArray = new Uint32Array(this.module.HEAPU8.buffer, ptr, len) const handles = Array.from(pointerArray).map((ptr) => this.memory.heapValueHandle(ptr as JSValuePointer), ) diff --git a/packages/quickjs-emscripten-core/src/memory.ts b/packages/quickjs-emscripten-core/src/memory.ts index 19628f8a9..54d68cf55 100644 --- a/packages/quickjs-emscripten-core/src/memory.ts +++ b/packages/quickjs-emscripten-core/src/memory.ts @@ -29,9 +29,39 @@ export interface TypedArrayConstructor { BYTES_PER_ELEMENT: number } +/** + * A TypedArray view into WASM memory that automatically refreshes when memory grows. + * + * When Emscripten's WASM memory grows (due to -sALLOW_MEMORY_GROWTH), all existing + * TypedArray views become detached because the underlying ArrayBuffer is replaced. + * This class lazily recreates the view when the buffer changes. + * + * @private + */ +export class RefreshableTypedArray { + private cachedArray: T | undefined + private lastBuffer: ArrayBufferLike | undefined + + constructor( + private readonly module: EitherModule, + private readonly kind: TypedArrayConstructor, + private readonly ptr: number, + private readonly length: number, + ) {} + + get value(): T { + const currentBuffer = this.module.HEAPU8.buffer + if (this.cachedArray === undefined || this.lastBuffer !== currentBuffer) { + this.cachedArray = new this.kind(currentBuffer, this.ptr, this.length) + this.lastBuffer = currentBuffer + } + return this.cachedArray + } +} + /** @private */ export type HeapTypedArray = Lifetime<{ - typedArray: JS + typedArray: RefreshableTypedArray ptr: C }> @@ -54,18 +84,17 @@ export class ModuleMemory { kind: TypedArrayConstructor, length: number, ): HeapTypedArray { - const zeros = new kind(new Array(length).fill(0)) - const numBytes = zeros.length * zeros.BYTES_PER_ELEMENT + const numBytes = length * kind.BYTES_PER_ELEMENT const ptr = this.module._malloc(numBytes) as C - const typedArray = new kind(this.module.HEAPU8.buffer, ptr, length) - typedArray.set(zeros) + const typedArray = new RefreshableTypedArray(this.module, kind, ptr, length) + typedArray.value.fill(0) return new Lifetime({ typedArray, ptr }, undefined, (value) => this.module._free(value.ptr)) } // TODO: shouldn't this be Uint32 instead of Int32? newMutablePointerArray( length: number, - ): Lifetime<{ typedArray: Int32Array; ptr: T }> { + ): Lifetime<{ typedArray: RefreshableTypedArray; ptr: T }> { return this.newTypedArray(Int32Array, length) } diff --git a/packages/quickjs-emscripten-core/src/runtime.ts b/packages/quickjs-emscripten-core/src/runtime.ts index e404f0648..061838129 100644 --- a/packages/quickjs-emscripten-core/src/runtime.ts +++ b/packages/quickjs-emscripten-core/src/runtime.ts @@ -251,7 +251,7 @@ export class QuickJSRuntime extends UsingDisposable implements Disposable { ctxPtrOut.value.ptr, ) - const ctxPtr = ctxPtrOut.value.typedArray[0] as JSContextPointer + const ctxPtr = ctxPtrOut.value.typedArray.value[0] as JSContextPointer ctxPtrOut.dispose() if (ctxPtr === 0) { // No jobs executed. From 28090bc15bce1862ade366a41d5d44bd34e6b525 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Mon, 16 Feb 2026 15:19:43 -0500 Subject: [PATCH 2/2] Fix formatting in context.ts Co-Authored-By: Claude Opus 4.5 --- packages/quickjs-emscripten-core/src/context.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/quickjs-emscripten-core/src/context.ts b/packages/quickjs-emscripten-core/src/context.ts index a4e0323d8..969aaea80 100644 --- a/packages/quickjs-emscripten-core/src/context.ts +++ b/packages/quickjs-emscripten-core/src/context.ts @@ -487,9 +487,9 @@ export class QuickJSContext mutablePointerArray.value.ptr, ) const promiseHandle = this.memory.heapValueHandle(promisePtr) - const [resolveHandle, rejectHandle] = Array.from(mutablePointerArray.value.typedArray.value).map( - (jsvaluePtr) => this.memory.heapValueHandle(jsvaluePtr as any), - ) + const [resolveHandle, rejectHandle] = Array.from( + mutablePointerArray.value.typedArray.value, + ).map((jsvaluePtr) => this.memory.heapValueHandle(jsvaluePtr as any)) return new QuickJSDeferredPromise({ context: this, promiseHandle,