let wasm = undefined; let nextId = 1; const contexts = new Map(); // import {a, b} from 'xxx' // import ab from 'xxx' // import * as ab from 'xxx' const re1 = /import\s+[\w\s,\*\}\{]*?\s+from\s+['"]([^'"]+)['"]/g; // import 'xxx' const re2 = /import\s+['"]([^'"]+)['"]/g; // export * from 'xxx' const re3 = /export\s+[\w\s,\*\}\{]*?\s+from\s+['"]([^'"]+)['"]/g; const res = [re1, re2, re3]; function* extractImportFroms(script) { if (!script) { return } for (const re of res) { for (const match of script.matchAll(re)) { yield match[1]; } } } class Context { options = undefined; ctx = undefined; moduleContents = {}; createPromise; invokeSandboxFunction; deleteSandboxFunction; hostFunctions = new Map(); constructor(options) { this.options = options; this.ctx = wasm._newContext(); contexts.set(this.ctx, this); this.loadSync(` global.__s__ = global.__s__ || { nextId: 1, promises: new Map(), callbacks: new Map(), callbacksLookup: new Map(), inspectingObjects: new Map(), currentStack: '', hostInspect: undefined, // inject later createPromise() { const promiseId = this.nextId++; const result = { __p__: promiseId }; this.promises.set(promiseId, new Promise((resolve, reject) => { result.resolve = this.wrapSandboxFunction(resolve); result.reject = this.wrapSandboxFunction(reject); })); return result; }, wrapSandboxFunction(callback, extra) { let callbackId = this.callbacksLookup.get(callback); if (callbackId === undefined) { callbackId = this.nextId++; this.callbacks.set(callbackId, callback); this.callbacksLookup.set(callback, callbackId); } return { __c__: callbackId, ...extra }; }, getAndDeletePromise(promiseId) { const promise = this.promises.get(promiseId); this.promises.delete(promiseId); return promise; }, invokeSandboxFunction(callbackToken, args) { const callbackId = callbackToken.__c__; if (!callbackId) { throw new Error('invokeSandboxFunction with invalid token: ' + JSON.stringify(callbackToken)); } const callback = this.callbacks.get(callbackId); if (!callback) { return undefined; } return callback.apply(undefined, args); }, deleteSandboxFunction(callbackToken) { const callbackId = callbackToken.__c__; if (!callbackId) { throw new Error('deleteSandboxFunction with invalid token: ' + JSON.stringify(callbackToken)); } let callback = this.callbacks.get(callbackId); if (callback !== undefined) { this.callbacks.delete(callbackId); this.callbacksLookup.delete(callback); } }, inspect(msg, obj) { const objId = this.nextId++; this.inspectingObjects.set(objId, obj); this.hostInspect(msg, typeof obj === 'object' ? { __o__: objId, keys: Reflect.ownKeys(obj) } : obj); }, getInspectingObjectProp(objId, prop) { const val = this.inspectingObjects.get(objId)[prop]; if (val && typeof val === 'object') { const valObjId = this.nextId++; this.inspectingObjects.set(valObjId, val); return { __o__: valObjId, keys: Reflect.ownKeys(val) }; } return val; }, asHostFunction(hostFunctionToken) { return (...args) => { return this.invokeHostFunction(hostFunctionToken, args); }; }, invokeHostFunction(hostFunctionToken, args) { if (!hostFunctionToken.nowrap) { args = args.map(arg => typeof arg === 'function' ? this.wrapSandboxFunction(arg, { once: true }) : arg); if (args[0] && typeof args[0] === 'object') { for (const [k, v] of Object.entries(args[0])) { if (typeof v === 'function') { args[0][k] = this.wrapSandboxFunction(v, { once: true }); } } } } const invokeResult = __invokeHostFunction(JSON.stringify(hostFunctionToken), JSON.stringify(args)); if (hostFunctionToken.returnsHostObject && invokeResult?.__h__) { return this.asHostFunction(invokeResult); } if (invokeResult?.__p__) { return this.getAndDeletePromise(invokeResult.__p__); } return invokeResult; }, callMethod(hostObj, method, ...args) { const result = hostObj('callMethod', method, args); return result?.__h__ ? this.asHostFunction(result) : result; }, getProp(hostObj, prop) { return hostObj('getProp', prop); }, setProp(hostObj, prop, propVal) { return hostObj('setProp', prop, propVal); }, deleteHostObject(hostObj) { return hostObj('delete'); } }; `); this.createPromise = this.def(`return __s__.createPromise()`); this.invokeSandboxFunction = this.def(`return __s__.invokeSandboxFunction(...arguments)`); this.deleteSandboxFunction = this.def(`return __s__.deleteSandboxFunction(...arguments)`); this.getInspectingObjectProp = this.def(`return __s__.getInspectingObjectProp(...arguments)`); this.currentStack = this.def(`return __s__.currentStack`); if (options?.global) { this.inject('global', options.global); for (const [k, v] of Object.entries(options.global)) { if (typeof v === 'object') { this.inject(k, v); } } } this.def(`__s__.hostInspect = arguments[0]`)(this.wrapHostFunction(this.hostInspect.bind(this), { nowrap: true })); } dispose() { if (!this.ctx) { return; // already disposed } for (const pModuleContent of Object.values(this.moduleContents)) { wasm._free(pModuleContent); } wasm._freeContext(this.ctx); contexts.delete(this.ctx); this.ctx = undefined; } hostInspect(msg, obj) { obj = this.wrapProxy(obj); console.warn('inspecting...', msg, obj); debugger; } wrapProxy(obj) { if (!obj) { return obj; } if (!obj.__o__) { return obj; } const proxy = {}; for (const key of obj.keys) { Object.defineProperty(proxy, key, { enumerable: true, get: () => { return this.wrapProxy(this.getInspectingObjectProp(obj.__o__, key)); } }); } return proxy; } asSandboxFunction(callbackToken) { let calledOnce = false; return (...args) => { if (!this.ctx) { return; } if (callbackToken.once) { if (calledOnce) { throw new Error(`callback ${JSON.stringify(callbackToken)} can only be callback once, if need to callback multiple times, use __s__.wrapSandboxFunction to manage callback lifetime explicitly`) } calledOnce = true; } try { return this.invokeSandboxFunction(callbackToken, args); } finally { if (callbackToken.once) { this.deleteSandboxFunction(callbackToken); } } } } inject(target, obj) { const args = [target]; for (const [k, v] of Object.entries(obj)) { if (typeof v === 'function') { args.push(k); args.push(this.wrapHostFunction(v)) } else { args.push(k); args.push(v); } } const f = this.def(` const obj = global[arguments[0]] = global[arguments[0]] || {}; for (let i = 1; i < arguments.length; i+=2) { obj[arguments[i]] = arguments[i+1]; }`); f(...args); } async dynamicImport({ctx, argc, argv, resolveFunc, rejectFunc, basename, filename }) { try { if (this.options?.loadModuleContent) { await this.require(basename, filename) } } catch(e) { wasm._call(ctx, rejectFunc, allocateUTF8(JSON.stringify(`failed to dynamicImport: ${e}`))); wasm._freeJsValue(ctx, resolveFunc); wasm._freeJsValue(ctx, rejectFunc); wasm._free(argv); return; } wasm._doDynamicImport(ctx, argc, argv); wasm._freeJsValue(ctx, resolveFunc); wasm._freeJsValue(ctx, rejectFunc); wasm._free(argv); } async require(basename, filename) { if (!this.options?.loadModuleContent) { throw new Error(`missing options.loadModuleContent can not load content of ${filename} imported by ${basename}`); } let moduleName = filename; if (filename[0] === '.') { const pBasename = allocateUTF8(basename); const pFilename = allocateUTF8(filename); const pModuleName = wasm._pathJoin(this.ctx, pBasename, pFilename); moduleName = wasm.UTF8ToString(pModuleName); wasm._free(pModuleName); } if (this.moduleContents[moduleName] !== undefined) { return; } this.moduleContents[moduleName] = 0; const content = await this.options.loadModuleContent(moduleName, { basename, filename }); this.moduleContents[moduleName] = allocateUTF8(content); const promises = []; for (const importFrom of extractImportFroms(content)) { promises.push(this.require(moduleName, importFrom)); } await Promise.all(promises); } loadSync(content, options) { const filename = options?.filename || ``; const pScript = allocateUTF8(content); const pScriptName = allocateUTF8(filename) const meta = options?.meta || { url: filename }; const pMeta = allocateUTF8(JSON.stringify(meta)); const pError = wasm._load(this.ctx, pScript, pScriptName, pMeta); if (pError) { const error = new Error(wasm.UTF8ToString(pError)); wasm._free(pError); throw error; } } async load(content, options) { const filename = options?.filename || ``; const promises = []; for (const importFrom of extractImportFroms(content)) { promises.push(this.require(filename, importFrom)); } await Promise.all(promises); this.loadSync(content, { ...options, filename }); if (!this._loadModule) { this._loadModule = await this.def(` return (async() => { const [moduleName] = arguments; const m = await import(moduleName); const exports = {}; for(const [k, v] of Object.entries(m)) { if (typeof v === 'function') { exports[k] = __s__.wrapSandboxFunction(v); } else { exports[k] = v; } } return JSON.stringify(exports); })(); `) } const loadedModule = JSON.parse(await this._loadModule(filename)); for (const [k, v] of Object.entries(loadedModule)) { if (v?.__c__) { loadedModule[k] = this.asSandboxFunction(v); } } return loadedModule; } def(script, options) { if (!this.ctx) { throw new Error('context has been disposed'); } return (...args) => { if (!this.ctx) { throw new Error('context has been disposed'); } const invocation = new Invocation(this, options?.timeout); const setSuccessToken = invocation.wrapHostFunction(invocation.setSuccess.bind(invocation), { nowrap: true }); const setFailureToken = invocation.wrapHostFunction(invocation.setFailure.bind(invocation), { nowrap: true }); const encodedArgs = args.map((arg, index) => typeof arg === 'function' ? invocation.wrapHostFunction(arg, { argIndex: index}) : arg); this.loadSync(` (() => { const setSuccess = __s__.asHostFunction(${JSON.stringify(setSuccessToken)}); const setFailure = __s__.asHostFunction(${JSON.stringify(setFailureToken)}); const args = ${JSON.stringify(encodedArgs)}; function f() { ${script} } try { const result = f.apply(undefined, args.map(arg => arg?.__h__ ? __s__.asHostFunction(arg) : arg)); if (result && result.then && result.catch) { result .then(v => { setSuccess(v); }) .catch(e => { setFailure('' + e + '' + e.stack); }) } else { setSuccess(result); } } catch(e) { setFailure('' + e + '' + e.stack); } })(); `, options); (async () => { try { await invocation.asyncResult; } catch (e) { // ignore } finally { invocation.dispose(); } })(); return invocation.syncResult(); } } wrapHostFunction(f, extra) { const hfId = nextId++; this.hostFunctions.set(hfId, f); return { __h__: hfId, ...extra } } deleteHostFunction(hostFunctionToken) { const hfId = hostFunctionToken.__h__; if (!hfId) { throw new Error('deleteHostFunction with invalid token: ' + JSON.stringify(hostFunctionToken)); } this.hostFunctions.delete(hfId); } invokeHostFunction(hostFunctionToken, args) { const hfId = hostFunctionToken.__h__; if (!hfId) { throw new Error('callHostFunction with invalid token: ' + JSON.stringify(hostFunctionToken)); } if (!hostFunctionToken.nowrap) { args = args.map(arg => arg?.__c__ ? this.asSandboxFunction(arg) : arg); if (args[0] && typeof args[0] === 'object') { for (const [k, v] of Object.entries(args[0])) { if (v?.__c__) { args[0][k] = this.asSandboxFunction(v); } } } } const hostFunc = this.hostFunctions.get(hfId); if (hostFunc === undefined) { throw new Error('host function not found: ' + JSON.stringify(hostFunctionToken)); } const invokeResult = hostFunc(...args); if (invokeResult && invokeResult.then && invokeResult.catch) { const { __p__, resolve, reject } = this.createPromise(); invokeResult .then(v => { if (this.ctx) { this.invokeSandboxFunction(resolve, [v]); } }) .catch(e => { if (this.ctx) { this.invokeSandboxFunction(reject, ['' + e]); } }) .finally(() => { if (this.ctx) { this.deleteSandboxFunction(resolve); this.deleteSandboxFunction(reject); } }); return { __p__ }; } if (hostFunctionToken.returnsHostObject) { return this.wrapHostObject(invokeResult); } return invokeResult; } wrapHostObject(val) { if (!val) { return val; } if (typeof val !== 'object') { return val; } const token = this.wrapHostFunction((action, prop, args) => { switch(action) { case 'callMethod': return this.wrapHostObject(val[prop](...args)); case 'getProp': return this.wrapHostObject(val[prop]); case 'setProp': val[prop] = args; return undefined; case 'delete': this.deleteHostFunction(token); return undefined; } throw new Error(`unknown action: ${action}`); }) return token; } } class Invocation { context; asyncResult; syncResult; resolveAsyncResult; rejectAsyncResult; hostFunctionTokens = []; constructor(context, timeout) { this.context = context; this.asyncResult = new Promise((resolve, reject) => { this.resolveAsyncResult = resolve; this.rejectAsyncResult = reject; }); this.syncResult = () => { return Promise.race([this.asyncResult, (async () => { if (timeout) { await new Promise(resolve => setTimeout(resolve, timeout)); throw new Error('execute function timeout'); } else { const noResult = this.syncResult; while (this.syncResult === noResult) { await new Promise(resolve => setTimeout(resolve, 1000)); } } })()]); }; } dispose() { for (const hostFunctionToken of this.hostFunctionTokens) { this.context.deleteHostFunction(hostFunctionToken); } } wrapHostFunction(f, extra) { const hostFunctionToken = this.context.wrapHostFunction(f, extra); this.hostFunctionTokens.push(hostFunctionToken); return hostFunctionToken; } setSuccess(value) { this.syncResult = () => value; this.resolveAsyncResult(value); return 0; } setFailure(error) { this.syncResult = () => { throw new Error(error) }; this.rejectAsyncResult(new Error(error)); return 0; } } function allocateUTF8(string) { if (string === undefined) { return 0; } return wasm.allocateUTF8(string); } module.exports = function (wasmProvider) { async function loadWasm(options) { if (!wasm) { wasm = await wasmProvider(options); wasm.dynamicImport = (ctx, argc, argv, resolveFunc, rejectFunc, basename, filename) => { basename = wasm.UTF8ToString(basename); filename = wasm.UTF8ToString(filename); const context = contexts.get(ctx); if (!context) { wasm._call(ctx, rejectFunc, allocateUTF8(JSON.stringify('internal error: context not found'))); wasm._freeJsValue(ctx, resolveFunc); wasm._freeJsValue(ctx, rejectFunc); wasm._free(argv); return; } context.dynamicImport({ ctx, argc, argv, resolveFunc, rejectFunc, basename, filename }); } wasm.getModuleContent = (ctx, filename) => { filename = wasm.UTF8ToString(filename); const context = contexts.get(ctx); if (!context) { throw new Error(`getModuleContent of ${filename} missing context`) } return context.moduleContents[filename]; } wasm.invokeHostFunction = (ctx, token, args) => { token = wasm.UTF8ToString(token); args = wasm.UTF8ToString(args); const context = contexts.get(ctx); if (!context) { throw new Error(`invokeHostFunction missing context`); } try { const result = JSON.stringify(context.invokeHostFunction(JSON.parse(token), JSON.parse(args))); // eval.c invokeHostFunction will free this memory return allocateUTF8(result); } catch(e) { return allocateUTF8(JSON.stringify({ __e__: e?.stack || `${e}`})); } } } return wasm; } async function defineFunction(script, options) { await loadWasm(options); return (...args) => { // start a isolated context for each invocation const ctx = new Context(options); function defAndCall() { const f = ctx.def(script, options); let result = undefined; try { return result = f(...args); } finally { if (result && result.finally) { result.finally(ctx.dispose.bind(ctx)); } else { ctx.dispose(); } } } return defAndCall(); }; }; defineFunction.context = async (contextOptions) => { // share context between invocations await loadWasm(contextOptions); const ctx = new Context(contextOptions); return { def(script, options) { return ctx.def(script, options); }, load(script, options) { return ctx.load(script, options) }, get currentStack() { return ctx.currentStack(); }, inject(target, obj) { ctx.inject(target, obj); }, wrapHostFunction(f, extra) { return ctx.wrapHostFunction(f, extra); }, dispose() { ctx.dispose(); } } }; defineFunction.default = defineFunction; // support import default return defineFunction; }