Skip to content

Commit 76c28ab

Browse files
committed
getPropNames, QuicJSIterator
1 parent 9bec8c7 commit 76c28ab

File tree

9 files changed

+409
-17
lines changed

9 files changed

+409
-17
lines changed

c/interface.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,22 @@ void QTS_DefineProp(JSContext *ctx, JSValueConst *this_val, JSValueConst *prop_n
591591
JS_FreeAtom(ctx, prop_atom);
592592
}
593593

594+
MaybeAsync(JSValue *) QTS_GetOwnPropertyNames(JSContext *ctx, JSValue **out_ptrs, uint32_t *out_len, JSValueConst *obj, int flags) {
595+
JSPropertyEnum *tab = NULL;
596+
int status = 0;
597+
status = JS_GetOwnPropertyNames(ctx, &tab, out_len, *obj, flags);
598+
if (status < 0) {
599+
return jsvalue_to_heap(JS_GetException(ctx));
600+
}
601+
*out_ptrs = malloc(sizeof(JSValue) * *out_len);
602+
for (int i = 0; i < *out_len; i++) {
603+
(*out_ptrs)[i] = jsvalue_to_heap(JS_AtomToValue(ctx, tab[i].atom));
604+
JS_FreeAtom(ctx, tab[i].atom);
605+
}
606+
js_free(ctx, tab);
607+
return NULL;
608+
}
609+
594610
MaybeAsync(JSValue *) QTS_Call(JSContext *ctx, JSValueConst *func_obj, JSValueConst *this_obj, int argc, JSValueConst **argv_ptrs) {
595611
// convert array of pointers to array of values
596612
JSValueConst argv[argc];
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import type { QuickJSContext } from "./context"
2+
import { Lifetime, UsingDisposable } from "./lifetime"
3+
import type { QuickJSRuntime } from "./runtime"
4+
import type { QuickJSHandle } from "./types"
5+
import type { VmCallResult } from "./vm-interface"
6+
7+
/**
8+
* Proxies the iteration protocol from the host to a guest iterator.
9+
* The guest iterator is a QuickJS object with a `next` method.
10+
* See [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols).
11+
*
12+
* If calling the `next` method or any other method of the iteration protocol throws an error,
13+
* the iterator is disposed after returning the exception as the final value.
14+
*
15+
* When the iterator is done, the handle is disposed automatically.
16+
* The caller is responsible for disposing each successive value.
17+
*
18+
* ```typescript
19+
* for (const nextResult of context.unwrapResult(context.getIterator(arrayHandle)) {
20+
* const nextHandle = context.unwrapResult(nextResult)
21+
* try {
22+
* // Do something with nextHandle
23+
* console.log(context.dump(nextHandle))
24+
* } finally {
25+
* nextHandle.dispose()
26+
* }
27+
* }
28+
* ```
29+
*/
30+
export class QuickJSIterator
31+
extends UsingDisposable
32+
implements Disposable, Iterator<VmCallResult<QuickJSHandle>>
33+
{
34+
public owner: QuickJSRuntime
35+
private _next: QuickJSHandle | undefined
36+
private _isDone = false
37+
38+
constructor(
39+
public handle: QuickJSHandle,
40+
public context: QuickJSContext,
41+
) {
42+
super()
43+
this.owner = context.runtime
44+
}
45+
46+
next(value?: QuickJSHandle): IteratorResult<VmCallResult<QuickJSHandle>, any> {
47+
if (!this.alive || this._isDone) {
48+
return {
49+
done: true,
50+
value: undefined,
51+
}
52+
}
53+
54+
const nextMethod = (this._next ??= this.context.getProp(this.handle, "next"))
55+
return this.callIteratorMethod(nextMethod, value)
56+
}
57+
58+
return(value?: QuickJSHandle): IteratorResult<VmCallResult<QuickJSHandle>, any> {
59+
if (!this.alive) {
60+
return {
61+
done: true,
62+
value: undefined,
63+
}
64+
}
65+
66+
const returnMethod = this.context.getProp(this.handle, "return")
67+
if (returnMethod === this.context.undefined && value === undefined) {
68+
// This may be an automatic call by the host Javascript engine,
69+
// but the guest iterator doesn't have a `return` method.
70+
// Don't call it then.
71+
this.dispose()
72+
return {
73+
done: true,
74+
value: undefined,
75+
}
76+
}
77+
78+
const result = this.callIteratorMethod(returnMethod, value)
79+
returnMethod.dispose()
80+
this.dispose()
81+
return result
82+
}
83+
84+
throw(e?: any): IteratorResult<VmCallResult<QuickJSHandle>, any> {
85+
if (!this.alive) {
86+
return {
87+
done: true,
88+
value: undefined,
89+
}
90+
}
91+
92+
const errorHandle = e instanceof Lifetime ? e : this.context.newError(e)
93+
const throwMethod = this.context.getProp(this.handle, "throw")
94+
const result = this.callIteratorMethod(throwMethod, e)
95+
if (errorHandle.alive) {
96+
errorHandle.dispose()
97+
}
98+
throwMethod.dispose()
99+
this.dispose()
100+
return result
101+
}
102+
103+
get alive() {
104+
return this.handle.alive
105+
}
106+
107+
dispose() {
108+
this._isDone = true
109+
this.handle.dispose()
110+
this._next?.dispose()
111+
}
112+
113+
private callIteratorMethod(
114+
method: QuickJSHandle,
115+
input?: QuickJSHandle,
116+
): IteratorResult<VmCallResult<QuickJSHandle>, any> {
117+
const callResult = input
118+
? this.context.callFunction(method, this.handle, input)
119+
: this.context.callFunction(method, this.handle)
120+
if (callResult.error) {
121+
this.dispose()
122+
return {
123+
value: callResult,
124+
}
125+
}
126+
127+
const done = this.context.getProp(callResult.value, "done").consume((v) => this.context.dump(v))
128+
if (done) {
129+
callResult.value.dispose()
130+
this.dispose()
131+
return {
132+
done,
133+
value: undefined,
134+
}
135+
}
136+
137+
const value = this.context.getProp(callResult.value, "value")
138+
callResult.value.dispose()
139+
return {
140+
value: { value },
141+
done,
142+
}
143+
}
144+
}

packages/quickjs-emscripten-core/src/context.ts

Lines changed: 110 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,15 @@ import { QuickJSDeferredPromise } from "./deferred-promise"
1818
// eslint-disable-next-line @typescript-eslint/no-unused-vars
1919
import type { shouldInterruptAfterDeadline } from "./interrupt-helpers"
2020
import { QuickJSPromisePending, QuickJSUnwrapError } from "./errors"
21-
import type { Disposable } from "./lifetime"
22-
import { Lifetime, Scope, StaticLifetime, UsingDisposable, WeakLifetime } from "./lifetime"
21+
import type { Disposable, DisposableArray } from "./lifetime"
22+
import {
23+
Lifetime,
24+
Scope,
25+
StaticLifetime,
26+
UsingDisposable,
27+
WeakLifetime,
28+
createDisposableArray,
29+
} from "./lifetime"
2330
import type { HeapTypedArray } from "./memory"
2431
import { ModuleMemory } from "./memory"
2532
import type { ContextCallbacks, QuickJSModuleCallbacks } from "./module"
@@ -28,15 +35,23 @@ import type {
2835
// eslint-disable-next-line @typescript-eslint/no-unused-vars
2936
ExecutePendingJobsResult,
3037
} from "./runtime"
31-
import type { ContextEvalOptions, JSValue, PromiseExecutor, QuickJSHandle } from "./types"
32-
import { evalOptionsToFlags } from "./types"
38+
import {
39+
ContextEvalOptions,
40+
GetOwnPropertyNamesOptions,
41+
JSValue,
42+
PromiseExecutor,
43+
QuickJSHandle,
44+
StaticJSValue,
45+
} from "./types"
46+
import { evalOptionsToFlags, getOwnPropertyNamesOptionsToFlags } from "./types"
3347
import type {
3448
LowLevelJavascriptVm,
3549
SuccessOrFail,
3650
VmCallResult,
3751
VmFunctionImplementation,
3852
VmPropertyDescriptor,
3953
} from "./vm-interface"
54+
import { QuickJSIterator } from "./QuickJSIterator"
4055

4156
/**
4257
* Property key for getting or setting a property on a handle with
@@ -109,6 +124,15 @@ class ContextMemory extends ModuleMemory implements Disposable {
109124
heapValueHandle(ptr: JSValuePointer): JSValue {
110125
return new Lifetime(ptr, this.copyJSValue, this.freeJSValue, this.owner)
111126
}
127+
128+
/** Manage a heap pointer with the lifetime of the context */
129+
staticHeapValueHandle(ptr: JSValuePointer | JSValueConstPointer): StaticJSValue {
130+
this.manage(this.heapValueHandle(ptr as JSValuePointer))
131+
// This isn't technically a static lifetime, but since it has the same
132+
// lifetime as the VM, it's okay to fake one since when the VM is
133+
// disposed, no other functions will accept the value.
134+
return new StaticLifetime(ptr as JSValueConstPointer, this.owner) as StaticJSValue
135+
}
112136
}
113137

114138
/**
@@ -178,6 +202,12 @@ export class QuickJSContext
178202
protected _BigInt: QuickJSHandle | undefined = undefined
179203
/** @private */
180204
protected uint32Out: HeapTypedArray<Uint32Array, UInt32Pointer>
205+
/** @private */
206+
protected _Symbol: QuickJSHandle | undefined = undefined
207+
/** @private */
208+
protected _SymbolIterator: QuickJSHandle | undefined = undefined
209+
/** @private */
210+
protected _SymbolAsyncIterator: QuickJSHandle | undefined = undefined
181211

182212
/**
183213
* Use {@link QuickJSRuntime#newContext} or {@link QuickJSWASMModule#newContext}
@@ -297,12 +327,7 @@ export class QuickJSContext
297327
const ptr = this.ffi.QTS_GetGlobalObject(this.ctx.value)
298328

299329
// Automatically clean up this reference when we dispose
300-
this.memory.manage(this.memory.heapValueHandle(ptr))
301-
302-
// This isn't technically a static lifetime, but since it has the same
303-
// lifetime as the VM, it's okay to fake one since when the VM is
304-
// disposed, no other functions will accept the value.
305-
this._global = new StaticLifetime(ptr, this.runtime)
330+
this._global = this.memory.staticHeapValueHandle(ptr)
306331
return this._global
307332
}
308333

@@ -349,6 +374,14 @@ export class QuickJSContext
349374
return this.memory.heapValueHandle(ptr)
350375
}
351376

377+
/**
378+
* Access a well-known symbol that is a property of the global Symbol object, like `Symbol.iterator`.
379+
*/
380+
getWellKnownSymbol(name: string): QuickJSHandle {
381+
this._Symbol ??= this.memory.manage(this.getProp(this.global, "Symbol"))
382+
return this.getProp(this._Symbol, name)
383+
}
384+
352385
/**
353386
* Create a QuickJS [bigint](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) value.
354387
*/
@@ -781,6 +814,73 @@ export class QuickJSContext
781814
return this.uint32Out.value.typedArray[0]
782815
}
783816

817+
/**
818+
* `Object.getOwnPropertyNames(handle)`.
819+
* Similar to the [standard semantics](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames).
820+
*/
821+
getPropNames(
822+
handle: QuickJSHandle,
823+
options: GetOwnPropertyNamesOptions,
824+
): SuccessOrFail<DisposableArray<JSValue>, QuickJSHandle> {
825+
this.runtime.assertOwned(handle)
826+
handle.value // assert alive
827+
const flags = getOwnPropertyNamesOptionsToFlags(options)
828+
return Scope.withScope((scope) => {
829+
const outPtr = scope.manage(this.memory.newMutablePointerArray<JSValuePointerPointer>(1))
830+
const errorPtr = this.ffi.QTS_GetOwnPropertyNames(
831+
this.ctx.value,
832+
outPtr.value.ptr,
833+
this.uint32Out.value.ptr,
834+
handle.value,
835+
flags,
836+
)
837+
if (errorPtr) {
838+
return { error: this.memory.heapValueHandle(errorPtr) }
839+
}
840+
const len = this.uint32Out.value.typedArray[0]
841+
const ptr = outPtr.value.typedArray[0]
842+
const pointerArray = new Uint32Array(this.module.HEAP8.buffer, ptr, len)
843+
const handles = Array.from(pointerArray).map((ptr) =>
844+
this.memory.heapValueHandle(ptr as JSValuePointer),
845+
)
846+
this.module._free(ptr)
847+
return { value: createDisposableArray(handles) }
848+
})
849+
}
850+
851+
/**
852+
* `handle[Symbol.iterator]()`. See {@link QuickJSIterator}.
853+
* Returns a host iterator that wraps and proxies calls to a guest iterator handle.
854+
* Each step of the iteration returns a result, either an error or a handle to the next value.
855+
* Once the iterator is done, the handle is automatically disposed, and the iterator
856+
* is considered done if the handle is disposed.
857+
*
858+
* ```typescript
859+
* for (const nextResult of context.unwrapResult(context.getIterator(arrayHandle)) {
860+
* const nextHandle = context.unwrapResult(nextResult)
861+
* try {
862+
* // Do something with nextHandle
863+
* console.log(context.dump(nextHandle))
864+
* } finally {
865+
* nextHandle.dispose()
866+
* }
867+
* }
868+
* ```
869+
*/
870+
getIterator(handle: QuickJSHandle): SuccessOrFail<QuickJSIterator, QuickJSHandle> {
871+
const SymbolIterator = (this._SymbolIterator ??= this.memory.manage(
872+
this.getWellKnownSymbol("iterator"),
873+
))
874+
return Scope.withScope((scope) => {
875+
const methodHandle = scope.manage(this.getProp(handle, SymbolIterator))
876+
const iteratorCallResult = this.callFunction(methodHandle, handle)
877+
if (iteratorCallResult.error) {
878+
return iteratorCallResult
879+
}
880+
return { value: new QuickJSIterator(iteratorCallResult.value, this) }
881+
})
882+
}
883+
784884
/**
785885
* `handle[key] = value`.
786886
* Set a property on a JSValue.

packages/quickjs-emscripten-core/src/lifetime.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,3 +318,48 @@ export class Scope extends UsingDisposable implements Disposable {
318318
this._disposables.dispose()
319319
}
320320
}
321+
322+
/**
323+
* An `Array` that also implements {@link Disposable}:
324+
*
325+
* - Considered {@link Disposable#alive} if any of its elements are `alive`.
326+
* - When {@link Disposable#dispose}d, it will dispose of all its elements that are `alive`.
327+
*/
328+
export type DisposableArray<T> = T[] & Disposable
329+
330+
/**
331+
* Create an array that also implements {@link Disposable}.
332+
*/
333+
export function createDisposableArray<T extends Disposable>(
334+
items?: Iterable<T>,
335+
): DisposableArray<T> {
336+
const array = items ? Array.from(items) : []
337+
338+
function disposeAlive() {
339+
return array.forEach((disposable) => (disposable.alive ? disposable.dispose() : undefined))
340+
}
341+
342+
function someIsAlive() {
343+
return array.some((disposable) => disposable.alive)
344+
}
345+
346+
Object.defineProperty(array, SymbolDispose, {
347+
configurable: true,
348+
enumerable: false,
349+
value: disposeAlive,
350+
})
351+
352+
Object.defineProperty(array, "dispose", {
353+
configurable: true,
354+
enumerable: false,
355+
value: disposeAlive,
356+
})
357+
358+
Object.defineProperty(array, "alive", {
359+
configurable: true,
360+
enumerable: false,
361+
get: someIsAlive,
362+
})
363+
364+
return array as T[] & Disposable
365+
}

0 commit comments

Comments
 (0)