quickjs-emscripten-sync 1.9.0 → 1.10.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 +9 -0
- package/dist/index.d.ts +7 -0
- package/dist/quickjs-emscripten-sync.js +424 -372
- package/dist/quickjs-emscripten-sync.umd.cjs +7 -7
- package/package.json +2 -1
- package/src/edge.test.ts +44 -2
- package/src/identity.test.ts +57 -0
- package/src/index.test.ts +20 -0
- package/src/index.ts +53 -2
- package/src/jsonleak.test.ts +178 -0
- package/src/marshal/function.test.ts +6 -4
- package/src/marshal/function.ts +25 -4
- package/src/marshal/index.ts +28 -3
- package/src/marshal/mapset.ts +11 -2
- package/src/marshal/object.ts +4 -1
- package/src/marshal/properties.ts +11 -0
- package/src/remarshalleak.test.ts +50 -0
- package/src/vmmap.ts +5 -1
- package/src/vmutil.ts +3 -1
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
(function(
|
|
1
|
+
(function(_,b){typeof exports=="object"&&typeof module!="undefined"?b(exports):typeof define=="function"&&define.amd?define(["exports"],b):(_=typeof globalThis!="undefined"?globalThis:_||self,b(_.QuickjsEmscriptenSync={}))})(this,(function(_){"use strict";var Fe=Object.defineProperty;var ee=Object.getOwnPropertySymbols;var Ie=Object.prototype.hasOwnProperty,Ne=Object.prototype.propertyIsEnumerable;var J=(_,b,j)=>b in _?Fe(_,b,{enumerable:!0,configurable:!0,writable:!0,value:j}):_[b]=j,te=(_,b)=>{for(var j in b||(b={}))Ie.call(b,j)&&J(_,j,b[j]);if(ee)for(var j of ee(b))Ne.call(b,j)&&J(_,j,b[j]);return _};var l=(_,b,j)=>J(_,typeof b!="symbol"?b+"":b,j);var ne=(_,b,j)=>new Promise((A,k)=>{var I=R=>{try{C(j.next(R))}catch(h){k(h)}},O=R=>{try{C(j.throw(R))}catch(h){k(h)}},C=R=>R.done?A(R.value):Promise.resolve(R.value).then(I,O);C((j=j.apply(_,b)).next())});const b=n=>{const e=new j(n);return new Proxy(n,{get(t,r,s){return r in e?e[r]:Reflect.get(t,r,s)}})};class j{constructor(e){l(this,"context");l(this,"fn");l(this,"fnGenerator");l(this,"fnCounter",Number.MIN_SAFE_INTEGER);l(this,"fnMap",new Map);l(this,"newFunction",(e,t)=>{this.fnCounter++;const r=this.fnCounter;return this.fnMap.set(r,t),this.context.unwrapResult(this.context.callFunction(this.fnGenerator,this.context.undefined,this.context.newString(e),this.context.newNumber(t.length),this.context.newNumber(r),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
|
|
8
|
+
})`))}disposeEx(){this.fnGenerator.dispose(),this.fn.dispose()}}const A=[[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 k(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 I(n,e){const t=k(n,n.evalCode(e)),r=(i,...o)=>k(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 O=new WeakMap;function C(n){O.has(n)||O.set(n,new Map)}function R(n){const e=O.get(n);if(e){for(const t of e.values())t.alive&&t.dispose();O.delete(n)}}function h(n,e,t,...r){const s=O.get(n);if(!s){const o=I(n,e);try{return o(t,...r)}finally{o.dispose()}}let i=s.get(e);return(!i||!i.alive)&&(i=k(n,n.evalCode(e)),s.set(e,i)),k(n,n.callFunction(i,t!=null?t:n.undefined,...r))}function re(n,e,t){return n.dump(h(n,"(a, b) => a instanceof b",void 0,e,t))}function W(n,e){const t=n.typeof(e);return t==="function"||t==="object"&&!n.sameValue(e,n.null)}function z(n,e){const t=JSON.stringify(e);return t?n.newString(t).consume(r=>h(n,"JSON.parse",void 0,r)):n.undefined}function P(n,e){try{return e(n)}finally{n.alive&&n.dispose()}}function $(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 B(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 N(n){return ie(n)?n.handle:n}function oe(n,e,t,r){var i;let s;for(const o of r)if(s=o(e,n),s)break;return s?(i=t(e,s))!=null?i:s:void 0}function ue(n,e){return typeof n!="symbol"?void 0:h(e,"d => Symbol(d)",void 0,n.description?e.newString(n.description):e.undefined)}function ae(n,e){return n instanceof Date?h(e,"d => new Date(d)",void 0,e.newNumber(n.getTime())):void 0}function fe(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 $([e.newArrayBuffer(t.buffer),e.newString(n.constructor.name)],([r,s])=>h(e,"(buf, name) => new globalThis[name](buf)",void 0,r,s))}}const ce=[ue,ae,fe];function L(n){return typeof n=="function"&&/^class\s/.test(Function.prototype.toString.call(n))}function E(n){return typeof n=="function"||typeof n=="object"&&n!==null}function D(n,e){const t=new Set,r=s=>{if(!(!E(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 le(n,e){return D(n,e?(t,r)=>r.size<e:void 0).size}function pe(){let n=()=>{},e=()=>{};return{promise:new Promise((r,s)=>{n=r,e=s}),resolve:n,reject:e}}function K(n,e,t,r,s=()=>{}){const i=n.newObject(),o=[],f=(a,u)=>{const d=r(a),y=typeof u.value=="undefined"?void 0:r(u.value),m=typeof u.get=="undefined"?void 0:r(u.get),c=typeof u.set=="undefined"?void 0:r(u.set);y&&o.push(y),m&&o.push(m),c&&o.push(c),n.newObject().consume(p=>{Object.entries(u).forEach(([v,g])=>{const w=v==="value"?y:v==="get"?m:v==="set"?c:g?n.true:n.false;w&&n.setProp(p,v,w)}),n.setProp(i,d,p)})};try{const a=Object.getOwnPropertyDescriptors(e);Object.entries(a).forEach(([u,d])=>f(u,d)),Object.getOwnPropertySymbols(a).forEach(u=>f(u,a[u])),h(n,"Object.defineProperties",void 0,t,i).dispose();for(const u of o)s(u)}finally{i.dispose()}}function de(n,e,t,r,s,i,o=()=>{},f=a=>a){var d;if(typeof e!="function")return;const a=n.newFunction(e.name,function(...y){const m=n.sameValue(this,n.global)?void 0:r(this),c=y.map(p=>r(p));if(L(e)&&E(m)){const p=new e(...c);return Object.entries(p).forEach(([v,g])=>{const w=t(g);n.setProp(this,v,w),o(w)}),this}return f(t(i?i(e,m,c):e.apply(m,c)))}).consume(y=>h(n,`Cls => {
|
|
9
9
|
const fn = function(...args) { return Cls.apply(this, args); };
|
|
10
10
|
fn.name = Cls.name;
|
|
11
11
|
fn.length = Cls.length;
|
|
12
12
|
return fn;
|
|
13
|
-
}`,void 0,
|
|
13
|
+
}`,void 0,y)),u=(d=s(e,a))!=null?d:a;return K(n,e,a,t,o),u}function ye(n,e,t){if(!(typeof e!="object"&&typeof e!="function")&&e!==null)return t(e,n.newHostRef(e).handle)}function he(n,e,t){var i;const r=z(n,e);return(i=t(e,r))!=null?i:r}function me(n,e,t,r,s=()=>{}){var i,o;if(e instanceof Map){const f=h(n,"() => new Map()"),a=(i=r(e,f))!=null?i:f;for(const[u,d]of e){const y=t(u),m=t(d);h(n,"(m, k, v) => m.set(k, v)",void 0,f,y,m).dispose(),s(y),s(m)}return a}if(e instanceof Set){const f=h(n,"() => new Set()"),a=(o=r(e,f))!=null?o:f;for(const u of e){const d=t(u);h(n,"(s, v) => s.add(v)",void 0,f,d).dispose(),s(d)}return a}}function _e(n,e,t,r,s=()=>{}){var u;if(typeof e!="object"||e===null)return;const i=Array.isArray(e)?n.newArray():n.newObject(),o=(u=r(e,i))!=null?u:i,f=Object.getPrototypeOf(e),a=f&&f!==Object.prototype&&f!==Array.prototype?t(f):void 0;return a&&(h(n,"Object.setPrototypeOf",void 0,o,a).dispose(),s(a)),K(n,e,i,t,s),o}function ve(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 be(n,e,t,r){var i;if(!(e instanceof Promise))return;const s=n.newPromise();return e.then(o=>s.resolve(t(o)),o=>s.reject(t(o))),(i=r(e,s))!=null?i:s.handle}function U(n,e){var y,m,c,p,v,g,w,T;const{ctx:t,unmarshal:r,isMarshalable:s,find:i,pre:o}=e;{const S=ve(t,n);if(S)return typeof n=="bigint"&&((y=e.registerTransient)==null||y.call(e,S)),S}{const S=i(n);if(S)return S}if((m=e.marshalByReference)!=null&&m.call(e,n)&&e.registerHostRef){const S=ye(t,n,e.registerHostRef);if(S)return S}const f=s==null?void 0:s(n);if(f===!1)return t.undefined;const a=(S,M)=>o(S,M,f);if(f==="json")return he(t,n,a);const u=S=>U(S,e),d=e.disposeTransient;return(T=(w=(g=(v=(p=oe(t,n,a,[...ce,...(c=e.custom)!=null?c:[]]))!=null?p:be(t,n,u,a))!=null?v:de(t,n,u,r,a,e.preApply,d,e.prepareReturn))!=null?g:me(t,n,u,a,d))!=null?w:_e(t,n,u,a,d))!=null?T:t.undefined}function we(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 ge(n,e){if(e.typeof(n)!=="symbol")return;const t=e.getString(e.getProp(n,"description"));return Symbol(t)}function je(n,e){if(!P(h(e,"a => a instanceof Date",void 0,n),r=>e.dump(r)))return;const t=P(h(e,"a => a.getTime()",void 0,n),r=>e.getNumber(r));return new Date(t)}function Se(n,e){if(P(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(P(h(e,"a => ArrayBuffer.isView(a)",void 0,n),t=>e.dump(t))){const t=P(h(e,"a => a.constructor.name",void 0,n),f=>e.getString(f)),r=globalThis[t];if(typeof r!="function")return;const s=h(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 Pe=[ge,je,Se];function Q(n,e,t,r){P(n.newFunction("",(s,i)=>{const[o]=r(s);if(typeof o!="string"&&typeof o!="number"&&typeof o!="symbol")return;const f=[["value",!0],["get",!0],["set",!0],["configurable",!1],["enumerable",!1],["writable",!1]].reduce((a,[u,d])=>{const y=n.getProp(i,u),m=n.typeof(y);if(m==="undefined")return a;if(!d&&m==="boolean")return a[u]=n.dump(n.getProp(i,u)),a;const[c,p]=r(y);return p&&y.dispose(),a[u]=c,a},{});Object.defineProperty(t,o,f)}),s=>{h(n,`(o, fn) => {
|
|
14
14
|
const descs = Object.getOwnPropertyDescriptors(o);
|
|
15
15
|
Object.entries(descs).forEach(([k, v]) => fn(k, v));
|
|
16
16
|
Object.getOwnPropertySymbols(descs).forEach(k => fn(k, descs[k]));
|
|
17
|
-
}`,void 0,e,s).dispose()})}function
|
|
17
|
+
}`,void 0,e,s).dispose()})}function Te(n,e,t,r,s){var f;if(n.typeof(e)!=="function")return;const i=function(...a){return B([t(this),...a.map(u=>t(u))],(u,...d)=>{if(new.target){const[p]=r(h(n,"(Cls, ...args) => new Cls(...args)",u,e,...d));return Object.defineProperties(this,Object.getOwnPropertyDescriptors(p)),this}const y=k(n,n.callFunction(e,u,...d)),[m,c]=r(y);return c&&y.dispose(),m})},o=(f=s(i,e))!=null?f:i;return Q(n,e,i,r),o}function q(n,e){const t=n.toHostRef(e);if(t)try{return{value:t.value}}finally{t.dispose()}}function ke(n,e,t,r){const s=P(h(n,"a => a instanceof Map",void 0,e),a=>n.dump(a)),i=!s&&P(h(n,"a => a instanceof Set",void 0,e),a=>n.dump(a));if(!s&&!i)return;const o=s?new Map:new Set;r(o,e);const f=n.unwrapResult(n.getIterator(e));try{for(const a of f){const u=n.unwrapResult(a);if(s){const d=n.getProp(u,0),y=n.getProp(u,1),[m,c]=t(d),[p,v]=t(y);c&&d.dispose(),v&&y.dispose(),o.set(m,p),u.dispose()}else{const[d,y]=t(u);y&&u.dispose(),o.add(d)}}}finally{f.alive&&f.dispose()}return o}function Re(n,e,t,r){var f;if(n.typeof(e)!=="object"||n.sameValue(e,n.null))return;const s=P(h(n,"Array.isArray",void 0,e),a=>n.dump(a))?[]:{},i=(f=r(s,e))!=null?f:s,o=P(h(n,`o => {
|
|
18
18
|
const p = Object.getPrototypeOf(o);
|
|
19
19
|
return !p || p === Object.prototype || p === Array.prototype ? undefined : p;
|
|
20
|
-
}`,void 0,e),a=>{if(n.typeof(a)==="undefined")return;const[
|
|
20
|
+
}`,void 0,e),a=>{if(n.typeof(a)==="undefined")return;const[u]=t(a);return u});return typeof o=="object"&&Object.setPrototypeOf(i,o),Q(n,e,s,t),i}function Ee(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 He(n,e,t,r){var u;if(!Oe(n,e))return;const s=pe(),[i,o]=t(s.resolve),[f,a]=t(s.reject);return h(n,"(p, res, rej) => { p.then(res, rej); }",void 0,e,i,f),o&&i.dispose(),a&&f.dispose(),(u=r(s.promise,e))!=null?u:s.promise}function Oe(n,e){return e.owner?n.unwrapResult(n.evalCode("Promise")).consume(t=>e.owner?re(n,e,t):!1):!1}function X(n,e){const[t]=Y(n,e);return t}function Y(n,e){var d,y,m;const{ctx:t,marshal:r,find:s,pre:i}=e;{const[c,p]=Ee(t,n);if(p)return[c,!1]}{const c=s(n);if(c)return[c,!0]}const o=c=>Y(c,e);if(e.hostRef){const c=q(t,n);if(c)return[c.value,!0]}const f=we(t,n,i,[...Pe,...(d=e.custom)!=null?d:[]]);if(f)return[f,!0];const a=ke(t,n,o,i);return a?[a,!0]:[(m=(y=He(t,n,r,i))!=null?y:Te(t,n,r,o,i))!=null?m:Re(t,n,o,i),!1]}class V{constructor(e){l(this,"ctx");l(this,"_keyToId",new Map);l(this,"_key2ToId",new Map);l(this,"_idToHandle",new Map);l(this,"_idToHandle2",new Map);l(this,"_idToKey",new Map);l(this,"_idToKey2",new Map);l(this,"_disposables",new Set);l(this,"_mapGet");l(this,"_mapSet");l(this,"_mapDelete");l(this,"_mapClear");l(this,"_nextId",Number.MIN_SAFE_INTEGER);this.ctx=e;const t=e.unwrapResult(e.evalCode(`() => {
|
|
21
21
|
const mapSym = new Map();
|
|
22
22
|
let map = new WeakMap();
|
|
23
23
|
let map2 = new WeakMap();
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
map2 = new WeakMap();
|
|
41
41
|
}
|
|
42
42
|
};
|
|
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
|
|
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 f;if(!t.alive||s&&!s.alive)return!1;const i=(f=this.get(e))!=null?f:this.get(r);if(i)return i===t||i===s;const o=this._nextId++;return 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)),this.ctx.newNumber(o).consume(a=>{this._call(this._mapSet,void 0,t,a,s!=null?s:this.ctx.undefined)}),!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 a;const r=(a=this._keyToId.get(e))!=null?a: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(u=>!!(u!=null&&u.alive)));const o=this._idToKey.get(r),f=this._idToKey2.get(r);typeof o!="undefined"&&this._keyToId.delete(o),typeof f!="undefined"&&this._key2ToId.delete(f),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 k(this.ctx,this.ctx.callFunction(e,typeof t=="undefined"?this.ctx.undefined:t,...r))}}function Me(n,e,t,r,s,i,o,f=!0){if(!E(e)||e instanceof Promise||e instanceof Date||e instanceof ArrayBuffer||ArrayBuffer.isView(e)||e instanceof Map||e instanceof Set||o&&!o(e))return;if(Ae(e,t)||!f)return e;const a=new Proxy(e,{get(u,d){return d===t?u:Reflect.get(u,d)},set(u,d,y,m){var v;const c=H(y,t),p=(v=i==null?void 0:i(m))!=null?v:"host";return p!=="vm"&&!Reflect.set(u,d,c)||p==="host"||!n.alive||B([s(m),s(d),s(c)],(g,w,T)=>{const[S,M]=F(n,g,r);M?S.consume(G=>n.setProp(G,w,T)):n.setProp(S,w,T)}),!0},deleteProperty(u,d){var m;const y=(m=i==null?void 0:i(a))!=null?m:"host";return B([s(a),s(d)],(c,p)=>{const[v,g]=F(n,c,r);if(y==="vm"||Reflect.deleteProperty(u,d)){if(y==="host"||!n.alive)return!0;g?v.consume(w=>h(n,"(a, b) => delete a[b]",void 0,w,p)):h(n,"(a, b) => delete a[b]",void 0,v,p)}return!0})},defineProperty(u,d,y){var p;const m=(p=i==null?void 0:i(a))!=null?p:"host",c=te({},y);return"value"in c&&(c.value=H(c.value,t)),typeof c.get=="function"&&(c.get=H(c.get,t)),typeof c.set=="function"&&(c.set=H(c.set,t)),m!=="vm"&&!Reflect.defineProperty(u,d,c)?!1:(m==="host"||!n.alive||B([s(a),s(d),s(c)],(v,g,w)=>{const[T,S]=F(n,v,r),M=G=>h(n,"(o, k, d) => { Object.defineProperty(o, k, d); }",void 0,G,g,w).dispose();S?T.consume(M):M(T)}),!0)}});return a}function Ce(n,e,t,r,s,i,o,f=!0){if(!W(n,e)||o&&!o(e,n))return[void 0,!1];if(Z(n,e,r))return[e,!1];if(!f)return[e,!1];const a=c=>{const p=i==null?void 0:i(s(c));return typeof p=="string"?n.newString(p):n.undefined},u=(c,p,v)=>{const g=s(c);if(!g)return;const w=s(p);if(w==="__proto__")return;const T=s(v);H(g,t)[w]=T},d=(c,p)=>{const v=s(c);if(!v)return;const g=s(p);Reflect.deleteProperty(H(v,t),g)},y=(c,p,v)=>{const g=s(c);if(!g)return;const w=s(p);if(w==="__proto__")return;const T=s(v);Object.defineProperty(H(g,t),w,T)},m=n.newFunction("proxyFuncs",(c,...p)=>{switch(n.getNumber(c)){case 1:return a(p[0]);case 2:return u(p[0],p[1],p[2]);case 3:return d(p[0],p[1]);case 4:return y(p[0],p[1],p[2])}return n.undefined});return P(m,c=>[h(n,`(target, sym, proxyFuncs) => {
|
|
44
44
|
const rec = new Proxy(target, {
|
|
45
45
|
get(obj, key, receiver) {
|
|
46
46
|
return key === sym ? obj : Reflect.get(obj, key, receiver)
|
|
@@ -77,4 +77,4 @@
|
|
|
77
77
|
},
|
|
78
78
|
});
|
|
79
79
|
return rec;
|
|
80
|
-
}`,void 0,e,r,c),!0])}function
|
|
80
|
+
}`,void 0,e,r,c),!0])}function H(n,e){var t;return E(n)&&(t=n[e])!=null?t:n}function F(n,e,t){return Z(n,e,t)?[n.getProp(e,t),!0]:[e,!1]}function Ae(n,e){return E(n)&&!!n[e]}function Z(n,e,t){return!!n.dump(h(n,'(a, s) => (a instanceof Promise) || (a instanceof Date) || (a instanceof ArrayBuffer) || (ArrayBuffer.isView(a)) || (a instanceof Map) || (a instanceof Set) || (typeof a === "object" && a !== null || typeof a === "function") && !!a[s]',void 0,e,t))}class x{constructor(e,t){l(this,"context");l(this,"_map");l(this,"_registeredMap");l(this,"_registeredMapDispose",new Set);l(this,"_transientHandles",new Set);l(this,"_sync",new Set);l(this,"_temporalSync",new Set);l(this,"_symbol",Symbol());l(this,"_symbolHandle");l(this,"_options");l(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"});l(this,"_marshalByReference",e=>{var t,r;return!!((r=(t=this._options)==null?void 0:t.marshalByReference)!=null&&r.call(t,this._unwrap(e)))});l(this,"_registerHostRef",(e,t)=>{const r=this._unwrap(e),s=this._map.get(r);return s||(this._map.set(r,t),t)});l(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});l(this,"_marshalPre",(e,t,r)=>{if(r==="json"){this._registerTransient(N(t));return}const s=this._register(e,N(t),this._map);if(s)return s[1];this._registeredMap.has(e)||this._registerTransient(N(t))});l(this,"_registerTransient",e=>{this._transientHandles.add(e)});l(this,"_disposeTransient",e=>{this._transientHandles.delete(e)&&e.alive&&e.dispose()});l(this,"_marshalPreApply",(e,t,r)=>{const s=E(t)?this._unwrap(t):void 0;s&&this._temporalSync.add(s);try{return e.apply(t,r)}finally{s&&this._temporalSync.delete(s)}});l(this,"_marshal",e=>{var o,f,a,u;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=U(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,custom:(f=this._options)==null?void 0:f.customMarshaller});this._transientHandles.delete(s);const i=(u=(a=this._options)==null?void 0:a.syncEnabled)!=null?u:!0;return[s,!i||!this._map.hasHandle(s)]});l(this,"_prepareMarshalReturn",e=>{var r,s;return((s=(r=this._options)==null?void 0:r.syncEnabled)!=null?s:!0)&&e.alive&&this._map.hasHandle(e)?e.dup():e});l(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]});l(this,"_unmarshalFind",e=>{var t;return(t=this._registeredMap.getByHandle(e))!=null?t:this._map.getByHandle(e)});l(this,"_unmarshal",e=>{var s,i,o;const t=this._registeredMap.getByHandle(e);if(typeof t!="undefined")return t;if((s=this._options)!=null&&s.marshalByReference){const f=q(this.context,e);if(f)return f.value}const[r]=this._wrapHandle(e);return X(r!=null?r:e,{ctx:this.context,marshal:this._marshal,find:this._unmarshalFind,pre:this._preUnmarshal,custom:(i=this._options)==null?void 0:i.customUnmarshaller,hostRef:!!((o=this._options)!=null&&o.marshalByReference)})});l(this,"_syncMode",e=>{const t=this._unwrap(e);return this._sync.has(t)||this._temporalSync.has(t)?"both":void 0});l(this,"_unwrapIfNotSynced",e=>{const t=this._unwrap(e);return t instanceof Promise||!this._sync.has(t)?t:e});var r;t!=null&&t.compat&&!("runtime"in e)&&(e.runtime={hasPendingJob:()=>e.hasPendingJob(),executePendingJobs:s=>e.executePendingJobs(s)}),this.context=t!=null&&t.experimentalContextEx?b(e):e,C(this.context),this._options=t,this._symbolHandle=e.unwrapResult(e.evalCode("Symbol()")),this._map=new V(e),this._registeredMap=new V(e),this.registerAll((r=t==null?void 0:t.registeredObjects)!=null?r:A)}dispose(){var e,t;for(const r of this._transientHandles)r.alive&&r.dispose();this._transientHandles.clear(),this._map.dispose(),this._registeredMap.dispose(),this._symbolHandle.dispose(),R(this.context),(t=(e=this.context).disposeEx)==null||t.call(e)}[Symbol.dispose](){this.dispose()}evalCode(e){const t=this.context.evalCode(e);return this._unwrapResultAndUnmarshal(t)}evalModule(e,t="module.js"){const r=this.context.evalCode(e,t,{type:"module"});return this._unwrapResultAndUnmarshal(r)}executePendingJobs(e){const t=this.context.runtime.executePendingJobs(e);if("value"in t)return t.value;throw this._unwrapIfNotSynced(P(t.error,this._unmarshal))}setMemoryLimit(e){this.context.runtime.setMemoryLimit(e)}setMaxStackSize(e){this.context.runtime.setMaxStackSize(e)}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:(D(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(!E(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(P(e.error,this._unmarshal))}_unwrapResultAndUnmarshal(e){if(e)return this._unwrapIfNotSynced(P(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]=this._wrapHandle(t),f=e instanceof Promise;if(!o||!i&&!f)return;f&&(i=e);const a=this._unwrap(e),[u,d]=this._unwrapHandle(t);if(r.set(i,o,a,u))s&&this._sync.add(a);else throw d&&u.dispose(),new Error("already registered");return[i,o]}_wrap(e){var t,r,s;return Me(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 H(e,this._symbol)}_wrapHandle(e){var t,r,s;return Ce(this.context,e,this._symbol,this._symbolHandle,this._unmarshal,this._syncMode,(t=this._options)==null?void 0:t.isHandleWrappable,(s=(r=this._options)==null?void 0:r.syncEnabled)!=null?s:!0)}_unwrapHandle(e){return F(this.context,e,this._symbolHandle)}}class Be extends x{constructor(t,r){super(t,r);l(this,"asyncContext");this.asyncContext=t}evalCodeAsync(t,r){return ne(this,null,function*(){const s=yield this.asyncContext.evalCodeAsync(t,r);return this._unwrapResultAndUnmarshal(s)})}}_.Arena=x,_.AsyncArena=Be,_.VMMap=V,_.call=h,_.complexity=le,_.consumeAll=$,_.defaultRegisteredObjects=A,_.isES2015Class=L,_.isHandleObject=W,_.isObject=E,_.json=z,_.marshal=U,_.unmarshal=X,_.walkObject=D,Object.defineProperty(_,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.10.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"quickjs-emscripten": "*"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
+
"@jitl/quickjs-wasmfile-debug-sync": "^0.32.0",
|
|
45
46
|
"@vitest/coverage-v8": "^4.1.6",
|
|
46
47
|
"eslint": "^9.39.4",
|
|
47
48
|
"eslint-config-reearth": "^0.3.8",
|
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,57 @@
|
|
|
1
|
+
import { getQuickJS } from "quickjs-emscripten";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { Arena } from ".";
|
|
5
|
+
|
|
6
|
+
// A host function's return value is disposed by the VM once consumed. Marshalled
|
|
7
|
+
// object handles are retained in the VMMap for identity while sync is on, so the
|
|
8
|
+
// returned handle must be dup'd — otherwise the map entry goes stale and the same
|
|
9
|
+
// value marshalled twice yields two distinct VM objects. These tests pin the
|
|
10
|
+
// identity behaviour (issue #4); the no-leak side is covered by
|
|
11
|
+
// remarshalleak.test.ts under the debug-sync runtime.
|
|
12
|
+
async function withArena(
|
|
13
|
+
options: ConstructorParameters<typeof Arena>[1],
|
|
14
|
+
fn: (arena: Arena) => void,
|
|
15
|
+
) {
|
|
16
|
+
const ctx = (await getQuickJS()).newContext();
|
|
17
|
+
const arena = new Arena(ctx, options);
|
|
18
|
+
fn(arena);
|
|
19
|
+
arena.dispose();
|
|
20
|
+
ctx.dispose();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("marshal identity", () => {
|
|
24
|
+
it("preserves identity for a VM object round-tripped through the host", async () => {
|
|
25
|
+
await withArena({ isMarshalable: true }, arena => {
|
|
26
|
+
arena.expose({ id: (x: any) => x });
|
|
27
|
+
// foo originates in the VM, is passed to the host and returned: the
|
|
28
|
+
// round-trip must yield the same object.
|
|
29
|
+
expect(arena.evalCode(`let foo = id({}); foo === id(foo)`)).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("returns the same VM object when a host function returns the same object twice", async () => {
|
|
34
|
+
await withArena({ isMarshalable: true }, arena => {
|
|
35
|
+
const shared = { k: 1 };
|
|
36
|
+
arena.expose({ get: () => shared });
|
|
37
|
+
expect(arena.evalCode(`get() === get()`)).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("keeps identity across retained references", async () => {
|
|
42
|
+
await withArena({ isMarshalable: true }, arena => {
|
|
43
|
+
const shared = { k: 1 };
|
|
44
|
+
arena.expose({ get: () => shared });
|
|
45
|
+
expect(arena.evalCode(`let a = get(); let b = get(); a === b`)).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("does not retain identity when sync is disabled", async () => {
|
|
50
|
+
await withArena({ isMarshalable: true, syncEnabled: false }, arena => {
|
|
51
|
+
const shared = { k: 1 };
|
|
52
|
+
arena.expose({ get: () => shared });
|
|
53
|
+
// With sync off, objects are not retained, so each marshal is independent.
|
|
54
|
+
expect(arena.evalCode(`get() === get()`)).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
package/src/index.test.ts
CHANGED
|
@@ -550,6 +550,26 @@ test("register and unregister", async () => {
|
|
|
550
550
|
ctx.dispose();
|
|
551
551
|
});
|
|
552
552
|
|
|
553
|
+
test("plain call passes `this` as undefined, method call passes the receiver", async () => {
|
|
554
|
+
const ctx = (await getQuickJS()).newContext();
|
|
555
|
+
const arena = new Arena(ctx, { isMarshalable: true });
|
|
556
|
+
|
|
557
|
+
arena.expose({
|
|
558
|
+
whoAmI() {
|
|
559
|
+
return this;
|
|
560
|
+
},
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// A plain call would see `this === globalThis` in plain JS; the VM global is
|
|
564
|
+
// intentionally not marshalled to the host, so `this` is undefined here.
|
|
565
|
+
expect(arena.evalCode(`whoAmI()`)).toBe(undefined);
|
|
566
|
+
// A method call still receives its receiver.
|
|
567
|
+
expect(arena.evalCode(`const o = { v: 42, whoAmI }; o.whoAmI().v`)).toBe(42);
|
|
568
|
+
|
|
569
|
+
arena.dispose();
|
|
570
|
+
ctx.dispose();
|
|
571
|
+
});
|
|
572
|
+
|
|
553
573
|
test("registeredObjects option", async () => {
|
|
554
574
|
const ctx = (await getQuickJS()).newContext();
|
|
555
575
|
const arena = new Arena(ctx, {
|
package/src/index.ts
CHANGED
|
@@ -79,6 +79,14 @@ export class Arena {
|
|
|
79
79
|
_map: VMMap;
|
|
80
80
|
_registeredMap: VMMap;
|
|
81
81
|
_registeredMapDispose = new Set<any>();
|
|
82
|
+
// Handles with no owner: "json" copies and BigInt values. Unlike
|
|
83
|
+
// proxy-marshalled objects they are not identity-tracked in `_map`. A nested
|
|
84
|
+
// one is disposed by its parent consumer via `_disposeTransient` as soon as
|
|
85
|
+
// the value has been copied; this set is the fallback that frees any that
|
|
86
|
+
// were never nested (e.g. a top-level value before its caller consumes it) on
|
|
87
|
+
// dispose. Top-level handles are removed in `_marshal` since their caller
|
|
88
|
+
// disposes them via `mayConsume`.
|
|
89
|
+
_transientHandles = new Set<QuickJSHandle>();
|
|
82
90
|
_sync = new Set<any>();
|
|
83
91
|
_temporalSync = new Set<any>();
|
|
84
92
|
_symbol = Symbol();
|
|
@@ -108,6 +116,10 @@ export class Arena {
|
|
|
108
116
|
* Dispose of the arena and managed handles. This method won't dispose the VM itself, so the VM has to be disposed of manually.
|
|
109
117
|
*/
|
|
110
118
|
dispose() {
|
|
119
|
+
for (const h of this._transientHandles) {
|
|
120
|
+
if (h.alive) h.dispose();
|
|
121
|
+
}
|
|
122
|
+
this._transientHandles.clear();
|
|
111
123
|
this._map.dispose();
|
|
112
124
|
this._registeredMap.dispose();
|
|
113
125
|
this._symbolHandle.dispose();
|
|
@@ -419,8 +431,29 @@ export class Arena {
|
|
|
419
431
|
h: QuickJSHandle | QuickJSDeferredPromise,
|
|
420
432
|
mode: true | "json" | undefined,
|
|
421
433
|
): Wrapped<QuickJSHandle> | undefined => {
|
|
422
|
-
if (mode === "json")
|
|
423
|
-
|
|
434
|
+
if (mode === "json") {
|
|
435
|
+
// json handles have no identity to track; register as transient so they
|
|
436
|
+
// are disposed once consumed (or on dispose) instead of leaking.
|
|
437
|
+
this._registerTransient(handleFrom(h));
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const registered = this._register(t, handleFrom(h), this._map);
|
|
441
|
+
if (registered) return registered[1];
|
|
442
|
+
// `_register` bails for value-only built-ins that `_wrap` excludes (Map,
|
|
443
|
+
// Set, Date, ArrayBuffer, TypedArray): they are marshalled by value with no
|
|
444
|
+
// entry in `_map`, so their handle has no owner. Track it as transient so a
|
|
445
|
+
// nested one is disposed once consumed, like the json path. (Objects already
|
|
446
|
+
// owned by `_registeredMap` are reached via `_marshalFind`, not here.)
|
|
447
|
+
if (!this._registeredMap.has(t)) this._registerTransient(handleFrom(h));
|
|
448
|
+
return;
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
_registerTransient = (handle: QuickJSHandle): void => {
|
|
452
|
+
this._transientHandles.add(handle);
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
_disposeTransient = (handle: QuickJSHandle): void => {
|
|
456
|
+
if (this._transientHandles.delete(handle) && handle.alive) handle.dispose();
|
|
424
457
|
};
|
|
425
458
|
|
|
426
459
|
_marshalPreApply = (target: (...args: any[]) => any, that: unknown, args: unknown[]): void => {
|
|
@@ -454,14 +487,32 @@ export class Arena {
|
|
|
454
487
|
registerHostRef: this._registerHostRef,
|
|
455
488
|
find: this._marshalFind,
|
|
456
489
|
pre: this._marshalPre,
|
|
490
|
+
registerTransient: this._registerTransient,
|
|
491
|
+
disposeTransient: this._disposeTransient,
|
|
457
492
|
preApply: this._marshalPreApply,
|
|
493
|
+
prepareReturn: this._prepareMarshalReturn,
|
|
458
494
|
custom: this._options?.customMarshaller,
|
|
459
495
|
});
|
|
460
496
|
|
|
497
|
+
// A top-level transient handle is disposed by the caller via `mayConsume`,
|
|
498
|
+
// so it must not also be retained (and re-disposed) by the transient set.
|
|
499
|
+
this._transientHandles.delete(handle);
|
|
500
|
+
|
|
461
501
|
const syncEnabled = this._options?.syncEnabled ?? true;
|
|
462
502
|
return [handle, !syncEnabled || !this._map.hasHandle(handle)];
|
|
463
503
|
};
|
|
464
504
|
|
|
505
|
+
_prepareMarshalReturn = (h: QuickJSHandle): QuickJSHandle => {
|
|
506
|
+
// A host function's return value is disposed by the VM once it is consumed.
|
|
507
|
+
// When sync is on, the VMMap retains object handles for identity, so the
|
|
508
|
+
// handle we hand back is the one the map owns: returning it directly would
|
|
509
|
+
// let the VM dispose the map's copy, leaving a stale entry that breaks
|
|
510
|
+
// `x === fn()` identity across calls. Hand the VM a dup instead and keep
|
|
511
|
+
// ours alive. With sync off, handles are not retained, so this is a no-op.
|
|
512
|
+
const syncEnabled = this._options?.syncEnabled ?? true;
|
|
513
|
+
return syncEnabled && h.alive && this._map.hasHandle(h) ? h.dup() : h;
|
|
514
|
+
};
|
|
515
|
+
|
|
465
516
|
_preUnmarshal = (t: any, h: QuickJSHandle): Wrapped<any> => {
|
|
466
517
|
return this._register(t, h, undefined, this._options?.syncEnabled ?? true)?.[0];
|
|
467
518
|
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import variant from "@jitl/quickjs-wasmfile-debug-sync";
|
|
2
|
+
import { newQuickJSWASMModuleFromVariant } from "quickjs-emscripten";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { Arena } from ".";
|
|
6
|
+
|
|
7
|
+
// A non-plain object that defaultIsMarshalable would reject, forcing the
|
|
8
|
+
// "json" marshal path.
|
|
9
|
+
class Weird {
|
|
10
|
+
x = 1;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const jsonIsMarshalable = (t: unknown) =>
|
|
14
|
+
typeof t !== "object" || t === null || Object.getPrototypeOf(t) === Object.prototype
|
|
15
|
+
? true
|
|
16
|
+
: "json";
|
|
17
|
+
|
|
18
|
+
async function withArena(
|
|
19
|
+
options: ConstructorParameters<typeof Arena>[1],
|
|
20
|
+
fn: (arena: Arena) => void,
|
|
21
|
+
) {
|
|
22
|
+
const mod = await newQuickJSWASMModuleFromVariant(variant as any);
|
|
23
|
+
const ctx = mod.newContext();
|
|
24
|
+
const arena = new Arena(ctx, options);
|
|
25
|
+
fn(arena);
|
|
26
|
+
arena.dispose();
|
|
27
|
+
// The debug-sync runtime aborts on dispose if any GC object handle leaked, so
|
|
28
|
+
// simply reaching here without an Emscripten abort means no leak.
|
|
29
|
+
expect(() => ctx.dispose()).not.toThrow();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("json marshal path handle leak", () => {
|
|
33
|
+
it("does not leak when a nested value is marshalled via json", async () => {
|
|
34
|
+
await withArena({ isMarshalable: jsonIsMarshalable }, arena => {
|
|
35
|
+
// Outer object goes through the proxy/object path; the nested `weird`
|
|
36
|
+
// property resolves to "json", creating a JSON.parse handle that nobody
|
|
37
|
+
// would otherwise dispose.
|
|
38
|
+
arena.expose({ obj: { weird: new Weird() } });
|
|
39
|
+
expect(arena.evalCode(`obj.weird.x`)).toBe(1);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("does not leak nor double-dispose a top-level json value", async () => {
|
|
44
|
+
await withArena({ isMarshalable: jsonIsMarshalable }, arena => {
|
|
45
|
+
// host fn receives a top-level json value as an argument; the handle is
|
|
46
|
+
// disposed by mayConsume and must not be re-disposed on dispose().
|
|
47
|
+
let received: any;
|
|
48
|
+
arena.expose({ sink: (v: any) => (received = v) });
|
|
49
|
+
arena.evalCode(`sink`);
|
|
50
|
+
const fn = arena.evalCode(`sink`);
|
|
51
|
+
fn(new Weird());
|
|
52
|
+
expect(received).toEqual({ x: 1 });
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("does not leak with the default marshal mode (json)", async () => {
|
|
57
|
+
// With no isMarshalable option the default mode is "json", so even a plain
|
|
58
|
+
// nested object exercises the json path that used to leak. (Functions are
|
|
59
|
+
// dropped by json serialization, so this path only carries plain values.)
|
|
60
|
+
await withArena(undefined, arena => {
|
|
61
|
+
arena.expose({ data: { a: { b: 1 } } });
|
|
62
|
+
expect(arena.evalCode(`data.a.b`)).toBe(1);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("does not leak when a json value round-trips VM -> host", async () => {
|
|
67
|
+
await withArena({ isMarshalable: jsonIsMarshalable }, arena => {
|
|
68
|
+
const received: any[] = [];
|
|
69
|
+
arena.expose({ host: { f: (v: any) => received.push(v) } });
|
|
70
|
+
// VM builds a non-plain object and passes it to the host callback.
|
|
71
|
+
arena.evalCode(`
|
|
72
|
+
const o = Object.create({ tag: "proto" });
|
|
73
|
+
o.a = 1;
|
|
74
|
+
host.f({ nested: o });
|
|
75
|
+
`);
|
|
76
|
+
expect(received).toEqual([{ nested: { a: 1 } }]);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("value-marshalled built-ins do not leak when nested", () => {
|
|
82
|
+
// Map/Set/Date/ArrayBuffer/TypedArray are excluded from proxy wrapping and
|
|
83
|
+
// marshalled by value, so they get no entry in `_map`. A top-level one is
|
|
84
|
+
// disposed by mayConsume, but a nested one used to have no owner and leaked
|
|
85
|
+
// its handle (aborting the debug runtime on dispose).
|
|
86
|
+
const builtinMarshalable = (t: unknown) =>
|
|
87
|
+
t instanceof Map || t instanceof Set ? true : jsonIsMarshalable(t);
|
|
88
|
+
|
|
89
|
+
it("nested Map", async () => {
|
|
90
|
+
await withArena({ isMarshalable: builtinMarshalable }, arena => {
|
|
91
|
+
arena.expose({ data: { m: new Map<string, unknown>([["b", new Weird()]]) } });
|
|
92
|
+
expect(arena.evalCode(`data.m.get("b").x`)).toBe(1);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("nested Set", async () => {
|
|
97
|
+
await withArena({ isMarshalable: builtinMarshalable }, arena => {
|
|
98
|
+
arena.expose({ data: { s: new Set<number>([1, 2]) } });
|
|
99
|
+
expect(arena.evalCode(`data.s.size`)).toBe(2);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("nested Date and TypedArray", async () => {
|
|
104
|
+
await withArena({ isMarshalable: true }, arena => {
|
|
105
|
+
arena.expose({ data: { d: new Date(0), a: new Uint8Array([1, 2, 3]) } });
|
|
106
|
+
expect(arena.evalCode(`data.a[1]`)).toBe(2);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("deeply nested Map", async () => {
|
|
111
|
+
await withArena({ isMarshalable: builtinMarshalable }, arena => {
|
|
112
|
+
arena.expose({ data: { a: { b: { m: new Map<string, number>([["x", 9]]) } } } });
|
|
113
|
+
expect(arena.evalCode(`data.a.b.m.get("x")`)).toBe(9);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("transient handles are freed as they are consumed (no accumulation)", () => {
|
|
119
|
+
// Marshal `make(i)` many times, disposing the top-level handle each time
|
|
120
|
+
// (mirroring `mayConsume`). With syncEnabled:false nothing is retained in
|
|
121
|
+
// `_map`, so any growth in live VM memory / object count is a genuine leak of
|
|
122
|
+
// a nested transient (json copy or BigInt) that was not disposed.
|
|
123
|
+
async function liveGrowth(
|
|
124
|
+
make: (i: number) => unknown,
|
|
125
|
+
isMarshalable: ConstructorParameters<typeof Arena>[1] extends infer O
|
|
126
|
+
? O extends { isMarshalable?: infer M }
|
|
127
|
+
? M
|
|
128
|
+
: never
|
|
129
|
+
: never,
|
|
130
|
+
) {
|
|
131
|
+
const mod = await newQuickJSWASMModuleFromVariant(variant as any);
|
|
132
|
+
const ctx = mod.newContext();
|
|
133
|
+
const arena = new Arena(ctx, { isMarshalable, syncEnabled: false }) as any;
|
|
134
|
+
const mem = () => ctx.runtime.computeMemoryUsage().consume((h: any) => ctx.dump(h));
|
|
135
|
+
const marshalOne = (i: number) => {
|
|
136
|
+
// `_marshal` returns [handle, shouldBeDisposed]; honour it like mayConsume.
|
|
137
|
+
const [h, shouldDispose] = arena._marshal(make(i));
|
|
138
|
+
if (shouldDispose && h.alive) h.dispose();
|
|
139
|
+
};
|
|
140
|
+
// Warm up so one-time shape/atom allocations are not counted.
|
|
141
|
+
for (let i = 0; i < 30; i++) marshalOne(i);
|
|
142
|
+
const before = mem();
|
|
143
|
+
for (let i = 0; i < 200; i++) marshalOne(i + 100000);
|
|
144
|
+
const after = mem();
|
|
145
|
+
arena.dispose();
|
|
146
|
+
ctx.dispose();
|
|
147
|
+
return {
|
|
148
|
+
usedBytes: after.memory_used_size - before.memory_used_size,
|
|
149
|
+
objects: after.obj_count - before.obj_count,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
it("does not leak nested BigInt values", async () => {
|
|
154
|
+
// Pre-fix this leaked ~7 bytes of BigInt storage per iteration.
|
|
155
|
+
const { usedBytes } = await liveGrowth(i => ({ b: BigInt(i) * 99999999n }), true);
|
|
156
|
+
expect(usedBytes).toBeLessThan(400);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("does not accumulate nested json objects", async () => {
|
|
160
|
+
// Pre-fix each iteration retained one JSON.parse object until dispose.
|
|
161
|
+
const { objects } = await liveGrowth(() => ({ w: new Weird() }), jsonIsMarshalable);
|
|
162
|
+
expect(objects).toBeLessThan(20);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("frees json/BigInt values stored in a Map or Set", async () => {
|
|
166
|
+
// A top-level Map/Set so the collection handle itself is not retained; this
|
|
167
|
+
// isolates the lifetime of its BigInt/json entries, which must be freed.
|
|
168
|
+
const collMarshalable = (t: unknown) =>
|
|
169
|
+
t instanceof Map || t instanceof Set ? true : jsonIsMarshalable(t);
|
|
170
|
+
const map = await liveGrowth(
|
|
171
|
+
i => new Map<string, unknown>([["a", BigInt(i)], ["b", new Weird()]]),
|
|
172
|
+
collMarshalable,
|
|
173
|
+
);
|
|
174
|
+
expect(map.usedBytes).toBeLessThan(400);
|
|
175
|
+
const set = await liveGrowth(() => new Set<unknown>([new Weird()]), collMarshalable);
|
|
176
|
+
expect(set.usedBytes).toBeLessThan(400);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -30,10 +30,12 @@ test("normal func", async () => {
|
|
|
30
30
|
expect(ctx.dump(result)).toBe("hoge");
|
|
31
31
|
expect(innerfn).toBeCalledWith(1, true);
|
|
32
32
|
expect(marshal).toHaveBeenLastCalledWith("hoge");
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
expect(unmarshal
|
|
33
|
+
// `this` coerces to the VM global for a plain call, which marshalFunction
|
|
34
|
+
// treats as undefined without unmarshalling it, so only the two args are
|
|
35
|
+
// unmarshalled.
|
|
36
|
+
expect(unmarshal).toBeCalledTimes(2);
|
|
37
|
+
expect(unmarshal.mock.results[0].value).toBe(1);
|
|
38
|
+
expect(unmarshal.mock.results[1].value).toBe(true);
|
|
37
39
|
|
|
38
40
|
handle.dispose();
|
|
39
41
|
ctx.dispose();
|