quickjs-emscripten-sync 1.5.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +128 -7
- package/dist/quickjs-emscripten-sync.mjs +694 -479
- package/dist/quickjs-emscripten-sync.umd.js +17 -10
- package/package.json +10 -10
- package/src/contextex.test.ts +43 -0
- package/src/contextex.ts +65 -0
- package/src/edge.test.ts +70 -0
- package/src/index.test.ts +324 -28
- package/src/index.ts +142 -2
- package/src/marshal/index.test.ts +2 -2
- package/src/vmmap.ts +1 -1
- package/src/wrapper.ts +44 -32
package/src/index.test.ts
CHANGED
|
@@ -218,6 +218,42 @@ describe("evalCode", () => {
|
|
|
218
218
|
arena.dispose();
|
|
219
219
|
ctx.dispose();
|
|
220
220
|
});
|
|
221
|
+
|
|
222
|
+
test("async function", async () => {
|
|
223
|
+
const ctx = (await getQuickJS()).newContext();
|
|
224
|
+
const arena = new Arena(ctx, { isMarshalable: true });
|
|
225
|
+
|
|
226
|
+
const consolelog = vi.fn();
|
|
227
|
+
arena.expose({
|
|
228
|
+
console: {
|
|
229
|
+
log: consolelog,
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
arena.evalCode(`
|
|
234
|
+
const someAsyncOperation = async () => "hello";
|
|
235
|
+
const execute = async () => {
|
|
236
|
+
try {
|
|
237
|
+
const res = await someAsyncOperation();
|
|
238
|
+
console.log(res);
|
|
239
|
+
} catch (e) {
|
|
240
|
+
console.log(e);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
execute();
|
|
244
|
+
`);
|
|
245
|
+
expect(consolelog).toBeCalledTimes(0);
|
|
246
|
+
expect(arena.executePendingJobs()).toBe(2);
|
|
247
|
+
|
|
248
|
+
arena.executePendingJobs();
|
|
249
|
+
|
|
250
|
+
expect(consolelog).toBeCalledTimes(1);
|
|
251
|
+
expect(consolelog).toBeCalledWith("hello");
|
|
252
|
+
expect(arena.executePendingJobs()).toBe(0);
|
|
253
|
+
|
|
254
|
+
arena.dispose();
|
|
255
|
+
ctx.dispose();
|
|
256
|
+
});
|
|
221
257
|
});
|
|
222
258
|
|
|
223
259
|
describe("expose without sync", () => {
|
|
@@ -577,41 +613,301 @@ describe("isMarshalable option", () => {
|
|
|
577
613
|
});
|
|
578
614
|
});
|
|
579
615
|
|
|
580
|
-
describe("
|
|
581
|
-
test("
|
|
616
|
+
describe("evalModule", () => {
|
|
617
|
+
test("module can modify exposed globals", async () => {
|
|
582
618
|
const ctx = (await getQuickJS()).newContext();
|
|
583
619
|
const arena = new Arena(ctx, { isMarshalable: true });
|
|
584
620
|
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
const exposed = {
|
|
588
|
-
get a() {
|
|
589
|
-
called.push("a");
|
|
590
|
-
return {
|
|
591
|
-
get b() {
|
|
592
|
-
called.push("b");
|
|
593
|
-
return obj;
|
|
594
|
-
},
|
|
595
|
-
};
|
|
596
|
-
},
|
|
597
|
-
};
|
|
598
|
-
const cb: { current?: () => any } = {};
|
|
599
|
-
const register = (fn: () => any) => {
|
|
600
|
-
cb.current = fn;
|
|
601
|
-
};
|
|
621
|
+
const data = arena.sync({ count: 0, message: "" });
|
|
622
|
+
arena.expose({ data });
|
|
602
623
|
|
|
603
|
-
|
|
604
|
-
|
|
624
|
+
// Module code can have side effects on exposed globals
|
|
625
|
+
arena.evalModule(`
|
|
626
|
+
data.count = 42;
|
|
627
|
+
data.message = "Hello from module";
|
|
628
|
+
`);
|
|
605
629
|
|
|
606
|
-
|
|
607
|
-
expect(
|
|
608
|
-
expect(called).toEqual(["a", "b"]);
|
|
630
|
+
expect(data.count).toBe(42);
|
|
631
|
+
expect(data.message).toBe("Hello from module");
|
|
609
632
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
633
|
+
arena.dispose();
|
|
634
|
+
ctx.dispose();
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
test("module with function side effects", async () => {
|
|
638
|
+
const ctx = (await getQuickJS()).newContext();
|
|
639
|
+
const arena = new Arena(ctx, { isMarshalable: true });
|
|
640
|
+
|
|
641
|
+
const results: number[] = [];
|
|
642
|
+
arena.expose({
|
|
643
|
+
results,
|
|
644
|
+
push: (value: number) => results.push(value),
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
arena.evalModule(`
|
|
648
|
+
push(1);
|
|
649
|
+
push(2);
|
|
650
|
+
push(3);
|
|
651
|
+
`);
|
|
652
|
+
|
|
653
|
+
expect(results).toEqual([1, 2, 3]);
|
|
613
654
|
|
|
614
655
|
arena.dispose();
|
|
615
|
-
|
|
656
|
+
ctx.dispose();
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test("module can define and use internal exports", async () => {
|
|
660
|
+
const ctx = (await getQuickJS()).newContext();
|
|
661
|
+
const arena = new Arena(ctx, { isMarshalable: true });
|
|
662
|
+
|
|
663
|
+
const output = arena.sync({ value: 0 });
|
|
664
|
+
arena.expose({ output });
|
|
665
|
+
|
|
666
|
+
// Module can export and use its own exports internally
|
|
667
|
+
arena.evalModule(`
|
|
668
|
+
export function double(x) {
|
|
669
|
+
return x * 2;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
export const value = 21;
|
|
673
|
+
|
|
674
|
+
// Use the exports internally
|
|
675
|
+
output.value = double(value);
|
|
676
|
+
`);
|
|
677
|
+
|
|
678
|
+
expect(output.value).toBe(42);
|
|
679
|
+
|
|
680
|
+
arena.dispose();
|
|
681
|
+
ctx.dispose();
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
test("module with custom filename", async () => {
|
|
685
|
+
const ctx = (await getQuickJS()).newContext();
|
|
686
|
+
const arena = new Arena(ctx, { isMarshalable: true });
|
|
687
|
+
|
|
688
|
+
const state = arena.sync({ executed: false });
|
|
689
|
+
arena.expose({ state });
|
|
690
|
+
|
|
691
|
+
// Test that custom filename doesn't break functionality
|
|
692
|
+
arena.evalModule(`state.executed = true;`, "custom-module.js");
|
|
693
|
+
|
|
694
|
+
expect(state.executed).toBe(true);
|
|
695
|
+
|
|
696
|
+
arena.dispose();
|
|
697
|
+
ctx.dispose();
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
test("module with strict mode", async () => {
|
|
701
|
+
const ctx = (await getQuickJS()).newContext();
|
|
702
|
+
const arena = new Arena(ctx, { isMarshalable: true });
|
|
703
|
+
|
|
704
|
+
const result = arena.sync({ ok: false });
|
|
705
|
+
arena.expose({ result });
|
|
706
|
+
|
|
707
|
+
// Modules are strict by default
|
|
708
|
+
arena.evalModule(`
|
|
709
|
+
// This would fail in strict mode if we tried: undeclaredVariable = 1;
|
|
710
|
+
result.ok = true;
|
|
711
|
+
`);
|
|
712
|
+
|
|
713
|
+
expect(result.ok).toBe(true);
|
|
714
|
+
|
|
715
|
+
arena.dispose();
|
|
716
|
+
ctx.dispose();
|
|
717
|
+
});
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
describe("memory management", () => {
|
|
721
|
+
test("getMemoryUsage returns statistics", async () => {
|
|
722
|
+
const ctx = (await getQuickJS()).newContext();
|
|
723
|
+
const arena = new Arena(ctx, { isMarshalable: true });
|
|
724
|
+
|
|
725
|
+
const stats = arena.getMemoryUsage();
|
|
726
|
+
|
|
727
|
+
// Check that key properties exist
|
|
728
|
+
expect(stats).toHaveProperty("memory_used_size");
|
|
729
|
+
expect(stats).toHaveProperty("malloc_limit");
|
|
730
|
+
expect(stats).toHaveProperty("obj_count");
|
|
731
|
+
expect(stats).toHaveProperty("malloc_count");
|
|
732
|
+
|
|
733
|
+
// Memory should be used
|
|
734
|
+
expect(stats.memory_used_size).toBeGreaterThan(0);
|
|
735
|
+
expect(stats.malloc_count).toBeGreaterThan(0);
|
|
736
|
+
|
|
737
|
+
arena.dispose();
|
|
738
|
+
ctx.dispose();
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
test("getMemoryUsage tracks allocations", async () => {
|
|
742
|
+
const ctx = (await getQuickJS()).newContext();
|
|
743
|
+
const arena = new Arena(ctx, { isMarshalable: true });
|
|
744
|
+
|
|
745
|
+
const before = arena.getMemoryUsage();
|
|
746
|
+
|
|
747
|
+
// Allocate some objects
|
|
748
|
+
arena.evalCode(`
|
|
749
|
+
const arr = [];
|
|
750
|
+
for (let i = 0; i < 100; i++) {
|
|
751
|
+
arr.push({ id: i, data: "test".repeat(10) });
|
|
752
|
+
}
|
|
753
|
+
`);
|
|
754
|
+
|
|
755
|
+
const after = arena.getMemoryUsage();
|
|
756
|
+
|
|
757
|
+
// Memory usage should increase
|
|
758
|
+
expect(after.memory_used_size).toBeGreaterThan(before.memory_used_size);
|
|
759
|
+
expect(after.obj_count).toBeGreaterThan(before.obj_count);
|
|
760
|
+
|
|
761
|
+
arena.dispose();
|
|
762
|
+
ctx.dispose();
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
test("dumpMemoryUsage returns formatted string", async () => {
|
|
766
|
+
const ctx = (await getQuickJS()).newContext();
|
|
767
|
+
const arena = new Arena(ctx, { isMarshalable: true });
|
|
768
|
+
|
|
769
|
+
const dump = arena.dumpMemoryUsage();
|
|
770
|
+
|
|
771
|
+
// Should be a non-empty string
|
|
772
|
+
expect(typeof dump).toBe("string");
|
|
773
|
+
expect(dump.length).toBeGreaterThan(0);
|
|
774
|
+
|
|
775
|
+
// Should contain key memory metrics (the format varies)
|
|
776
|
+
expect(dump).toMatch(/memory|malloc/i);
|
|
777
|
+
|
|
778
|
+
arena.dispose();
|
|
779
|
+
ctx.dispose();
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
test("setMemoryLimit enforces memory constraints", async () => {
|
|
783
|
+
const ctx = (await getQuickJS()).newContext();
|
|
784
|
+
const arena = new Arena(ctx, { isMarshalable: true });
|
|
785
|
+
|
|
786
|
+
// Set a low memory limit (100KB)
|
|
787
|
+
arena.setMemoryLimit(100 * 1024);
|
|
788
|
+
|
|
789
|
+
// Verify limit is set
|
|
790
|
+
const stats = arena.getMemoryUsage();
|
|
791
|
+
expect(stats.malloc_limit).toBe(100 * 1024);
|
|
792
|
+
|
|
793
|
+
// Try to allocate too much memory
|
|
794
|
+
expect(() => {
|
|
795
|
+
arena.evalCode(`
|
|
796
|
+
const huge = [];
|
|
797
|
+
for (let i = 0; i < 1000000; i++) {
|
|
798
|
+
huge.push({ data: "x".repeat(1000) });
|
|
799
|
+
}
|
|
800
|
+
`);
|
|
801
|
+
}).toThrow();
|
|
802
|
+
|
|
803
|
+
arena.dispose();
|
|
804
|
+
ctx.dispose();
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
test("setMemoryLimit can be removed with -1", async () => {
|
|
808
|
+
const ctx = (await getQuickJS()).newContext();
|
|
809
|
+
const arena = new Arena(ctx, { isMarshalable: true });
|
|
810
|
+
|
|
811
|
+
// Set a limit
|
|
812
|
+
arena.setMemoryLimit(100 * 1024);
|
|
813
|
+
expect(arena.getMemoryUsage().malloc_limit).toBe(100 * 1024);
|
|
814
|
+
|
|
815
|
+
// Remove the limit
|
|
816
|
+
arena.setMemoryLimit(-1);
|
|
817
|
+
const stats = arena.getMemoryUsage();
|
|
818
|
+
expect(stats.malloc_limit).toBeGreaterThan(100 * 1024);
|
|
819
|
+
|
|
820
|
+
arena.dispose();
|
|
821
|
+
ctx.dispose();
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
test("setMaxStackSize prevents stack overflow", async () => {
|
|
825
|
+
const ctx = (await getQuickJS()).newContext();
|
|
826
|
+
const arena = new Arena(ctx, { isMarshalable: true });
|
|
827
|
+
|
|
828
|
+
// Set a small stack size (256KB)
|
|
829
|
+
arena.setMaxStackSize(256 * 1024);
|
|
830
|
+
|
|
831
|
+
// Try to cause stack overflow with deep recursion
|
|
832
|
+
expect(() => {
|
|
833
|
+
arena.evalCode(`
|
|
834
|
+
function recurse(n) {
|
|
835
|
+
if (n > 0) return recurse(n - 1);
|
|
836
|
+
return n;
|
|
837
|
+
}
|
|
838
|
+
recurse(100000);
|
|
839
|
+
`);
|
|
840
|
+
}).toThrow();
|
|
841
|
+
|
|
842
|
+
arena.dispose();
|
|
843
|
+
ctx.dispose();
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
test("setMaxStackSize can be removed with 0", async () => {
|
|
847
|
+
const ctx = (await getQuickJS()).newContext();
|
|
848
|
+
const arena = new Arena(ctx, { isMarshalable: true });
|
|
849
|
+
|
|
850
|
+
// Set a very small stack
|
|
851
|
+
arena.setMaxStackSize(128 * 1024);
|
|
852
|
+
|
|
853
|
+
// This should fail with small stack
|
|
854
|
+
expect(() => {
|
|
855
|
+
arena.evalCode(`
|
|
856
|
+
function recurse(n) {
|
|
857
|
+
if (n > 0) return recurse(n - 1);
|
|
858
|
+
return n;
|
|
859
|
+
}
|
|
860
|
+
recurse(10000);
|
|
861
|
+
`);
|
|
862
|
+
}).toThrow();
|
|
863
|
+
|
|
864
|
+
// Remove the limit
|
|
865
|
+
arena.setMaxStackSize(0);
|
|
866
|
+
|
|
867
|
+
// Now it should work (or at least get further)
|
|
868
|
+
const result = arena.evalCode(`
|
|
869
|
+
function recurse(n) {
|
|
870
|
+
if (n > 0) return recurse(n - 1);
|
|
871
|
+
return n;
|
|
872
|
+
}
|
|
873
|
+
recurse(1000);
|
|
874
|
+
`);
|
|
875
|
+
|
|
876
|
+
expect(result).toBe(0);
|
|
877
|
+
|
|
878
|
+
arena.dispose();
|
|
879
|
+
ctx.dispose();
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
test("memory limits work with marshaling", async () => {
|
|
883
|
+
const ctx = (await getQuickJS()).newContext();
|
|
884
|
+
const arena = new Arena(ctx, { isMarshalable: true });
|
|
885
|
+
|
|
886
|
+
// Set limit
|
|
887
|
+
arena.setMemoryLimit(200 * 1024);
|
|
888
|
+
|
|
889
|
+
const data = arena.sync({ items: [] as any[] });
|
|
890
|
+
arena.expose({ data });
|
|
891
|
+
|
|
892
|
+
// Should be able to add reasonable data
|
|
893
|
+
arena.evalCode(`
|
|
894
|
+
for (let i = 0; i < 10; i++) {
|
|
895
|
+
data.items.push({ id: i });
|
|
896
|
+
}
|
|
897
|
+
`);
|
|
898
|
+
|
|
899
|
+
expect(data.items.length).toBe(10);
|
|
900
|
+
|
|
901
|
+
// But not unlimited data
|
|
902
|
+
expect(() => {
|
|
903
|
+
arena.evalCode(`
|
|
904
|
+
for (let i = 0; i < 100000; i++) {
|
|
905
|
+
data.items.push({ id: i, data: "x".repeat(1000) });
|
|
906
|
+
}
|
|
907
|
+
`);
|
|
908
|
+
}).toThrow();
|
|
909
|
+
|
|
910
|
+
arena.dispose();
|
|
911
|
+
ctx.dispose();
|
|
616
912
|
});
|
|
617
913
|
});
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
VmCallResult,
|
|
7
7
|
} from "quickjs-emscripten";
|
|
8
8
|
|
|
9
|
+
import { wrapContext, QuickJSContextEx } from "./contextex";
|
|
9
10
|
import { defaultRegisteredObjects } from "./default";
|
|
10
11
|
import marshal from "./marshal";
|
|
11
12
|
import unmarshal from "./unmarshal";
|
|
@@ -48,13 +49,15 @@ export type Options = {
|
|
|
48
49
|
isHandleWrappable?: (handle: QuickJSHandle, ctx: QuickJSContext) => boolean;
|
|
49
50
|
/** Compatibility with quickjs-emscripten prior to v0.15. Inject code for compatibility into context at Arena class initialization time. */
|
|
50
51
|
compat?: boolean;
|
|
52
|
+
/** Experimental: use QuickJSContextEx, which wraps existing QuickJSContext. */
|
|
53
|
+
experimentalContextEx?: boolean;
|
|
51
54
|
};
|
|
52
55
|
|
|
53
56
|
/**
|
|
54
57
|
* The Arena class manages all generated handles at once by quickjs-emscripten and automatically converts objects between the host and the QuickJS VM.
|
|
55
58
|
*/
|
|
56
59
|
export class Arena {
|
|
57
|
-
context:
|
|
60
|
+
context: QuickJSContextEx;
|
|
58
61
|
_map: VMMap;
|
|
59
62
|
_registeredMap: VMMap;
|
|
60
63
|
_registeredMapDispose: Set<any> = new Set();
|
|
@@ -74,7 +77,7 @@ export class Arena {
|
|
|
74
77
|
};
|
|
75
78
|
}
|
|
76
79
|
|
|
77
|
-
this.context = ctx;
|
|
80
|
+
this.context = options?.experimentalContextEx ? wrapContext(ctx) : ctx;
|
|
78
81
|
this._options = options;
|
|
79
82
|
this._symbolHandle = ctx.unwrapResult(ctx.evalCode(`Symbol()`));
|
|
80
83
|
this._map = new VMMap(ctx);
|
|
@@ -89,6 +92,7 @@ export class Arena {
|
|
|
89
92
|
this._map.dispose();
|
|
90
93
|
this._registeredMap.dispose();
|
|
91
94
|
this._symbolHandle.dispose();
|
|
95
|
+
this.context.disposeEx?.();
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
/**
|
|
@@ -99,6 +103,27 @@ export class Arena {
|
|
|
99
103
|
return this._unwrapResultAndUnmarshal(handle);
|
|
100
104
|
}
|
|
101
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Evaluate ES module code in the VM. The module can import/export, but the return value
|
|
108
|
+
* depends on whether the module exports are accessible (implementation varies by quickjs-emscripten version).
|
|
109
|
+
* Use this for side effects or when you don't need access to exports.
|
|
110
|
+
*
|
|
111
|
+
* @param code - The ES module code to evaluate
|
|
112
|
+
* @param filename - Optional filename for debugging purposes (default: "module.js")
|
|
113
|
+
* @returns Undefined in most cases (module side effects are applied)
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```js
|
|
117
|
+
* // Execute module code with side effects
|
|
118
|
+
* arena.expose({ data: { count: 0 } });
|
|
119
|
+
* arena.evalModule('import { data } from "globals"; data.count = 42;'); // undefined, but data.count is now 42
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
evalModule(code: string, filename = "module.js"): void {
|
|
123
|
+
const handle = this.context.evalCode(code, filename, { type: "module" });
|
|
124
|
+
this._unwrapResultAndUnmarshal(handle);
|
|
125
|
+
}
|
|
126
|
+
|
|
102
127
|
/**
|
|
103
128
|
* Almost same as `vm.executePendingJobs()`, but it converts and re-throws error objects when an error is thrown during evaluation.
|
|
104
129
|
*/
|
|
@@ -110,6 +135,121 @@ export class Arena {
|
|
|
110
135
|
throw this._unwrapIfNotSynced(result.error.consume(this._unmarshal));
|
|
111
136
|
}
|
|
112
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Set the max memory this runtime can allocate.
|
|
140
|
+
* To remove the limit, set to `-1`.
|
|
141
|
+
*
|
|
142
|
+
* This is useful for preventing runaway memory usage in untrusted code.
|
|
143
|
+
*
|
|
144
|
+
* @param limitBytes - Maximum memory in bytes, or -1 to remove limit
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```js
|
|
148
|
+
* // Limit sandbox to 10MB
|
|
149
|
+
* arena.setMemoryLimit(10 * 1024 * 1024);
|
|
150
|
+
*
|
|
151
|
+
* try {
|
|
152
|
+
* arena.evalCode(`const huge = new Array(1000000000);`);
|
|
153
|
+
* } catch (e) {
|
|
154
|
+
* console.log("Memory limit exceeded");
|
|
155
|
+
* }
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
setMemoryLimit(limitBytes: number): void {
|
|
159
|
+
this.context.runtime.setMemoryLimit(limitBytes);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Set the max stack size for this runtime, in bytes.
|
|
164
|
+
* To remove the limit, set to `0`.
|
|
165
|
+
*
|
|
166
|
+
* This is useful for preventing stack overflow from deeply nested calls or recursion.
|
|
167
|
+
*
|
|
168
|
+
* @param stackSize - Maximum stack size in bytes, or 0 to remove limit
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```js
|
|
172
|
+
* // Limit stack to 512KB
|
|
173
|
+
* arena.setMaxStackSize(512 * 1024);
|
|
174
|
+
*
|
|
175
|
+
* try {
|
|
176
|
+
* arena.evalCode(`function recurse() { recurse(); } recurse();`);
|
|
177
|
+
* } catch (e) {
|
|
178
|
+
* console.log("Stack overflow prevented");
|
|
179
|
+
* }
|
|
180
|
+
* ```
|
|
181
|
+
*/
|
|
182
|
+
setMaxStackSize(stackSize: number): void {
|
|
183
|
+
this.context.runtime.setMaxStackSize(stackSize);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get detailed memory usage statistics for this runtime.
|
|
188
|
+
*
|
|
189
|
+
* @returns An object containing detailed memory allocation information
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```js
|
|
193
|
+
* const stats = arena.getMemoryUsage();
|
|
194
|
+
* console.log(`Memory used: ${stats.memory_used_size} bytes`);
|
|
195
|
+
* console.log(`Object count: ${stats.obj_count}`);
|
|
196
|
+
* console.log(`Memory limit: ${stats.malloc_limit}`);
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
199
|
+
getMemoryUsage(): {
|
|
200
|
+
malloc_limit: number;
|
|
201
|
+
memory_used_size: number;
|
|
202
|
+
malloc_count: number;
|
|
203
|
+
memory_used_count: number;
|
|
204
|
+
atom_count: number;
|
|
205
|
+
atom_size: number;
|
|
206
|
+
str_count: number;
|
|
207
|
+
str_size: number;
|
|
208
|
+
obj_count: number;
|
|
209
|
+
obj_size: number;
|
|
210
|
+
prop_count: number;
|
|
211
|
+
prop_size: number;
|
|
212
|
+
shape_count: number;
|
|
213
|
+
shape_size: number;
|
|
214
|
+
js_func_count: number;
|
|
215
|
+
js_func_size: number;
|
|
216
|
+
js_func_code_size: number;
|
|
217
|
+
js_func_pc2line_count: number;
|
|
218
|
+
js_func_pc2line_size: number;
|
|
219
|
+
c_func_count: number;
|
|
220
|
+
array_count: number;
|
|
221
|
+
fast_array_count: number;
|
|
222
|
+
fast_array_elements: number;
|
|
223
|
+
binary_object_count: number;
|
|
224
|
+
binary_object_size: number;
|
|
225
|
+
} {
|
|
226
|
+
const handle = this.context.runtime.computeMemoryUsage();
|
|
227
|
+
try {
|
|
228
|
+
return this.context.dump(handle);
|
|
229
|
+
} finally {
|
|
230
|
+
handle.dispose();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get a human-readable description of memory usage in this runtime.
|
|
236
|
+
*
|
|
237
|
+
* @returns A formatted string showing memory statistics
|
|
238
|
+
*
|
|
239
|
+
* @example
|
|
240
|
+
* ```js
|
|
241
|
+
* console.log(arena.dumpMemoryUsage());
|
|
242
|
+
* // Output:
|
|
243
|
+
* // QuickJS memory usage:
|
|
244
|
+
* // malloc_limit: 4294967295
|
|
245
|
+
* // memory_used_size: 67078
|
|
246
|
+
* // ...
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
dumpMemoryUsage(): string {
|
|
250
|
+
return this.context.runtime.dumpMemoryUsage();
|
|
251
|
+
}
|
|
252
|
+
|
|
113
253
|
/**
|
|
114
254
|
* Expose objects as global objects in the VM.
|
|
115
255
|
*
|
|
@@ -153,7 +153,7 @@ test("class", async () => {
|
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
expect(A.name).toBe("
|
|
156
|
+
expect(A.name).toBe("A");
|
|
157
157
|
const handle = marshal(A);
|
|
158
158
|
if (!map) throw new Error("map is undefined");
|
|
159
159
|
|
|
@@ -167,7 +167,7 @@ test("class", async () => {
|
|
|
167
167
|
|
|
168
168
|
expect(ctx.typeof(handle)).toBe("function");
|
|
169
169
|
expect(ctx.dump(ctx.getProp(handle, "length"))).toBe(1);
|
|
170
|
-
expect(ctx.dump(ctx.getProp(handle, "name"))).toBe("
|
|
170
|
+
expect(ctx.dump(ctx.getProp(handle, "name"))).toBe("A");
|
|
171
171
|
const staticA = ctx.getProp(handle, "a");
|
|
172
172
|
expect(instanceOf(ctx, staticA, handle)).toBe(true);
|
|
173
173
|
expect(ctx.dump(ctx.getProp(staticA, "a"))).toBe(100);
|
package/src/vmmap.ts
CHANGED
package/src/wrapper.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { QuickJSHandle, QuickJSContext } from "quickjs-emscripten";
|
|
2
2
|
|
|
3
3
|
import { isObject } from "./util";
|
|
4
|
-
import { call, isHandleObject,
|
|
4
|
+
import { call, isHandleObject, mayConsumeAll } from "./vmutil";
|
|
5
5
|
|
|
6
6
|
export type SyncMode = "both" | "vm" | "host";
|
|
7
7
|
|
|
@@ -86,32 +86,45 @@ export function wrapHandle(
|
|
|
86
86
|
|
|
87
87
|
if (isHandleWrapped(ctx, handle, proxyKeySymbolHandle)) return [handle, false];
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
89
|
+
const getSyncMode = (h: QuickJSHandle) => {
|
|
90
|
+
const res = syncMode?.(unmarshal(h));
|
|
91
|
+
if (typeof res === "string") return ctx.newString(res);
|
|
92
|
+
return ctx.undefined;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const setter = (h: QuickJSHandle, keyHandle: QuickJSHandle, valueHandle: QuickJSHandle) => {
|
|
96
|
+
const target = unmarshal(h);
|
|
97
|
+
if (!target) return;
|
|
98
|
+
const key = unmarshal(keyHandle);
|
|
99
|
+
if (key === "__proto__") return; // for security
|
|
100
|
+
const value = unmarshal(valueHandle);
|
|
101
|
+
unwrap(target, proxyKeySymbol)[key] = value;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const deleter = (h: QuickJSHandle, keyHandle: QuickJSHandle) => {
|
|
105
|
+
const target = unmarshal(h);
|
|
106
|
+
if (!target) return;
|
|
107
|
+
const key = unmarshal(keyHandle);
|
|
108
|
+
delete unwrap(target, proxyKeySymbol)[key];
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return ctx
|
|
112
|
+
.newFunction("proxyFuncs", (t, ...args) => {
|
|
113
|
+
const name = ctx.getNumber(t);
|
|
114
|
+
switch (name) {
|
|
115
|
+
case 1:
|
|
116
|
+
return getSyncMode(args[0]);
|
|
117
|
+
case 2:
|
|
118
|
+
return setter(args[0], args[1], args[2]);
|
|
119
|
+
case 3:
|
|
120
|
+
return deleter(args[0], args[1]);
|
|
121
|
+
}
|
|
122
|
+
return ctx.undefined;
|
|
123
|
+
})
|
|
124
|
+
.consume(proxyFuncs => [
|
|
112
125
|
call(
|
|
113
126
|
ctx,
|
|
114
|
-
`(target, sym,
|
|
127
|
+
`(target, sym, proxyFuncs) => {
|
|
115
128
|
const rec = new Proxy(target, {
|
|
116
129
|
get(obj, key, receiver) {
|
|
117
130
|
return key === sym ? obj : Reflect.get(obj, key, receiver)
|
|
@@ -120,19 +133,19 @@ export function wrapHandle(
|
|
|
120
133
|
const v = typeof value === "object" && value !== null || typeof value === "function"
|
|
121
134
|
? value[sym] ?? value
|
|
122
135
|
: value;
|
|
123
|
-
const sync =
|
|
136
|
+
const sync = proxyFuncs(1, receiver) ?? "vm";
|
|
124
137
|
if (sync === "host" || Reflect.set(obj, key, v, receiver)) {
|
|
125
138
|
if (sync !== "vm") {
|
|
126
|
-
|
|
139
|
+
proxyFuncs(2, receiver, key, v);
|
|
127
140
|
}
|
|
128
141
|
}
|
|
129
142
|
return true;
|
|
130
143
|
},
|
|
131
144
|
deleteProperty(obj, key) {
|
|
132
|
-
const sync =
|
|
145
|
+
const sync = proxyFuncs(1, rec) ?? "vm";
|
|
133
146
|
if (sync === "host" || Reflect.deleteProperty(obj, key)) {
|
|
134
147
|
if (sync !== "vm") {
|
|
135
|
-
|
|
148
|
+
proxyFuncs(3, rec, key);
|
|
136
149
|
}
|
|
137
150
|
}
|
|
138
151
|
return true;
|
|
@@ -143,11 +156,10 @@ export function wrapHandle(
|
|
|
143
156
|
undefined,
|
|
144
157
|
handle,
|
|
145
158
|
proxyKeySymbolHandle,
|
|
146
|
-
|
|
159
|
+
proxyFuncs,
|
|
147
160
|
) as Wrapped<QuickJSHandle>,
|
|
148
161
|
true,
|
|
149
|
-
]
|
|
150
|
-
);
|
|
162
|
+
]);
|
|
151
163
|
}
|
|
152
164
|
|
|
153
165
|
export function unwrap<T>(obj: T, key: string | symbol): T {
|