Skip to content

Commit

Permalink
feat: support Date and custom marshaller options
Browse files Browse the repository at this point in the history
  • Loading branch information
rot1024 committed Aug 26, 2022
1 parent 276feff commit 5ef7df6
Show file tree
Hide file tree
Showing 16 changed files with 320 additions and 107 deletions.
35 changes: 28 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,19 +193,40 @@ Options accepted:

```ts
type Options = {
/** A callback that returns a boolean value that determines whether an object is marshalled or not. If false, no marshaling will be done and undefined will be passed to the QuickJS VM, otherwise marshaling will be done. By default, all objects will be marshalled. */
isMarshalable?: boolean | "json" | ((target: any) => boolean | "json");
/** Pre-registered pairs of objects that will be considered the same between the host and the QuickJS VM. This will be used automatically during the conversion. By default, it will be registered automatically with `defaultRegisteredObjects`.
*
* Instead of a string, you can also pass a QuickJSHandle directly. In that case, however, you have to dispose of them manually when destroying the VM.
*/
registeredObjects?: Iterable<[any, QuickJSHandle | string]>;
/** Register functions to convert an object to a QuickJS handle. */
customMarshaller?: Iterable<
(target: unknown, ctx: QuickJSContext) => QuickJSHandle | undefined
>;
/** Register functions to convert a QuickJS handle to an object. */
customUnmarshaller?: Iterable<
(target: QuickJSHandle, ctx: QuickJSContext) => any
>;
/** A callback that returns a boolean value that determines whether an object is wrappable by proxies. If returns false, note that the object cannot be synchronized between the host and the QuickJS even if arena.sync is used. */
isWrappable?: (target: any) => boolean;
/** A callback that returns a boolean value that determines whether an QuickJS handle is wrappable by proxies. If returns false, note that the handle cannot be synchronized between the host and the QuickJS even if arena.sync is used. */
isHandleWrappable?: (handle: QuickJSHandle, ctx: QuickJSContext) => boolean;
/** Compatibility with quickjs-emscripten prior to v0.15. Inject code for compatibility into context at Arena class initialization time. */
compat?: boolean;
}
```
- **`isMarshalable`**: Determines how marshalling will be done when sending objects from the host to the context. **Make sure to set the marshalling to be the minimum necessary as it may reduce the security of your application.** [Please read the section on security above.](#security-warning)
- `"json"` (**default**, safety): Target object will be serialized as JSON in host and then parsed in context. Functions and classes will be lost in the process.
- `false` (safety): Target object will not be always marshalled as `undefined`.
- `(target: any) => boolean | "json"` (recoomended): You can control marshalling mode for each objects. If you want to do marshalling, usually use this method. Allow partial marshalling by returning `true` only for some objects.
- `true` (**risky and not recommended**): Target object will be always marshaled. This setting may reduce security.
- **`registeredObjects`**: You can pre-register a pair of objects that will be considered the same between the host and the QuickJS context. This will be used automatically during the conversion. By default, it will be registered automatically with [`defaultRegisteredObjects`](src/default.ts). If you want to add a new pair to this, please do the following:
- **`compat`**: If you want to use quickjs-emscripten v0.15 or older, enable this option to automatically inject code to the VM for compatibility.
Notes:
**`isMarshalable`**: Determines how marshalling will be done when sending objects from the host to the context. **Make sure to set the marshalling to be the minimum necessary as it may reduce the security of your application.** [Please read the section on security above.](#security-warning)
- `"json"` (**default**, safety): Target object will be serialized as JSON in host and then parsed in context. Functions and classes will be lost in the process.
- `false` (safety): Target object will not be always marshalled as `undefined`.
- `(target: any) => boolean | "json"` (recoomended): You can control marshalling mode for each objects. If you want to do marshalling, usually use this method. Allow partial marshalling by returning `true` only for some objects.
- `true` (**risky and not recommended**): Target object will be always marshaled. This setting may reduce security.
**`registeredObjects`**: You can pre-register a pair of objects that will be considered the same between the host and the QuickJS context. This will be used automatically during the conversion. By default, it will be registered automatically with [`defaultRegisteredObjects`](src/default.ts). If you want to add a new pair to this, please do the following:
```js
import { defaultRegisteredObjects } from "quickjs-emscripten-sync";
Expand Down
13 changes: 13 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,19 @@ describe("evalCode", () => {
ctx.dispose();
});

test("Date", async () => {
const ctx = (await getQuickJS()).newContext();
const arena = new Arena(ctx, { isMarshalable: true });

const date = new Date(2022, 7, 26);
expect(arena.evalCode("new Date(2022, 7, 26)")).toEqual(date);
expect(arena.evalCode("d => d instanceof Date")(date)).toBe(true);
expect(arena.evalCode("d => d.getTime()")(date)).toBe(date.getTime());

arena.dispose();
ctx.dispose();
});

test("class", async () => {
const ctx = (await getQuickJS()).newContext();
const arena = new Arena(ctx, { isMarshalable: true });
Expand Down
22 changes: 19 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,23 @@ export {
export type Options = {
/** A callback that returns a boolean value that determines whether an object is marshalled or not. If false, no marshaling will be done and undefined will be passed to the QuickJS VM, otherwise marshaling will be done. By default, all objects will be marshalled. */
isMarshalable?: boolean | "json" | ((target: any) => boolean | "json");
/** You can pre-register a pair of objects that will be considered the same between the host and the QuickJS VM. This will be used automatically during the conversion. By default, it will be registered automatically with `defaultRegisteredObjects`.
/** Pre-registered pairs of objects that will be considered the same between the host and the QuickJS VM. This will be used automatically during the conversion. By default, it will be registered automatically with `defaultRegisteredObjects`.
*
* Instead of a string, you can also pass a QuickJSHandle directly. In that case, however, you have to dispose of them manually when destroying the VM.
*/
registeredObjects?: Iterable<[any, QuickJSHandle | string]>;
/** Register functions to convert an object to a QuickJS handle. */
customMarshaller?: Iterable<
(target: unknown, ctx: QuickJSContext) => QuickJSHandle | undefined
>;
/** Register functions to convert a QuickJS handle to an object. */
customUnmarshaller?: Iterable<
(target: QuickJSHandle, ctx: QuickJSContext) => any
>;
/** A callback that returns a boolean value that determines whether an object is wrappable by proxies. If returns false, note that the object cannot be synchronized between the host and the QuickJS even if arena.sync is used. */
isWrappable?: (target: any) => boolean;
/** A callback that returns a boolean value that determines whether an QuickJS handle is wrappable by proxies. If returns false, note that the handle cannot be synchronized between the host and the QuickJS even if arena.sync is used. */
isHandleWrappable?: (handle: QuickJSHandle, ctx: QuickJSContext) => boolean;
/** Compatibility with quickjs-emscripten prior to v0.15. Inject code for compatibility into context at Arena class initialization time. */
compat?: boolean;
};
Expand Down Expand Up @@ -265,6 +277,7 @@ export class Arena {
find: this._marshalFind,
pre: this._marshalPre,
preApply: this._marshalPreApply,
custom: this._options?.customMarshaller,
});

return [handle, !this._map.hasHandle(handle)];
Expand All @@ -290,6 +303,7 @@ export class Arena {
marshal: this._marshal,
find: this._unmarshalFind,
pre: this._preUnmarshal,
custom: this._options?.customUnmarshaller,
});
};

Expand Down Expand Up @@ -338,7 +352,8 @@ export class Arena {
this._symbol,
this._symbolHandle,
this._marshal,
this._syncMode
this._syncMode,
this._options?.isWrappable
);
}

Expand All @@ -362,7 +377,8 @@ export class Arena {
this._symbol,
this._symbolHandle,
this._unmarshal,
this._syncMode
this._syncMode,
this._options?.isHandleWrappable
);
}

Expand Down
53 changes: 53 additions & 0 deletions src/marshal/custom.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { getQuickJS } from "quickjs-emscripten";
import { expect, test, vi } from "vitest";
import { call } from "../vmutil";

import marshalCustom, { defaultCustom } from "./custom";

test("symbol", async () => {
const ctx = (await getQuickJS()).newContext();
const pre = vi.fn();
const sym = Symbol("foobar");

const marshal = (t: unknown) => marshalCustom(ctx, t, pre, defaultCustom);

expect(marshal({})).toBe(undefined);
expect(pre).toBeCalledTimes(0);

const handle = marshal(sym);
if (!handle) throw new Error("handle is undefined");
expect(ctx.typeof(handle)).toBe("symbol");
expect(ctx.getString(ctx.getProp(handle, "description"))).toBe("foobar");
expect(pre).toReturnTimes(1);
expect(pre.mock.calls[0][0]).toBe(sym);
expect(pre.mock.calls[0][1] === handle).toBe(true);

handle.dispose();
ctx.dispose();
});

test("date", async () => {
const ctx = (await getQuickJS()).newContext();
const pre = vi.fn();
const date = new Date(2022, 7, 26);

const marshal = (t: unknown) => marshalCustom(ctx, t, pre, defaultCustom);

expect(marshal({})).toBe(undefined);
expect(pre).toBeCalledTimes(0);

const handle = marshal(date);
if (!handle) throw new Error("handle is undefined");
expect(ctx.dump(call(ctx, "d => d instanceof Date", undefined, handle))).toBe(
true
);
expect(ctx.dump(call(ctx, "d => d.getTime()", undefined, handle))).toBe(
date.getTime()
);
expect(pre).toReturnTimes(1);
expect(pre.mock.calls[0][0]).toBe(date);
expect(pre.mock.calls[0][1] === handle).toBe(true);

handle.dispose();
ctx.dispose();
});
52 changes: 52 additions & 0 deletions src/marshal/custom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";

import { call } from "../vmutil";

export default function marshalCustom(
ctx: QuickJSContext,
target: unknown,
preMarshal: (
target: unknown,
handle: QuickJSHandle
) => QuickJSHandle | undefined,
custom: Iterable<
(target: unknown, ctx: QuickJSContext) => QuickJSHandle | undefined
>
): QuickJSHandle | undefined {
let handle: QuickJSHandle | undefined;
for (const c of custom) {
handle = c(target, ctx);
if (handle) break;
}
return handle ? preMarshal(target, handle) ?? handle : undefined;
}

export function symbol(
target: unknown,
ctx: QuickJSContext
): QuickJSHandle | undefined {
if (typeof target !== "symbol") return;
const handle = call(
ctx,
"d => Symbol(d)",
undefined,
target.description ? ctx.newString(target.description) : ctx.undefined
);
return handle;
}

export function date(
target: unknown,
ctx: QuickJSContext
): QuickJSHandle | undefined {
if (!(target instanceof Date)) return;
const handle = call(
ctx,
"d => new Date(d)",
undefined,
ctx.newNumber(target.getTime())
);
return handle;
}

export const defaultCustom = [symbol, date];
21 changes: 20 additions & 1 deletion src/marshal/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,26 @@ test("class", async () => {
methodHoge.dispose();
getter.dispose();
setter.dispose();
map.dispose();
dispose();
});

test("date", async () => {
const { ctx, map, marshal, dispose } = await setup();

const date = new Date(2022, 7, 26);
const handle = marshal(date);
if (!map) throw new Error("map is undefined");

expect(map.size).toBe(1);
expect(map.get(date)).toBe(handle);

expect(ctx.dump(call(ctx, "d => d instanceof Date", undefined, handle))).toBe(
true
);
expect(ctx.dump(call(ctx, "d => d.getTime()", undefined, handle))).toBe(
date.getTime()
);

dispose();
});

Expand Down
10 changes: 8 additions & 2 deletions src/marshal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {
import marshalFunction from "./function";
import marshalObject from "./object";
import marshalPrimitive from "./primitive";
import marshalSymbol from "./symbol";
import marshalCustom, { defaultCustom } from "./custom";
import marshalJSON from "./json";
import marshalPromise from "./promise";

Expand All @@ -22,6 +22,9 @@ export type Options = {
mode: true | "json" | undefined
) => QuickJSHandle | undefined;
preApply?: (target: Function, thisArg: unknown, args: unknown[]) => any;
custom?: Iterable<
(obj: unknown, ctx: QuickJSContext) => QuickJSHandle | undefined
>;
};

export function marshal(target: unknown, options: Options): QuickJSHandle {
Expand Down Expand Up @@ -52,7 +55,10 @@ export function marshal(target: unknown, options: Options): QuickJSHandle {

const marshal2 = (t: unknown) => marshal(t, options);
return (
marshalSymbol(ctx, target, pre2) ??
marshalCustom(ctx, target, pre2, [
...defaultCustom,
...(options.custom ?? []),
]) ??
marshalPromise(ctx, target, marshal2, pre2) ??
marshalFunction(ctx, target, marshal2, unmarshal, pre2, options.preApply) ??
marshalObject(ctx, target, marshal2, pre2) ??
Expand Down
24 changes: 0 additions & 24 deletions src/marshal/symbol.test.ts

This file was deleted.

21 changes: 0 additions & 21 deletions src/marshal/symbol.ts

This file was deleted.

51 changes: 51 additions & 0 deletions src/unmarshal/custom.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { getQuickJS, QuickJSHandle } from "quickjs-emscripten";
import { expect, test, vi } from "vitest";
import unmarshalCustom, { defaultCustom } from "./custom";

test("symbol", async () => {
const ctx = (await getQuickJS()).newContext();
const pre = vi.fn();
const obj = ctx.newObject();
const handle = ctx.unwrapResult(ctx.evalCode(`Symbol("foobar")`));

const unmarshal = (h: QuickJSHandle): any =>
unmarshalCustom(ctx, h, pre, defaultCustom);

expect(unmarshal(obj)).toBe(undefined);
expect(pre).toBeCalledTimes(0);

const sym = unmarshal(handle);
expect(typeof sym).toBe("symbol");
expect((sym as any).description).toBe("foobar");
expect(pre).toReturnTimes(1);
expect(pre.mock.calls[0][0]).toBe(sym);
expect(pre.mock.calls[0][1] === handle).toBe(true);

handle.dispose();
obj.dispose();
ctx.dispose();
});

test("date", async () => {
const ctx = (await getQuickJS()).newContext();
const pre = vi.fn();
const obj = ctx.newObject();
const handle = ctx.unwrapResult(ctx.evalCode(`new Date(2022, 7, 26)`));

const unmarshal = (h: QuickJSHandle): any =>
unmarshalCustom(ctx, h, pre, defaultCustom);

expect(unmarshal(obj)).toBe(undefined);
expect(pre).toBeCalledTimes(0);

const date = unmarshal(handle);
expect(date).toBeInstanceOf(Date);
expect(date.getTime()).toBe(new Date(2022, 7, 26).getTime());
expect(pre).toReturnTimes(1);
expect(pre.mock.calls[0][0]).toBe(date);
expect(pre.mock.calls[0][1] === handle).toBe(true);

handle.dispose();
obj.dispose();
ctx.dispose();
});
Loading

0 comments on commit 5ef7df6

Please sign in to comment.