Skip to content
Open
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
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 = ctxPtrOut.value.typedArray[0] as JSContextPointer
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
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
14 changes: 4 additions & 10 deletions scripts/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,21 +223,15 @@ function renderFunction(args: {

const ffiParams = JSON.stringify(params.map((param) => param.type.ffi))
const cwrapArgs = [JSON.stringify(functionName), JSON.stringify(returnType.ffi), ffiParams]
if (context.DEBUG && async) {
if (async) {
// https://emscripten.org/docs/porting/asyncify.html#usage-with-ccall
// Passing {async:true} to cwrap/ccall will wrap all return values in
// Promise.resolve(...), even if the c code doesn't suspend and returns a
// primitive value.
//
// When compiled with -s ASSERTIONS=1, Emscripten will throw if the
// function suspends and {async: true} wasn't passed.
//
// However, we'd like to avoid Promise/async overhead if the call can
// return a primitive value directly. So, we compile in {async:true}
// only in DEBUG mode, where assertions are enabled.
//
// Then we rely on our type system to ensure our code supports both
// primitive and promise-wrapped return values in production mode.
// Asyncify requires this option whenever the function may suspend. Debug
// builds assert when it is missing; release builds otherwise continue in
// a corrupted state.
cwrapArgs.push("{ async: true }")
}

Expand Down