quickjs-emscripten-sync 1.9.1 → 1.11.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/README.md +54 -0
- package/dist/index.d.ts +99 -3
- package/dist/quickjs-emscripten-sync.js +742 -559
- package/dist/quickjs-emscripten-sync.umd.cjs +33 -14
- package/package.json +7 -7
- package/src/contextex.ts +19 -7
- package/src/edge.test.ts +44 -2
- package/src/faultinjection.test.ts +265 -0
- package/src/identity.test.ts +57 -0
- package/src/index.test.ts +302 -0
- package/src/index.ts +215 -41
- package/src/marshal/custom.ts +20 -11
- package/src/marshal/function.test.ts +42 -18
- package/src/marshal/function.ts +106 -40
- package/src/marshal/index.ts +25 -1
- package/src/marshal/mapset.ts +30 -16
- package/src/marshal/object.ts +23 -14
- package/src/marshal/promise.test.ts +13 -11
- package/src/marshal/promise.ts +14 -6
- package/src/marshal/properties.test.ts +14 -5
- package/src/marshal/properties.ts +60 -12
- package/src/remarshalleak.test.ts +50 -0
- package/src/unmarshal/custom.test.ts +6 -3
- package/src/unmarshal/function.test.ts +46 -20
- package/src/unmarshal/index.test.ts +19 -10
- package/src/unmarshal/mapset.ts +24 -9
- package/src/unmarshal/object.test.ts +18 -9
- package/src/unmarshal/promise.test.ts +5 -4
- package/src/unmarshal/promise.ts +6 -2
- package/src/unmarshal/properties.test.ts +3 -2
- package/src/unmarshal/properties.ts +70 -36
- package/src/vmmap.ts +19 -7
- package/src/vmutil.test.ts +2 -1
- package/src/wrapper.test.ts +5 -3
- package/src/wrapper.ts +144 -37
|
@@ -1,23 +1,33 @@
|
|
|
1
|
-
(function(
|
|
1
|
+
(function(v,g){typeof exports=="object"&&typeof module!="undefined"?g(exports):typeof define=="function"&&define.amd?define(["exports"],g):(v=typeof globalThis!="undefined"?globalThis:v||self,g(v.QuickjsEmscriptenSync={}))})(this,(function(v){"use strict";var De=Object.defineProperty,Ve=Object.defineProperties;var Ue=Object.getOwnPropertyDescriptors;var ne=Object.getOwnPropertySymbols;var Ge=Object.prototype.hasOwnProperty,Je=Object.prototype.propertyIsEnumerable;var W=(v,g,P)=>g in v?De(v,g,{enumerable:!0,configurable:!0,writable:!0,value:P}):v[g]=P,z=(v,g)=>{for(var P in g||(g={}))Ge.call(g,P)&&W(v,P,g[P]);if(ne)for(var P of ne(g))Je.call(g,P)&&W(v,P,g[P]);return v},re=(v,g)=>Ve(v,Ue(g));var d=(v,g,P)=>W(v,typeof g!="symbol"?g+"":g,P);var $=(v,g,P)=>new Promise((E,F)=>{var N=O=>{try{A(P.next(O))}catch(I){F(I)}},_=O=>{try{A(P.throw(O))}catch(I){F(I)}},A=O=>O.done?E(O.value):Promise.resolve(O.value).then(N,_);A((P=P.apply(v,g)).next())});function g(n,e){if("error"in e&&e.error){const{error:t}=e;let r;try{r=n.dump(t)}catch(i){}finally{t.alive&&t.dispose()}const s=new Error(typeof r=="object"&&r&&"message"in r?String(r.message):r!==void 0?String(r):"quickjs-emscripten-sync: VM evaluation failed");throw typeof r=="object"&&r&&("name"in r&&(s.name=String(r.name)),"stack"in r&&(s.stack=String(r.stack))),s}return e.value}function P(n,e){const t=g(n,n.evalCode(e)),r=(i,...o)=>g(n,n.callFunction(t,i!=null?i:n.undefined,...o)),s=()=>t.dispose();return r.dispose=s,r[Symbol.dispose]=s,Object.defineProperty(r,"alive",{get:()=>t.alive}),r}const E=new WeakMap;function F(n){E.has(n)||E.set(n,new Map)}function N(n){const e=E.get(n);if(e){for(const t of e.values())t.alive&&t.dispose();E.delete(n)}}function _(n,e,t,...r){const s=E.get(n);if(!s){const o=P(n,e);try{return o(t,...r)}finally{o.dispose()}}let i=s.get(e);return(!i||!i.alive)&&(i=g(n,n.evalCode(e)),s.set(e,i)),g(n,n.callFunction(i,t!=null?t:n.undefined,...r))}function A(n,e,t){return n.dump(_(n,"(a, b) => a instanceof b",void 0,e,t))}function O(n,e){const t=n.typeof(e);return t==="function"||t==="object"&&!n.sameValue(e,n.null)}function I(n,e){const t=JSON.stringify(e);return t?n.newString(t).consume(r=>_(n,"JSON.parse",void 0,r)):n.undefined}function H(n,e){try{return e(n)}finally{n.alive&&n.dispose()}}function D(n,e){try{return e(n)}finally{for(const t of n)t.alive&&t.dispose()}}function se([n,e],t){try{return t(n)}finally{e&&n.dispose()}}function C(n,e){try{return e(...n.map(t=>t[0]))}finally{for(const[t,r]of n)r&&t.dispose()}}function ie(n){return"handle"in n}function V(n){return ie(n)?n.handle:n}const oe=n=>{const e=new ae(n);return new Proxy(n,{get(t,r,s){return r in e?e[r]:Reflect.get(t,r,s)}})};class ae{constructor(e){d(this,"context");d(this,"fn");d(this,"fnGenerator");d(this,"fnCounter",Number.MIN_SAFE_INTEGER);d(this,"fnMap",new Map);d(this,"newFunction",(e,t)=>{this.fnCounter++;const r=this.fnCounter;return this.fnMap.set(r,t),D([this.context.newString(e),this.context.newNumber(t.length),this.context.newNumber(r)],([s,i,o])=>this.context.unwrapResult(this.context.callFunction(this.fnGenerator,this.context.undefined,s,i,o,this.fn)))});this.context=e;const t=this.fnMap;this.fn=this.context.newFunction("",function(r,...s){const i=e.getNumber(r),o=t.get(i);if(!o)throw new Error("function is not registered");return o.call(this,...s)}),this.fnGenerator=e.unwrapResult(e.evalCode(`((name, length, id, f) => {
|
|
2
2
|
const fn = function(...args) {
|
|
3
3
|
return f.call(this, id, ...args);
|
|
4
4
|
};
|
|
5
5
|
fn.name = name;
|
|
6
6
|
fn.length = length;
|
|
7
7
|
return fn;
|
|
8
|
-
})`))}disposeEx(){this.fnGenerator.dispose(),this.fn.dispose()}}const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
8
|
+
})`))}disposeEx(){this.fnGenerator.dispose(),this.fn.dispose()}}const K=[[Symbol,"Symbol"],[Symbol.prototype,"Symbol.prototype"],[Object,"Object"],[Object.prototype,"Object.prototype"],[Function,"Function"],[Function.prototype,"Function.prototype"],[Boolean,"Boolean"],[Boolean.prototype,"Boolean.prototype"],[Array,"Array"],[Array.prototype,"Array.prototype"],[Error,"Error"],[Error.prototype,"Error.prototype"],[EvalError,"EvalError"],[EvalError.prototype,"EvalError.prototype"],[RangeError,"RangeError"],[RangeError.prototype,"RangeError.prototype"],[ReferenceError,"ReferenceError"],[ReferenceError.prototype,"ReferenceError.prototype"],[SyntaxError,"SyntaxError"],[SyntaxError.prototype,"SyntaxError.prototype"],[TypeError,"TypeError"],[TypeError.prototype,"TypeError.prototype"],[URIError,"URIError"],[URIError.prototype,"URIError.prototype"],...Object.getOwnPropertyNames(Symbol).filter(n=>typeof Symbol[n]=="symbol").map(n=>[Symbol[n],`Symbol.${n}`])];function ue(n,e,t,r){var o;let s;for(const a of r)if(s=a(e,n),s)break;if(!s)return;let i=!0;try{const a=(o=t(e,s))!=null?o:s;return i=!1,a}finally{i&&s.alive&&s.dispose()}}function fe(n,e){if(typeof n=="symbol")return n.description?H(e.newString(n.description),t=>_(e,"d => Symbol(d)",void 0,t)):_(e,"d => Symbol(d)",void 0,e.undefined)}function le(n,e){if(n instanceof Date)return H(e.newNumber(n.getTime()),t=>_(e,"d => new Date(d)",void 0,t))}function ce(n,e){if(n instanceof ArrayBuffer)return e.newArrayBuffer(n.slice(0));if(ArrayBuffer.isView(n)){const t=new Uint8Array(n.buffer,n.byteOffset,n.byteLength).slice();return D([e.newArrayBuffer(t.buffer),e.newString(n.constructor.name)],([r,s])=>_(e,"(buf, name) => new globalThis[name](buf)",void 0,r,s))}}const pe=[fe,le,ce];function L(n){return typeof n=="function"&&/^class\s/.test(Function.prototype.toString.call(n))}function R(n){return typeof n=="function"||typeof n=="object"&&n!==null}function U(n,e){const t=new Set,r=s=>{if(!(!R(s)||t.has(s)||(e==null?void 0:e(s,t))===!1)){if(t.add(s),Array.isArray(s)){for(const i of s)r(i);return}if(typeof s=="object"){const i=Object.getPrototypeOf(s);i&&i!==Object.prototype&&r(i)}for(const i of Object.values(Object.getOwnPropertyDescriptors(s)))"value"in i&&r(i.value),"get"in i&&r(i.get),"set"in i&&r(i.set)}};return r(n),t}function de(n,e){return U(n,e?(t,r)=>r.size<e:void 0).size}function ye(){let n=()=>{},e=()=>{};return{promise:new Promise((r,s)=>{n=r,e=s}),resolve:n,reject:e}}function Q(n,e,t,r,s=()=>{}){let i;const o=[],a=Object.getPrototypeOf(e),u=a===Object.prototype||a===Array.prototype,l=(f,c)=>{if(u&&typeof f=="string"&&f!=="__proto__"&&c.writable===!0&&c.enumerable===!0&&c.configurable===!0&&c.get===void 0&&c.set===void 0){const b=r(f),j=r(c.value);n.setProp(t,b,j),s(j);return}const m=r(f),p=typeof c.value=="undefined"?void 0:r(c.value),y=typeof c.get=="undefined"?void 0:r(c.get),h=typeof c.set=="undefined"?void 0:r(c.set);p&&o.push(p),y&&o.push(y),h&&o.push(h);const S=i!=null?i:i=n.newObject();H(n.newObject(),b=>{Object.entries(c).forEach(([j,w])=>{const k=j==="value"?p:j==="get"?y:j==="set"?h:w?n.true:n.false;k&&n.setProp(b,j,k)}),n.setProp(S,m,b)})};try{const f=Object.getOwnPropertyDescriptors(e);if(Object.entries(f).forEach(([c,m])=>l(c,m)),Object.getOwnPropertySymbols(f).forEach(c=>l(c,f[c])),i){_(n,"Object.defineProperties",void 0,t,i).dispose();for(const c of o)s(c)}}finally{i==null||i.dispose()}}function he(n,e,t,r,s,i,o=()=>{},a=f=>f,u=f=>f,l){var S;if(typeof e!="function")return;const f=u(e),c=L(f),p=!!l&&l(f)&&"newAsyncifiedFunction"in n?n.newAsyncifiedFunction(e.name,function(...b){return $(this,null,function*(){const j=n.sameValue(this,n.global)?void 0:r(this),w=b.map(M=>r(M)),k=yield i?i(e,j,w):e.apply(j,w);return a(t(k))})}):n.newFunction(e.name,function(...b){const j=n.sameValue(this,n.global)?void 0:r(this),w=b.map(k=>r(k));if(c&&R(j)){const k=new e(...w);return Object.entries(k).forEach(([M,Ne])=>{const te=t(Ne);n.setProp(this,M,te),o(te)}),this}return a(t(i?i(e,j,w):e.apply(j,w)))}),y=H(p,b=>_(n,`Cls => {
|
|
9
|
+
const fn = function(...args) { return Cls.apply(this, args); };
|
|
10
|
+
fn.name = Cls.name;
|
|
11
|
+
fn.length = Cls.length;
|
|
12
|
+
return fn;
|
|
13
|
+
}`,void 0,b));let h=!0;try{const b=(S=s(e,y))!=null?S:y;return h=!1,Q(n,e,y,t,o),b}finally{h&&y.alive&&y.dispose()}}function me(n,e,t){if(!(typeof e!="object"&&typeof e!="function")&&e!==null)return t(e,n.newHostRef(e).handle)}function _e(n,e,t){var i;const r=I(n,e);return(i=t(e,r))!=null?i:r}function ve(n,e,t,r,s=()=>{}){var i,o;if(e instanceof Map){const a=_(n,"() => new Map()");let u=!0;try{const l=(i=r(e,a))!=null?i:a;u=!1;for(const[f,c]of e){const m=t(f),p=t(c);_(n,"(m, k, v) => m.set(k, v)",void 0,a,m,p).dispose(),s(m),s(p)}return l}finally{u&&a.alive&&a.dispose()}}if(e instanceof Set){const a=_(n,"() => new Set()");let u=!0;try{const l=(o=r(e,a))!=null?o:a;u=!1;for(const f of e){const c=t(f);_(n,"(s, v) => s.add(v)",void 0,a,c).dispose(),s(c)}return l}finally{u&&a.alive&&a.dispose()}}}function be(n,e,t,r,s=()=>{}){var a;if(typeof e!="object"||e===null)return;const i=Array.isArray(e)?n.newArray():n.newObject();let o=!0;try{const u=(a=r(e,i))!=null?a:i;o=!1;const l=Object.getPrototypeOf(e),f=l&&l!==Object.prototype&&l!==Array.prototype?t(l):void 0;return f&&(_(n,"Object.setPrototypeOf",void 0,u,f).dispose(),s(f)),Q(n,e,i,t,s),u}finally{o&&i.alive&&i.dispose()}}function we(n,e){switch(typeof e){case"undefined":return n.undefined;case"number":return n.newNumber(e);case"string":return n.newString(e);case"boolean":return e?n.true:n.false;case"bigint":return n.newBigInt(e);case"object":return e===null?n.null:void 0}}function ge(n,e,t,r){var o;if(!(e instanceof Promise))return;const s=n.newPromise();let i=!0;try{e.then(u=>s.resolve(t(u)),u=>s.reject(t(u)));const a=(o=r(e,s))!=null?o:s.handle;return i=!1,a}finally{i&&s.alive&&s.dispose()}}function G(n,e){var c,m,p,y,h,S,b,j;const{ctx:t,unmarshal:r,isMarshalable:s,find:i,pre:o}=e;{const w=we(t,n);if(w)return typeof n=="bigint"&&((c=e.registerTransient)==null||c.call(e,w)),w}{const w=i(n);if(w)return w}if((m=e.marshalByReference)!=null&&m.call(e,n)&&e.registerHostRef){const w=me(t,n,e.registerHostRef);if(w)return w}const a=s==null?void 0:s(n);if(a===!1)return t.undefined;const u=(w,k)=>o(w,k,a);if(a==="json")return _e(t,n,u);const l=w=>G(w,e),f=e.disposeTransient;return(j=(b=(S=(h=(y=ue(t,n,u,[...pe,...(p=e.custom)!=null?p:[]]))!=null?y:ge(t,n,l,u))!=null?h:he(t,n,l,r,u,e.preApply,f,e.prepareReturn,e.unwrap,e.asyncify))!=null?S:ve(t,n,l,u,f))!=null?b:be(t,n,l,u,f))!=null?j:t.undefined}function je(n,e,t,r){var i;let s;for(const o of r)if(s=o(e,n),s)break;return s?(i=t(s,e))!=null?i:s:void 0}function Se(n,e){if(e.typeof(n)!=="symbol")return;const t=e.getString(e.getProp(n,"description"));return Symbol(t)}function He(n,e){if(!H(_(e,"a => a instanceof Date",void 0,n),r=>e.dump(r)))return;const t=H(_(e,"a => a.getTime()",void 0,n),r=>e.getNumber(r));return new Date(t)}function Pe(n,e){if(H(_(e,"a => a instanceof ArrayBuffer",void 0,n),t=>e.dump(t))){const t=e.getArrayBuffer(n),r=t.value.slice();return t.dispose(),r.buffer}if(H(_(e,"a => ArrayBuffer.isView(a)",void 0,n),t=>e.dump(t))){const t=H(_(e,"a => a.constructor.name",void 0,n),a=>e.getString(a)),r=globalThis[t];if(typeof r!="function")return;const s=_(e,"a => a.buffer.slice(a.byteOffset, a.byteOffset + a.byteLength)",void 0,n),i=e.getArrayBuffer(s),o=i.value.slice();return i.dispose(),s.dispose(),new r(o.buffer)}}const ke=[Se,He,Pe],Oe=`(o, fn) => {
|
|
14
|
+
const descs = Object.getOwnPropertyDescriptors(o);
|
|
15
|
+
const emit = (k, d) => {
|
|
16
|
+
let f = 0;
|
|
17
|
+
if (typeof d.value !== "undefined") f |= 1;
|
|
18
|
+
if (typeof d.get !== "undefined") f |= 2;
|
|
19
|
+
if (typeof d.set !== "undefined") f |= 4;
|
|
20
|
+
if (typeof d.configurable === "boolean") { f |= 8; if (d.configurable) f |= 16; }
|
|
21
|
+
if (typeof d.enumerable === "boolean") { f |= 32; if (d.enumerable) f |= 64; }
|
|
22
|
+
if (typeof d.writable === "boolean") { f |= 128; if (d.writable) f |= 256; }
|
|
23
|
+
fn(k, d, f);
|
|
24
|
+
};
|
|
25
|
+
Object.entries(descs).forEach(([k, v]) => emit(k, v));
|
|
26
|
+
Object.getOwnPropertySymbols(descs).forEach(k => emit(k, descs[k]));
|
|
27
|
+
}`;function q(n,e,t,r){H(n.newFunction("",(s,i,o)=>{const[a]=r(s);if(typeof a!="string"&&typeof a!="number"&&typeof a!="symbol")return;const u=n.getNumber(o),l={},f=c=>{const m=n.getProp(i,c);let p=!1;try{const[y,h]=r(m);return p=!h,y}finally{!p&&m.alive&&m.dispose()}};u&1&&(l.value=f("value")),u&2&&(l.get=f("get")),u&4&&(l.set=f("set")),u&8&&(l.configurable=!!(u&16)),u&32&&(l.enumerable=!!(u&64)),u&128&&(l.writable=!!(u&256)),Object.defineProperty(t,a,l)}),s=>{_(n,Oe,void 0,e,s).dispose()})}function Re(n,e,t,r,s){var a;if(n.typeof(e)!=="function")return;const i=function(...u){return C([t(this),...u.map(l=>t(l))],(l,...f)=>{if(new.target){const[y]=r(_(n,"(Cls, ...args) => new Cls(...args)",l,e,...f));return Object.defineProperties(this,Object.getOwnPropertyDescriptors(y)),this}const c=g(n,n.callFunction(e,l,...f)),[m,p]=r(c);return p&&c.dispose(),m})},o=(a=s(i,e))!=null?a:i;return q(n,e,i,r),o}function X(n,e){const t=n.toHostRef(e);if(t)try{return{value:t.value}}finally{t.dispose()}}function Te(n,e,t,r){const s=H(_(n,"a => a instanceof Map",void 0,e),u=>n.dump(u)),i=!s&&H(_(n,"a => a instanceof Set",void 0,e),u=>n.dump(u));if(!s&&!i)return;const o=s?new Map:new Set;r(o,e);const a=n.unwrapResult(n.getIterator(e));try{for(const u of a){const l=n.unwrapResult(u);if(s){const f=n.getProp(l,0),c=n.getProp(l,1);let m=!0,p=!0;try{const[y,h]=t(f);m=h;const[S,b]=t(c);p=b,o.set(y,S)}finally{m&&f.alive&&f.dispose(),p&&c.alive&&c.dispose(),l.alive&&l.dispose()}}else{let f=!0;try{const[c,m]=t(l);f=m,o.add(c)}finally{f&&l.alive&&l.dispose()}}}}finally{a.alive&&a.dispose()}return o}function Ee(n,e,t,r){var a;if(n.typeof(e)!=="object"||n.sameValue(e,n.null))return;const s=H(_(n,"Array.isArray",void 0,e),u=>n.dump(u))?[]:{},i=(a=r(s,e))!=null?a:s,o=H(_(n,`o => {
|
|
18
28
|
const p = Object.getPrototypeOf(o);
|
|
19
29
|
return !p || p === Object.prototype || p === Array.prototype ? undefined : p;
|
|
20
|
-
}`,void 0,e),
|
|
30
|
+
}`,void 0,e),u=>{if(n.typeof(u)==="undefined")return;const[l]=t(u);return l});return typeof o=="object"&&Object.setPrototypeOf(i,o),q(n,e,s,t),i}function Y(n,e){const t=n.typeof(e);return t==="undefined"||t==="number"||t==="string"||t==="boolean"?[n.dump(e),!0]:t==="bigint"?[n.getBigInt(e),!0]:t==="object"&&n.sameValue(e,n.null)?[null,!0]:[void 0,!1]}function Me(n,e,t,r){var l;if(!Ie(n,e))return;const s=ye(),[i,o]=t(s.resolve),[a,u]=t(s.reject);return _(n,"(p, res, rej) => { p.then(res, rej); }",void 0,e,i,a),o&&i.dispose(),u&&a.dispose(),(l=r(s.promise,e))!=null?l:s.promise}function Ie(n,e){return e.owner?H(n.unwrapResult(n.evalCode("Promise")),t=>e.owner?A(n,e,t):!1):!1}function Z(n,e){const[t]=x(n,e);return t}function x(n,e){var f,c,m;const{ctx:t,marshal:r,find:s,pre:i}=e;{const[p,y]=Y(t,n);if(y)return[p,!1]}{const p=s(n);if(p)return[p,!0]}const o=p=>x(p,e);if(e.hostRef){const p=X(t,n);if(p)return[p.value,!0]}const a=je(t,n,i,[...ke,...(f=e.custom)!=null?f:[]]);if(a)return[a,!0];const u=Te(t,n,o,i);return u?[u,!0]:[(m=(c=Me(t,n,r,i))!=null?c:Re(t,n,r,o,i))!=null?m:Ee(t,n,o,i),!1]}class J{constructor(e){d(this,"ctx");d(this,"_keyToId",new Map);d(this,"_key2ToId",new Map);d(this,"_idToHandle",new Map);d(this,"_idToHandle2",new Map);d(this,"_idToKey",new Map);d(this,"_idToKey2",new Map);d(this,"_disposables",new Set);d(this,"_mapGet");d(this,"_mapSet");d(this,"_mapDelete");d(this,"_mapClear");d(this,"_nextId",Number.MIN_SAFE_INTEGER);this.ctx=e;const t=e.unwrapResult(e.evalCode(`() => {
|
|
21
31
|
const mapSym = new Map();
|
|
22
32
|
let map = new WeakMap();
|
|
23
33
|
let map2 = new WeakMap();
|
|
@@ -40,7 +50,16 @@
|
|
|
40
50
|
map2 = new WeakMap();
|
|
41
51
|
}
|
|
42
52
|
};
|
|
43
|
-
}`)).consume(r=>this._call(r,void 0));this._mapGet=e.getProp(t,"get"),this._mapSet=e.getProp(t,"set"),this._mapDelete=e.getProp(t,"delete"),this._mapClear=e.getProp(t,"clear"),t.dispose(),this._disposables.add(this._mapGet),this._disposables.add(this._mapSet),this._disposables.add(this._mapDelete),this._disposables.add(this._mapClear)}set(e,t,r,s){var
|
|
53
|
+
}`)).consume(r=>this._call(r,void 0));this._mapGet=e.getProp(t,"get"),this._mapSet=e.getProp(t,"set"),this._mapDelete=e.getProp(t,"delete"),this._mapClear=e.getProp(t,"clear"),t.dispose(),this._disposables.add(this._mapGet),this._disposables.add(this._mapSet),this._disposables.add(this._mapDelete),this._disposables.add(this._mapClear)}set(e,t,r,s){var a;if(!t.alive||s&&!s.alive)return!1;const i=(a=this.get(e))!=null?a:this.get(r);if(i)return i===t||i===s;const o=this._nextId;return H(this.ctx.newNumber(o),u=>{this._call(this._mapSet,void 0,t,u,s!=null?s:this.ctx.undefined)}),this._nextId++,this._keyToId.set(e,o),this._idToHandle.set(o,t),this._idToKey.set(o,e),r&&(this._key2ToId.set(r,o),this._idToKey2.set(o,r),s&&this._idToHandle2.set(o,s)),!0}merge(e){if(e)for(const t of e)t&&t[1]&&this.set(t[0],t[1],t[2],t[3])}get(e){var s;const t=(s=this._keyToId.get(e))!=null?s:this._key2ToId.get(e),r=typeof t=="number"?this._idToHandle.get(t):void 0;if(r){if(!r.alive){this.delete(e,!0);return}return r}}getByHandle(e){if(e.alive)return this._idToKey.get(this.ctx.getNumber(this._call(this._mapGet,void 0,e)))}has(e){return!!this.get(e)}hasHandle(e){return typeof this.getByHandle(e)!="undefined"}keys(){return this._keyToId.keys()}delete(e,t){var u;const r=(u=this._keyToId.get(e))!=null?u:this._key2ToId.get(e);if(typeof r=="undefined")return;const s=this._idToHandle.get(r),i=this._idToHandle2.get(r);this._call(this._mapDelete,void 0,...[s,i].filter(l=>!!(l!=null&&l.alive)));const o=this._idToKey.get(r),a=this._idToKey2.get(r);typeof o!="undefined"&&this._keyToId.delete(o),typeof a!="undefined"&&this._key2ToId.delete(a),this._idToHandle.delete(r),this._idToHandle2.delete(r),this._idToKey.delete(r),this._idToKey2.delete(r),t&&(s!=null&&s.alive&&s.dispose(),i!=null&&i.alive&&i.dispose())}deleteByHandle(e,t){const r=this.getByHandle(e);typeof r!="undefined"&&this.delete(r,t)}clear(){this._nextId=0,this._keyToId.clear(),this._key2ToId.clear(),this._idToHandle.clear(),this._idToHandle2.clear(),this._idToKey.clear(),this._idToKey2.clear(),this._mapClear.alive&&this._call(this._mapClear,void 0)}dispose(){for(const e of this._disposables.values())e.alive&&e.dispose();for(const e of this._idToHandle.values())e.alive&&e.dispose();for(const e of this._idToHandle2.values())e.alive&&e.dispose();this._disposables.clear(),this.clear()}get size(){return this._keyToId.size}[Symbol.iterator](){const e=this._keyToId.keys();return{next:()=>{for(;;){const t=e.next();if(t.done)return{value:void 0,done:!0};const r=this._keyToId.get(t.value);if(typeof r=="undefined")continue;const s=this._idToHandle.get(r),i=this._idToHandle2.get(r);if(!s)continue;const o=this._idToKey2.get(r);return{value:[t.value,s,o,i],done:!1}}}}}_call(e,t,...r){return g(this.ctx,this.ctx.callFunction(e,typeof t=="undefined"?this.ctx.undefined:t,...r))}}function Ae(n,e,t,r,s,i,o,a=!0){if(!R(e)||e instanceof Promise||e instanceof Date||e instanceof ArrayBuffer||ArrayBuffer.isView(e)||e instanceof Map||e instanceof Set||o&&!o(e))return;if(Ce(e,t)||!a)return e;const u=new Proxy(e,{get(l,f){return f===t?l:Reflect.get(l,f)},set(l,f,c,m){var h;const p=T(c,t),y=(h=i==null?void 0:i(m))!=null?h:"host";return y!=="vm"&&!Reflect.set(l,f,p)||y==="host"||!n.alive||C([s(m),s(f),s(p)],(S,b,j)=>{const[w,k]=B(n,S,r);k?H(w,M=>n.setProp(M,b,j)):n.setProp(w,b,j)}),!0},deleteProperty(l,f){var m;const c=(m=i==null?void 0:i(u))!=null?m:"host";return C([s(u),s(f)],(p,y)=>{const[h,S]=B(n,p,r);if(c==="vm"||Reflect.deleteProperty(l,f)){if(c==="host"||!n.alive)return!0;S?H(h,b=>_(n,"(a, b) => delete a[b]",void 0,b,y)):_(n,"(a, b) => delete a[b]",void 0,h,y)}return!0})},defineProperty(l,f,c){var y;const m=(y=i==null?void 0:i(u))!=null?y:"host",p=z({},c);return"value"in p&&(p.value=T(p.value,t)),typeof p.get=="function"&&(p.get=T(p.get,t)),typeof p.set=="function"&&(p.set=T(p.set,t)),m!=="vm"&&!Reflect.defineProperty(l,f,p)?!1:(m==="host"||!n.alive||C([s(u),s(f),s(p)],(h,S,b)=>{const[j,w]=B(n,h,r),k=M=>_(n,"(o, k, d) => { Object.defineProperty(o, k, d); }",void 0,M,S,b).dispose();w?H(j,k):k(j)}),!0)}});return u}function Fe(n,e,t,r,s,i,o=!0){let a;const u=y=>{const h=s==null?void 0:s(r(y));return typeof h=="string"?n.newString(h):n.undefined},l=(y,h,S)=>{const b=r(y);if(!b)return;const j=r(h);if(j==="__proto__")return;const w=r(S);T(b,e)[j]=w},f=(y,h)=>{const S=r(y);if(!S)return;const b=r(h);Reflect.deleteProperty(T(S,e),b)},c=(y,h,S)=>{const b=r(y);if(!b)return;const j=r(h);if(j==="__proto__")return;const w=r(S);Object.defineProperty(T(b,e),j,w)},m=()=>(a&&a.alive||(a=n.newFunction("proxyFuncs",(y,...h)=>{switch(n.getNumber(y)){case 1:return u(h[0]);case 2:return l(h[0],h[1],h[2]);case 3:return f(h[0],h[1]);case 4:return c(h[0],h[1],h[2])}return n.undefined})),a);return{wrapHandle:y=>{if(!O(n,y)||i&&!i(y,n))return[void 0,!1];if(!o)return[y,!1];const h=_(n,`(target, sym, proxyFuncs) => {
|
|
54
|
+
if (
|
|
55
|
+
(target instanceof Promise) ||
|
|
56
|
+
(target instanceof Date) ||
|
|
57
|
+
(target instanceof ArrayBuffer) ||
|
|
58
|
+
(ArrayBuffer.isView(target)) ||
|
|
59
|
+
(target instanceof Map) ||
|
|
60
|
+
(target instanceof Set) ||
|
|
61
|
+
target[sym]
|
|
62
|
+
) return null;
|
|
44
63
|
const rec = new Proxy(target, {
|
|
45
64
|
get(obj, key, receiver) {
|
|
46
65
|
return key === sym ? obj : Reflect.get(obj, key, receiver)
|
|
@@ -77,4 +96,4 @@
|
|
|
77
96
|
},
|
|
78
97
|
});
|
|
79
98
|
return rec;
|
|
80
|
-
}`,void 0,
|
|
99
|
+
}`,void 0,y,t,m());return n.sameValue(h,n.null)?(h.dispose(),[y,!1]):[h,!0]},dispose:()=>{a&&a.alive&&a.dispose(),a=void 0}}}function T(n,e){var t;return R(n)&&(t=n[e])!=null?t:n}function B(n,e,t){const r=_(n,'(a, s) => (typeof a === "object" && a !== null || typeof a === "function") && a[s] || null',void 0,e,t);return n.sameValue(r,n.null)?(r.dispose(),[e,!1]):[r,!0]}function Ce(n,e){return R(n)&&!!n[e]}class ee{constructor(e,t){d(this,"context");d(this,"_map");d(this,"_registeredMap");d(this,"_registeredMapDispose",new Set);d(this,"_transientHandles",new Set);d(this,"_sync",new Set);d(this,"_temporalSync",new Set);d(this,"_symbol",Symbol());d(this,"_symbolHandle");d(this,"_wrapHandleImpl");d(this,"_options");d(this,"_isMarshalable",e=>{var r,s;const t=(r=this._options)==null?void 0:r.isMarshalable;return(s=typeof t=="function"?t(this._unwrap(e)):t)!=null?s:"json"});d(this,"_marshalByReference",e=>{var t,r;return!!((r=(t=this._options)==null?void 0:t.marshalByReference)!=null&&r.call(t,this._unwrap(e)))});d(this,"_asyncify",e=>{var s;const t=(s=this._options)==null?void 0:s.asyncify;if(!t)return!1;const r=this._unwrap(e);return typeof t=="function"?typeof r=="function"&&t(r):Object.prototype.toString.call(r)==="[object AsyncFunction]"});d(this,"_registerHostRef",(e,t)=>{const r=this._unwrap(e),s=this._map.get(r);return s||(this._map.set(r,t),t)});d(this,"_marshalFind",e=>{var s,i,o;const t=this._unwrap(e);return(o=(i=(s=this._registeredMap.get(e))!=null?s:t!==e?this._registeredMap.get(t):void 0)!=null?i:this._map.get(e))!=null?o:t!==e?this._map.get(t):void 0});d(this,"_marshalPre",(e,t,r)=>{if(r==="json"){this._registerTransient(V(t));return}const s=this._register(e,V(t),this._map);if(s)return s[1];this._registeredMap.has(e)||this._registerTransient(V(t))});d(this,"_registerTransient",e=>{this._transientHandles.add(e)});d(this,"_disposeTransient",e=>{this._transientHandles.delete(e)&&e.alive&&e.dispose()});d(this,"_marshalPreApply",(e,t,r)=>{const s=R(t)?this._unwrap(t):void 0;s&&this._temporalSync.add(s);try{return e.apply(t,r)}finally{s&&this._temporalSync.delete(s)}});d(this,"_marshal",e=>{var o,a,u,l,f;const t=this._registeredMap.get(e);if(t)return[t,!1];const r=this._marshalByReference(e)?this._unwrap(e):(o=this._wrap(e))!=null?o:e,s=G(r,{ctx:this.context,unmarshal:this._unmarshal,isMarshalable:this._isMarshalable,marshalByReference:this._marshalByReference,registerHostRef:this._registerHostRef,find:this._marshalFind,pre:this._marshalPre,registerTransient:this._registerTransient,disposeTransient:this._disposeTransient,preApply:this._marshalPreApply,prepareReturn:this._prepareMarshalReturn,unwrap:c=>this._unwrap(c),asyncify:(a=this._options)!=null&&a.asyncify?this._asyncify:void 0,custom:(u=this._options)==null?void 0:u.customMarshaller});return this._transientHandles.delete(s),!((f=(l=this._options)==null?void 0:l.syncEnabled)!=null?f:!0)||!R(e)?[s,!0]:[s,!this._map.hasHandle(s)]});d(this,"_prepareMarshalReturn",e=>{var r,s;return((s=(r=this._options)==null?void 0:r.syncEnabled)!=null?s:!0)&&e.alive&&O(this.context,e)&&this._map.hasHandle(e)?e.dup():e});d(this,"_preUnmarshal",(e,t)=>{var r,s,i;return(i=this._register(e,t,void 0,(s=(r=this._options)==null?void 0:r.syncEnabled)!=null?s:!0))==null?void 0:i[0]});d(this,"_unmarshalFind",e=>{var t;return(t=this._registeredMap.getByHandle(e))!=null?t:this._map.getByHandle(e)});d(this,"_unmarshal",e=>{var u,l,f;const[t,r]=Y(this.context,e);if(r)return t;const s=this._registeredMap.getByHandle(e);if(typeof s!="undefined")return s;if((u=this._options)!=null&&u.marshalByReference){const c=X(this.context,e);if(c)return c.value}const[i,o]=this._wrapHandle(e);let a=!1;try{const c=Z(i!=null?i:e,{ctx:this.context,marshal:this._marshal,find:this._unmarshalFind,pre:this._preUnmarshal,custom:(l=this._options)==null?void 0:l.customUnmarshaller,hostRef:!!((f=this._options)!=null&&f.marshalByReference)});return a=!0,c}finally{!a&&o&&(i!=null&&i.alive)&&i!==e&&i.dispose()}});d(this,"_syncMode",e=>{const t=this._unwrap(e);return this._sync.has(t)||this._temporalSync.has(t)?"both":void 0});d(this,"_unwrapIfNotSynced",e=>{const t=this._unwrap(e);return t instanceof Promise||!this._sync.has(t)?t:e});var r,s,i,o;t!=null&&t.compat&&!("runtime"in e)&&(e.runtime={hasPendingJob:()=>e.hasPendingJob(),executePendingJobs:a=>e.executePendingJobs(a)}),this.context=t!=null&&t.experimentalContextEx?oe(e):e,F(this.context),this._options=t,this._symbolHandle=e.unwrapResult(e.evalCode("Symbol()")),this._wrapHandleImpl=Fe(this.context,this._symbol,this._symbolHandle,this._unmarshal,this._syncMode,(r=this._options)==null?void 0:r.isHandleWrappable,(i=(s=this._options)==null?void 0:s.syncEnabled)!=null?i:!0),this._map=new J(e),this._registeredMap=new J(e),this.registerAll((o=t==null?void 0:t.registeredObjects)!=null?o:K)}dispose(){var e,t;for(const r of this._transientHandles)r.alive&&r.dispose();this._transientHandles.clear(),this._map.dispose(),this._registeredMap.dispose(),this._wrapHandleImpl.dispose(),this._symbolHandle.dispose(),N(this.context),(t=(e=this.context).disposeEx)==null||t.call(e)}[Symbol.dispose](){this.dispose()}evalCode(e,t,r){const s=this.context.evalCode(e,t,r);return this._unwrapResultAndUnmarshal(s)}evalModule(e,t="module.js",r){const s=this.context.evalCode(e,t,re(z({},typeof r=="number"?{}:r),{type:"module"}));return this._unwrapResultAndUnmarshal(s)}executePendingJobs(e){const t=this.context.runtime.executePendingJobs(e);if("value"in t)return t.value;throw this._unwrapIfNotSynced(H(t.error,this._unmarshal))}setMemoryLimit(e){this.context.runtime.setMemoryLimit(e)}setMaxStackSize(e){this.context.runtime.setMaxStackSize(e)}setInterruptHandler(e){this.context.runtime.setInterruptHandler(e)}removeInterruptHandler(){this.context.runtime.removeInterruptHandler()}getMemoryUsage(){const e=this.context.runtime.computeMemoryUsage();try{return this.context.dump(e)}finally{e.dispose()}}dumpMemoryUsage(){return this.context.runtime.dumpMemoryUsage()}expose(e){for(const[t,r]of Object.entries(e))se(this._marshal(r),s=>{this.context.setProp(this.context.global,t,s)})}sync(e){const t=this._wrap(e);return typeof t=="undefined"?e:(U(t,r=>{const s=this._unwrap(r);this._sync.add(s)}),t)}register(e,t){if(this._registeredMap.has(e))return;const r=typeof t=="string"?this._unwrapResult(this.context.evalCode(t)):t;this.context.sameValue(r,this.context.undefined)||(typeof t=="string"&&this._registeredMapDispose.add(e),this._registeredMap.set(e,r))}registerAll(e){for(const[t,r]of e)this.register(t,r)}unregister(e,t){this._registeredMap.delete(e,this._registeredMapDispose.has(e)||t),this._registeredMapDispose.delete(e)}unregisterAll(e,t){for(const r of e)this.unregister(r,t)}startSync(e){if(!R(e))return;const t=this._unwrap(e);this._sync.add(t)}endSync(e){this._sync.delete(this._unwrap(e))}_unwrapResult(e){if("value"in e)return e.value;throw this._unwrapIfNotSynced(H(e.error,this._unmarshal))}_unwrapResultAndUnmarshal(e){if(e)return this._unwrapIfNotSynced(H(this._unwrapResult(e),this._unmarshal))}_register(e,t,r=this._map,s){if(this._registeredMap.has(e)||this._registeredMap.hasHandle(t))return;let i=this._wrap(e);const[o,a]=this._wrapHandle(t),u=e instanceof Promise;if(!o||!i&&!u)return;u&&(i=e);const l=this._unwrap(e);let f,c=!1,m=!1;try{if([f,c]=this._unwrapHandle(t),r.set(i,o,l,f))s&&this._sync.add(l);else throw new Error("already registered");return m=!0,[i,o]}finally{m||(a&&o.alive&&o.dispose(),c&&(f!=null&&f.alive)&&f.dispose())}}_wrap(e){var t,r,s;return Ae(this.context,e,this._symbol,this._symbolHandle,this._marshal,this._syncMode,(t=this._options)==null?void 0:t.isWrappable,(s=(r=this._options)==null?void 0:r.syncEnabled)!=null?s:!0)}_unwrap(e){return T(e,this._symbol)}_wrapHandle(e){return this._wrapHandleImpl.wrapHandle(e)}_unwrapHandle(e){return B(this.context,e,this._symbolHandle)}}class Be extends ee{constructor(t,r){super(t,r);d(this,"asyncContext");this.asyncContext=t}evalCodeAsync(t,r,s){return $(this,null,function*(){const i=yield this.asyncContext.evalCodeAsync(t,r,s);return this._unwrapResultAndUnmarshal(i)})}}v.Arena=ee,v.AsyncArena=Be,v.VMMap=J,v.call=_,v.complexity=de,v.consumeAll=D,v.defaultRegisteredObjects=K,v.isES2015Class=L,v.isHandleObject=O,v.isObject=R,v.json=I,v.marshal=G,v.unmarshal=Z,v.walkObject=U,Object.defineProperty(v,Symbol.toStringTag,{value:"Module"})}));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quickjs-emscripten-sync",
|
|
3
3
|
"author": "rot1024",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.11.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -43,14 +43,14 @@
|
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@jitl/quickjs-wasmfile-debug-sync": "^0.32.0",
|
|
46
|
-
"@vitest/coverage-v8": "^4.1.
|
|
47
|
-
"eslint": "^
|
|
48
|
-
"eslint-config-reearth": "^0.
|
|
49
|
-
"prettier": "^3.
|
|
46
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
47
|
+
"eslint": "^10.6.0",
|
|
48
|
+
"eslint-config-reearth": "^0.4.0",
|
|
49
|
+
"prettier": "^3.9.4",
|
|
50
50
|
"quickjs-emscripten": "^0.32.0",
|
|
51
51
|
"typescript": "^5.9.3",
|
|
52
|
-
"vite": "^7.3.
|
|
52
|
+
"vite": "^7.3.6",
|
|
53
53
|
"vite-plugin-dts": "^4.3.0",
|
|
54
|
-
"vitest": "^4.1.
|
|
54
|
+
"vitest": "^4.1.10"
|
|
55
55
|
}
|
|
56
56
|
}
|
package/src/contextex.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { QuickJSContext, QuickJSHandle, VmFunctionImplementation } from "quickjs-emscripten";
|
|
1
|
+
import type { QuickJSContext, QuickJSHandle, VmFunctionImplementation } from "quickjs-emscripten";
|
|
2
|
+
|
|
3
|
+
import { consumeAll } from "./vmutil";
|
|
2
4
|
|
|
3
5
|
export type QuickJSContextEx = QuickJSContext & {
|
|
4
6
|
disposeEx?: () => void;
|
|
@@ -51,15 +53,25 @@ export class ContextEx {
|
|
|
51
53
|
this.fnCounter++;
|
|
52
54
|
const id = this.fnCounter;
|
|
53
55
|
this.fnMap.set(id, fn);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
// `callFunction` does not dispose its arguments, so the name/length/id handles
|
|
57
|
+
// are consumed here (even on throw) instead of leaking on every newFunction.
|
|
58
|
+
return consumeAll(
|
|
59
|
+
[
|
|
58
60
|
this.context.newString(name),
|
|
59
61
|
this.context.newNumber(fn.length),
|
|
60
62
|
this.context.newNumber(id),
|
|
61
|
-
|
|
62
|
-
)
|
|
63
|
+
],
|
|
64
|
+
([nameHandle, lengthHandle, idHandle]) =>
|
|
65
|
+
this.context.unwrapResult(
|
|
66
|
+
this.context.callFunction(
|
|
67
|
+
this.fnGenerator,
|
|
68
|
+
this.context.undefined,
|
|
69
|
+
nameHandle,
|
|
70
|
+
lengthHandle,
|
|
71
|
+
idHandle,
|
|
72
|
+
this.fn,
|
|
73
|
+
),
|
|
74
|
+
),
|
|
63
75
|
);
|
|
64
76
|
};
|
|
65
77
|
}
|
package/src/edge.test.ts
CHANGED
|
@@ -36,12 +36,54 @@ describe("edge cases", () => {
|
|
|
36
36
|
expect(cb.current?.()).toBe(0);
|
|
37
37
|
expect(called).toEqual(["a", "b"]);
|
|
38
38
|
|
|
39
|
+
// The host getters are still traversed on every access, so `called` grows.
|
|
40
|
+
// But `obj` keeps its identity across marshals (sync is on), so the VM holds
|
|
41
|
+
// the value snapshot from the first marshal: a later host-side mutation is
|
|
42
|
+
// not re-marshalled. Use `arena.sync(obj)` to propagate host writes (see the
|
|
43
|
+
// "getter (synced)" test below).
|
|
39
44
|
obj.c = 1;
|
|
40
|
-
expect(cb.current?.()).toBe(
|
|
45
|
+
expect(cb.current?.()).toBe(0);
|
|
41
46
|
expect(called).toEqual(["a", "b", "a", "b"]);
|
|
42
47
|
|
|
43
48
|
arena.dispose();
|
|
44
|
-
// ctx.dispose()
|
|
49
|
+
// Re-marshalling `obj` used to leak a handle, which aborted ctx.dispose().
|
|
50
|
+
// Now that the stale-entry handle is disposed, the context disposes cleanly.
|
|
51
|
+
expect(() => ctx.dispose()).not.toThrow();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// this test takes more than about 20s
|
|
55
|
+
test("getter (synced)", async () => {
|
|
56
|
+
const ctx = (await getQuickJS()).newContext();
|
|
57
|
+
const arena = new Arena(ctx, { isMarshalable: true });
|
|
58
|
+
|
|
59
|
+
// A synced object propagates host-side writes to the VM, so re-reads see the
|
|
60
|
+
// updated value while still keeping a stable identity.
|
|
61
|
+
const obj = arena.sync({ c: 0 });
|
|
62
|
+
const exposed = {
|
|
63
|
+
get a() {
|
|
64
|
+
return {
|
|
65
|
+
get b() {
|
|
66
|
+
return obj;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
const cb: { current?: () => any } = {};
|
|
72
|
+
arena.expose({
|
|
73
|
+
exposed,
|
|
74
|
+
register: (fn: () => any) => {
|
|
75
|
+
cb.current = fn;
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
arena.evalCode(`register(() => exposed.a.b.c);`);
|
|
80
|
+
expect(cb.current?.()).toBe(0);
|
|
81
|
+
|
|
82
|
+
obj.c = 1;
|
|
83
|
+
expect(cb.current?.()).toBe(1);
|
|
84
|
+
|
|
85
|
+
arena.dispose();
|
|
86
|
+
expect(() => ctx.dispose()).not.toThrow();
|
|
45
87
|
});
|
|
46
88
|
|
|
47
89
|
test(
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import variant from "@jitl/quickjs-wasmfile-debug-sync";
|
|
2
|
+
import { newQuickJSWASMModuleFromVariant, type QuickJSWASMModule } from "quickjs-emscripten";
|
|
3
|
+
import { describe, expect, test } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { Arena } from ".";
|
|
6
|
+
|
|
7
|
+
// Deterministic fault-injection harness for issue #88.
|
|
8
|
+
//
|
|
9
|
+
// When a hard abort (memory limit or interrupt) lands *mid-flight* in the deep
|
|
10
|
+
// sync/marshal/unmarshal chain, a host-held QuickJS handle can be orphaned. The
|
|
11
|
+
// debug-sync runtime aborts on `ctx.dispose()` (a C-level `RuntimeError`) if any
|
|
12
|
+
// GC object handle leaked (`Assertion failed: list_empty(&rt->gc_obj_list) ... at
|
|
13
|
+
// JS_FreeRuntime`). That abort IS the detector: a scenario is run under an
|
|
14
|
+
// injected fault, then `arena.dispose()` + `ctx.dispose()` must NOT throw. A
|
|
15
|
+
// throw means a handle was orphaned.
|
|
16
|
+
//
|
|
17
|
+
// We deliberately use the debug-sync variant (not `getQuickJS()`): only the debug
|
|
18
|
+
// build asserts the GC object list is empty at `JS_FreeRuntime`, and that
|
|
19
|
+
// assertion is what surfaces the leak. This mirrors the sibling leak tests
|
|
20
|
+
// (jsonleak/remarshalleak).
|
|
21
|
+
//
|
|
22
|
+
// Two injection mechanisms:
|
|
23
|
+
//
|
|
24
|
+
// A. Memory-limit sweep — `arena.setMemoryLimit(limit)` makes any VM allocation
|
|
25
|
+
// (`ctx.new*` / `callFunction`) fail once the working set crosses `limit`, so
|
|
26
|
+
// the abort lands at an allocation deep in the host-driven marshal/unmarshal
|
|
27
|
+
// chain. Swept over a fixed byte range that brackets each scenario's working
|
|
28
|
+
// set. The step (320) is chosen to match the range used while developing the
|
|
29
|
+
// fix: a *finer* step occasionally lands the OOM on an exact byte that trips a
|
|
30
|
+
// pre-existing quickjs-emscripten hang (an OOM inside its GC that never
|
|
31
|
+
// returns control — unrelated to handle leaks, and unfixable host-side). The
|
|
32
|
+
// step here is verified hang-free for these payloads, and, because the WASM is
|
|
33
|
+
// deterministic, that holds in CI too.
|
|
34
|
+
//
|
|
35
|
+
// B. Interrupt sweep — `arena.setInterruptHandler` fires only during VM bytecode
|
|
36
|
+
// execution, so it cannot land in the *host-side* marshal code that the
|
|
37
|
+
// memory sweep targets (a direct `expose`/`evalCode` never invokes the
|
|
38
|
+
// handler — its VM work is below the interrupt-check interval). To exercise
|
|
39
|
+
// the boundary under interrupts we drive it from *inside* a VM loop: a synced
|
|
40
|
+
// mutation loop and a host-function round-trip loop repeatedly cross into the
|
|
41
|
+
// marshal/unmarshal chain, and the interrupt can trip inside a nested VM
|
|
42
|
+
// `call` there. A counter-based handler interrupts on its Nth invocation; N is
|
|
43
|
+
// swept until a run completes uninterrupted.
|
|
44
|
+
|
|
45
|
+
class Point {
|
|
46
|
+
x: number;
|
|
47
|
+
y: number;
|
|
48
|
+
constructor(x: number, y: number) {
|
|
49
|
+
this.x = x;
|
|
50
|
+
this.y = y;
|
|
51
|
+
}
|
|
52
|
+
dist() {
|
|
53
|
+
return Math.hypot(this.x, this.y);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// A payload that crosses every marshal path: primitives, arrays, Date & Symbol
|
|
58
|
+
// (custom), a function, a class instance (custom prototype + method), Map & Set
|
|
59
|
+
// (mapset), and nesting.
|
|
60
|
+
const richHost = () => ({
|
|
61
|
+
n: 42,
|
|
62
|
+
s: "hi",
|
|
63
|
+
arr: [1, 2, 3],
|
|
64
|
+
d: new Date(0),
|
|
65
|
+
sym: Symbol("k"),
|
|
66
|
+
fn: (a: number, b: number) => a + b,
|
|
67
|
+
pt: new Point(3, 4),
|
|
68
|
+
m: new Map<string, number>([
|
|
69
|
+
["a", 1],
|
|
70
|
+
["b", 2],
|
|
71
|
+
]),
|
|
72
|
+
set: new Set<number>([7, 8, 9]),
|
|
73
|
+
nested: { a: { b: [{ c: 1 }] } },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const scenarios: Record<string, (arena: Arena) => void> = {
|
|
77
|
+
// 1. Mutating a synced object from the VM (the known-bad case from the issue):
|
|
78
|
+
// the abort is swept through the sync -> marshal -> setter chain.
|
|
79
|
+
syncedMutation: arena => {
|
|
80
|
+
const s: any = arena.sync({ a: { b: 1 } });
|
|
81
|
+
arena.expose({ s });
|
|
82
|
+
arena.evalCode(`s.a.b = { deep: [1, 2, 3] }`);
|
|
83
|
+
},
|
|
84
|
+
// 2. Marshal chain: expose a nested object/function mix.
|
|
85
|
+
marshal: arena => {
|
|
86
|
+
arena.expose({ obj: richHost() });
|
|
87
|
+
arena.evalCode(`obj.pt.x + obj.arr[1] + obj.m.get("a") + obj.set.size`);
|
|
88
|
+
},
|
|
89
|
+
// 3. Unmarshal chain: evaluate a deep object literal with functions/arrays. It
|
|
90
|
+
// deliberately avoids holding BOTH a Map and a Set in one unmarshalled
|
|
91
|
+
// literal, which trips the pre-existing quickjs OOM hang described above;
|
|
92
|
+
// the mapset *marshal* path is covered by scenarios 2 and 4 instead.
|
|
93
|
+
unmarshal: arena => {
|
|
94
|
+
arena.evalCode(
|
|
95
|
+
`({ n: 1, s: "x", arr: [1, 2, { z: 3 }], d: new Date(0), fn: a => a * 2, nested: { a: { b: { c: [4, 5] } } }, sym: Symbol("q") })`,
|
|
96
|
+
);
|
|
97
|
+
},
|
|
98
|
+
// 4. Round-trip: an exposed host function called from the VM with an object
|
|
99
|
+
// argument (unmarshal) and an object return (marshal, incl. Date/Symbol/Set).
|
|
100
|
+
roundTrip: arena => {
|
|
101
|
+
arena.expose({
|
|
102
|
+
host: (o: any) => ({
|
|
103
|
+
sum: o.a + o.b,
|
|
104
|
+
when: new Date(0),
|
|
105
|
+
tag: Symbol("r"),
|
|
106
|
+
items: [o.a, o.b],
|
|
107
|
+
pt: new Point(o.a, o.b),
|
|
108
|
+
tags: new Set([o.a]),
|
|
109
|
+
}),
|
|
110
|
+
});
|
|
111
|
+
arena.evalCode(`host({ a: 1, b: 2 }).sum`);
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
async function newModule(): Promise<QuickJSWASMModule> {
|
|
116
|
+
return newQuickJSWASMModuleFromVariant(variant as any);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function newArena(mod: QuickJSWASMModule) {
|
|
120
|
+
const ctx = mod.newContext();
|
|
121
|
+
return { ctx, arena: new Arena(ctx, { isMarshalable: true }) };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Dispose the arena then the context; report whether the C-level GC assertion
|
|
125
|
+
// aborted (which surfaces as a thrown RuntimeError). A leaked handle => true.
|
|
126
|
+
function disposeAborted(arena: Arena, ctx: ReturnType<QuickJSWASMModule["newContext"]>): boolean {
|
|
127
|
+
try {
|
|
128
|
+
arena.dispose();
|
|
129
|
+
ctx.dispose();
|
|
130
|
+
return false;
|
|
131
|
+
} catch {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Sweep the memory limit across [lo, hi]; return the limits at which dispose
|
|
137
|
+
// aborted plus how many runs failed vs. succeeded (to confirm the range brackets
|
|
138
|
+
// the scenario's working set). A recreated module isolates any abort so the
|
|
139
|
+
// sweep can enumerate every offending limit.
|
|
140
|
+
async function memoryLimitSweep(run: (arena: Arena) => void) {
|
|
141
|
+
const lo = 8000;
|
|
142
|
+
const hi = 14000;
|
|
143
|
+
const step = 320;
|
|
144
|
+
let mod = await newModule();
|
|
145
|
+
const reds: number[] = [];
|
|
146
|
+
let failed = 0;
|
|
147
|
+
let succeeded = 0;
|
|
148
|
+
for (let limit = lo; limit <= hi; limit += step) {
|
|
149
|
+
const { ctx, arena } = newArena(mod);
|
|
150
|
+
let threw = false;
|
|
151
|
+
arena.setMemoryLimit(limit);
|
|
152
|
+
try {
|
|
153
|
+
run(arena);
|
|
154
|
+
} catch {
|
|
155
|
+
threw = true;
|
|
156
|
+
}
|
|
157
|
+
// Reset before teardown so disposal (which allocates, e.g. to clear the map)
|
|
158
|
+
// is not itself starved by the limit.
|
|
159
|
+
arena.setMemoryLimit(-1);
|
|
160
|
+
if (threw) failed++;
|
|
161
|
+
else succeeded++;
|
|
162
|
+
if (disposeAborted(arena, ctx)) {
|
|
163
|
+
reds.push(limit);
|
|
164
|
+
mod = await newModule();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { reds, failed, succeeded };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Sweep the interrupt threshold N = 1, 2, ... until a run completes without the
|
|
171
|
+
// handler firing N times (i.e. the whole loop ran with fewer than N interrupt
|
|
172
|
+
// checks), which means every interruptible depth has been covered.
|
|
173
|
+
async function interruptSweep(setup: (arena: Arena) => void, body: string, cap = 100) {
|
|
174
|
+
let mod = await newModule();
|
|
175
|
+
const reds: number[] = [];
|
|
176
|
+
let fullCount = -1;
|
|
177
|
+
for (let n = 1; n <= cap; n++) {
|
|
178
|
+
const { ctx, arena } = newArena(mod);
|
|
179
|
+
setup(arena);
|
|
180
|
+
let count = 0;
|
|
181
|
+
arena.setInterruptHandler(() => ++count === n);
|
|
182
|
+
try {
|
|
183
|
+
arena.evalCode(body);
|
|
184
|
+
} catch {
|
|
185
|
+
// any thrown Error is acceptable
|
|
186
|
+
}
|
|
187
|
+
arena.removeInterruptHandler();
|
|
188
|
+
if (disposeAborted(arena, ctx)) {
|
|
189
|
+
reds.push(n);
|
|
190
|
+
mod = await newModule();
|
|
191
|
+
}
|
|
192
|
+
if (count < n) {
|
|
193
|
+
// The run finished before the handler reached N: all depths swept.
|
|
194
|
+
fullCount = count;
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return { reds, fullCount, capHit: fullCount < 0 };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
describe("fault injection: memory-limit sweep leaves no orphaned handle", () => {
|
|
202
|
+
for (const [name, run] of Object.entries(scenarios)) {
|
|
203
|
+
test(
|
|
204
|
+
`${name}`,
|
|
205
|
+
async () => {
|
|
206
|
+
const { reds, failed, succeeded } = await memoryLimitSweep(run);
|
|
207
|
+
// The range must bracket the working set (some limits fail mid-flight,
|
|
208
|
+
// some succeed) or the sweep would not be injecting faults at all.
|
|
209
|
+
expect(failed, `scenario "${name}": no memory limit forced a mid-flight failure`).toBeGreaterThan(0);
|
|
210
|
+
expect(succeeded, `scenario "${name}": no memory limit let the run complete`).toBeGreaterThan(0);
|
|
211
|
+
expect(
|
|
212
|
+
reds,
|
|
213
|
+
`scenario "${name}": ctx.dispose() aborted (a GC handle leaked) at memory limit(s) ${reds.join(", ")}`,
|
|
214
|
+
).toEqual([]);
|
|
215
|
+
},
|
|
216
|
+
30000,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe("fault injection: interrupt sweep leaves no orphaned handle", () => {
|
|
222
|
+
// Loop-driven forms so the VM interrupt actually lands in the marshal/unmarshal
|
|
223
|
+
// chain (see header). Each asserts the interrupt genuinely fired (fullCount > 0)
|
|
224
|
+
// and that no swept threshold orphaned a handle.
|
|
225
|
+
test(
|
|
226
|
+
"synced mutation loop",
|
|
227
|
+
async () => {
|
|
228
|
+
const { reds, fullCount, capHit } = await interruptSweep(
|
|
229
|
+
arena => {
|
|
230
|
+
const s: any = arena.sync({ a: { b: 0 } });
|
|
231
|
+
arena.expose({ s });
|
|
232
|
+
},
|
|
233
|
+
`for (let i = 0; i < 100; i++) { s.a.b = { deep: [i, i + 1], when: new Date(i) }; }`,
|
|
234
|
+
);
|
|
235
|
+
expect(capHit, "interrupt handler never stopped firing; raise the cap").toBe(false);
|
|
236
|
+
expect(fullCount, "interrupt never fired; the scenario does not exercise mechanism B").toBeGreaterThan(0);
|
|
237
|
+
expect(reds, `dispose aborted (a GC handle leaked) at interrupt threshold(s) ${reds.join(", ")}`).toEqual([]);
|
|
238
|
+
},
|
|
239
|
+
30000,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
test(
|
|
243
|
+
"host round-trip loop",
|
|
244
|
+
async () => {
|
|
245
|
+
const { reds, fullCount, capHit } = await interruptSweep(
|
|
246
|
+
arena => {
|
|
247
|
+
arena.expose({
|
|
248
|
+
host: (o: any) => ({
|
|
249
|
+
sum: o.a + o.b,
|
|
250
|
+
when: new Date(0),
|
|
251
|
+
tag: Symbol("r"),
|
|
252
|
+
items: [o.a, o.b],
|
|
253
|
+
pt: new Point(o.a, o.b),
|
|
254
|
+
}),
|
|
255
|
+
});
|
|
256
|
+
},
|
|
257
|
+
`let acc = 0; for (let i = 0; i < 150; i++) { acc += host({ a: i, b: 1 }).sum; } acc`,
|
|
258
|
+
);
|
|
259
|
+
expect(capHit, "interrupt handler never stopped firing; raise the cap").toBe(false);
|
|
260
|
+
expect(fullCount, "interrupt never fired; the scenario does not exercise mechanism B").toBeGreaterThan(0);
|
|
261
|
+
expect(reds, `dispose aborted (a GC handle leaked) at interrupt threshold(s) ${reds.join(", ")}`).toEqual([]);
|
|
262
|
+
},
|
|
263
|
+
30000,
|
|
264
|
+
);
|
|
265
|
+
});
|