Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 0 additions & 96 deletions .github/workflows/main.yml

This file was deleted.

9 changes: 9 additions & 0 deletions packages/quickjs-emscripten-core/src/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ export class ModuleMemory {
return new Lifetime({ typedArray, ptr }, undefined, (value) => this.module._free(value.ptr))
}

/**
* Read one pointer out of the heap through a view built at call time. WASM
* memory growth detaches any view made earlier, so a caller that suspends
* between allocating and reading must not reuse the original typed array.
*/
readPointer<T extends number>(ptr: number): T {
return new Int32Array(this.module.HEAPU8.buffer, ptr, 1)[0] as T
}

// TODO: shouldn't this be Uint32 instead of Int32?
newMutablePointerArray<T extends number>(
length: number,
Expand Down
25 changes: 25 additions & 0 deletions packages/quickjs-emscripten-core/src/runtime-asyncify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {
QuickJSAsyncEmscriptenModule,
QuickJSAsyncFFI,
JSContextPointer,
JSContextPointerPointer,
JSRuntimePointer,
} from "@jitl/quickjs-ffi-types"
import { QuickJSAsyncContext } from "./context-asyncify"
Expand All @@ -21,6 +22,7 @@ import type {
} from "./types"
import { intrinsicsToFlags } from "./types"
import { Lifetime } from "./lifetime"
import type { ExecutePendingJobsResult } from "./runtime"

export class QuickJSAsyncRuntime extends QuickJSRuntime {
declare public context: QuickJSAsyncContext | undefined
Expand Down Expand Up @@ -92,4 +94,27 @@ export class QuickJSAsyncRuntime extends QuickJSRuntime {
public override setMaxStackSize(stackSize: number): void {
return super.setMaxStackSize(stackSize)
}

/**
* Asyncified version of {@link QuickJSRuntime.executePendingJobs}.
*
* Pending jobs may call an asyncified host function, so the synchronous
* version cannot safely drive a runtime created from an Asyncify build.
*/
async executePendingJobsAsync(
maxJobsToExecute: number | void = -1,
): Promise<ExecutePendingJobsResult> {
const ctxPtrOut = this.memory.newMutablePointerArray<JSContextPointerPointer>(1)
try {
const valuePtr = await this.ffi.QTS_ExecutePendingJob_MaybeAsync(
this.rt.value,
maxJobsToExecute ?? -1,
ctxPtrOut.value.ptr,
)
const ctxPtr = this.memory.readPointer<JSContextPointer>(ctxPtrOut.value.ptr)
return this.resolveExecutePendingJobsResult(valuePtr, ctxPtr)
} finally {
ctxPtrOut.dispose()
}
}
}
9 changes: 9 additions & 0 deletions packages/quickjs-emscripten-core/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
JSContextPointer,
JSContextPointerPointer,
JSRuntimePointer,
JSValuePointer,
EitherFFI,
EitherModule,
} from "@jitl/quickjs-ffi-types"
Expand Down Expand Up @@ -253,6 +254,14 @@ export class QuickJSRuntime extends UsingDisposable implements Disposable {

const ctxPtr = ctxPtrOut.value.typedArray[0] as JSContextPointer
ctxPtrOut.dispose()
return this.resolveExecutePendingJobsResult(valuePtr, ctxPtr)
}

/** @private */
protected resolveExecutePendingJobsResult(
valuePtr: JSValuePointer,
ctxPtr: JSContextPointer,
): ExecutePendingJobsResult {
if (ctxPtr === 0) {
// No jobs executed.
this.ffi.QTS_FreeValuePointerRuntime(this.rt.value, valuePtr)
Expand Down
31 changes: 31 additions & 0 deletions packages/quickjs-emscripten/src/asyncify-sync-driver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { assert, describe, it } from "vitest"
import { newQuickJSAsyncWASMModuleFromVariant } from "quickjs-emscripten-core"
import { RELEASE_ASYNC } from "./variants"

// Lives in its own file on purpose. A suspension that is reported rather than resumed leaves the
// Emscripten module suspended, and that state outlives the context it happened in — in a debug
// build the same case trips an Emscripten assertion, which aborts the module outright. Either
// way the damage is module-wide, so nothing else may share this one.
describe("asyncify synchronous job driver", () => {
it("reports a suspended job instead of returning a value it never produced", async () => {
const wasm = await newQuickJSAsyncWASMModuleFromVariant(RELEASE_ASYNC)
const vm = wasm.newContext()

vm.newAsyncifiedFunction("get", async (pathHandle) =>
vm.newString(vm.getString(pathHandle)),
).consume((fn) => vm.setProp(vm.global, "get", fn))

const result = await vm.evalCodeAsync(`
(async () => {
await get("/a")
await get("/b")
})()
`)
vm.unwrapResult(result).dispose()

// The second await resumes from a pending job and suspends into the host function.
// executePendingJobs cannot deliver that result synchronously, so it has to say so —
// silently continuing means reading a pointer the unwound call never wrote.
assert.throws(() => vm.runtime.executePendingJobs(), /returned a Promise/)
})
})
28 changes: 28 additions & 0 deletions packages/quickjs-emscripten/src/quickjs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1224,6 +1224,34 @@ function asyncContextTests(
})

describe("asyncify functions", () => {
it("supports sequential awaited host calls in one evaluation", async () => {
let asyncFunctionCalls = 0
vm.newAsyncifiedFunction("get", async (pathHandle) => {
asyncFunctionCalls++
const path = vm.getString(pathHandle)
return vm.newString(JSON.stringify({ path }))
}).consume((fn) => vm.setProp(vm.global, "get", fn))

const result = await vm.evalCodeAsync(`
(async () => {
const a = JSON.parse(await get("/a"))
const b = JSON.parse(await get("/b"))
const c = JSON.parse(await get("/c"))
return [a, b, c]
})()
`)

const promise = vm.unwrapResult(result).consume((handle) => vm.resolvePromise(handle))
vm.unwrapResult(await vm.runtime.executePendingJobsAsync())
const resolved = vm.unwrapResult(await promise)
assert.deepEqual(vm.dump(resolved), [{ path: "/a" }, { path: "/b" }, { path: "/c" }])
resolved.dispose()
assert.equal(asyncFunctionCalls, 3)

const reuseResult = await vm.evalCodeAsync("1 + 1")
assert.equal(vm.unwrapResult(reuseResult).consume(vm.dump), 2)
})

it("sees Promise<handle> as synchronous", async () => {
let asyncFunctionCalls = 0
const asyncFn = async () => {
Expand Down
37 changes: 17 additions & 20 deletions packages/variant-quickjs-ng-wasmfile-debug-asyncify/src/ffi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ export class QuickJSAsyncFFI {
ctx: JSContextPointer,
value: JSValuePointer | JSValueConstPointer,
) => JSBorrowedCharPointer = assertSync(
this.module.cwrap("QTS_GetSymbolDescriptionOrKey", "number", ["number", "number"]),
this.module.cwrap("QTS_GetSymbolDescriptionOrKey", "number", ["number", "number"], {}),
)

QTS_GetSymbolDescriptionOrKey_MaybeAsync: (
Expand Down Expand Up @@ -229,7 +229,7 @@ export class QuickJSAsyncFFI {
maxJobsToExecute: number,
lastJobContext: JSContextPointerPointer,
) => JSValuePointer = assertSync(
this.module.cwrap("QTS_ExecutePendingJob", "number", ["number", "number", "number"]),
this.module.cwrap("QTS_ExecutePendingJob", "number", ["number", "number", "number"], {}),
)

QTS_ExecutePendingJob_MaybeAsync: (
Expand All @@ -248,7 +248,7 @@ export class QuickJSAsyncFFI {
this_val: JSValuePointer | JSValueConstPointer,
prop_name: JSValuePointer | JSValueConstPointer,
) => JSValuePointer = assertSync(
this.module.cwrap("QTS_GetProp", "number", ["number", "number", "number"]),
this.module.cwrap("QTS_GetProp", "number", ["number", "number", "number"], {}),
)

QTS_GetProp_MaybeAsync: (
Expand All @@ -267,7 +267,7 @@ export class QuickJSAsyncFFI {
this_val: JSValuePointer | JSValueConstPointer,
prop_name: number,
) => JSValuePointer = assertSync(
this.module.cwrap("QTS_GetPropNumber", "number", ["number", "number", "number"]),
this.module.cwrap("QTS_GetPropNumber", "number", ["number", "number", "number"], {}),
)

QTS_GetPropNumber_MaybeAsync: (
Expand All @@ -287,7 +287,7 @@ export class QuickJSAsyncFFI {
prop_name: JSValuePointer | JSValueConstPointer,
prop_value: JSValuePointer | JSValueConstPointer,
) => void = assertSync(
this.module.cwrap("QTS_SetProp", null, ["number", "number", "number", "number"]),
this.module.cwrap("QTS_SetProp", null, ["number", "number", "number", "number"], {}),
)

QTS_SetProp_MaybeAsync: (
Expand Down Expand Up @@ -331,13 +331,12 @@ export class QuickJSAsyncFFI {
obj: JSValuePointer | JSValueConstPointer,
flags: number,
) => JSValuePointer = assertSync(
this.module.cwrap("QTS_GetOwnPropertyNames", "number", [
this.module.cwrap(
"QTS_GetOwnPropertyNames",
"number",
"number",
"number",
"number",
"number",
]),
["number", "number", "number", "number", "number"],
{},
),
)

QTS_GetOwnPropertyNames_MaybeAsync: (
Expand All @@ -360,7 +359,7 @@ export class QuickJSAsyncFFI {
argc: number,
argv_ptrs: JSValueConstPointerPointer,
) => JSValuePointer = assertSync(
this.module.cwrap("QTS_Call", "number", ["number", "number", "number", "number", "number"]),
this.module.cwrap("QTS_Call", "number", ["number", "number", "number", "number", "number"], {}),
)

QTS_Call_MaybeAsync: (
Expand All @@ -383,7 +382,7 @@ export class QuickJSAsyncFFI {
ctx: JSContextPointer,
obj: JSValuePointer | JSValueConstPointer,
) => JSBorrowedCharPointer = assertSync(
this.module.cwrap("QTS_Dump", "number", ["number", "number"]),
this.module.cwrap("QTS_Dump", "number", ["number", "number"], {}),
)

QTS_Dump_MaybeAsync: (
Expand All @@ -404,14 +403,12 @@ export class QuickJSAsyncFFI {
detectModule: EvalDetectModule,
evalFlags: EvalFlags,
) => JSValuePointer = assertSync(
this.module.cwrap("QTS_Eval", "number", [
"number",
"number",
"number",
"string",
"number",
this.module.cwrap(
"QTS_Eval",
"number",
]),
["number", "number", "number", "string", "number", "number"],
{},
),
)

QTS_Eval_MaybeAsync: (
Expand Down
Loading