Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add custom function constructor option #711

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open

Add custom function constructor option #711

wants to merge 2 commits into from

Conversation

ncake
Copy link

@ncake ncake commented Jan 15, 2023

This adds an option called functionClass, which purpose is to allow to bring your own execution runtime.

Here's a basic example of how one could integrate quickjs-emscripten module to execute code in a WASM-powered virtual machine.

// index.js
import LoadSafeEJS from "./safe-ejs.js";

const safeEJS = await LoadSafeEJS();
const res = safeEJS.render("<%- 'hello ' + world %>", {world: 'world'});
console.log(res);
safeEJS.dispose();
// safe-ejs.js
import ejs from 'ejs/ejs.js';
import { getQuickJS } from "quickjs-emscripten"

/** @param {import('quickjs-emscripten/dist').QuickJSWASMModule} QuickJS */
function SafeEJS(QuickJS){
	const vm = QuickJS.newContext();

	this.dispose = function(){
		vm.dispose();
	};

	this.render = function(source, data, ejsOptions){
		return ejs.render(source, data, {...ejsOptions, functionClass})
	};

	const functionClass = function(argNames, funcBody){
		// not implemented here:
		// - caching the function between calls (thus useless for ejs.compile)
		// - async, include(), probably more
		return (locals, escapeFn, include, rethrow) => {
			// create function from source
			const ctor = vm.getProp(vm.global, 'Function');
			const argNamesHandle = vm.newString(argNames);
			const funcBodyHandle = vm.newString(funcBody);
			const newFuncRet = vm.callFunction(ctor, vm.undefined, argNamesHandle, funcBodyHandle);
			ctor.dispose();
			argNamesHandle.dispose();
			funcBodyHandle.dispose();
			const newFunc = vm.unwrapResult(newFuncRet);
			// wrap user-passed data into quickjs values
			const handleList = [];
			const makeDisposable = handle => (handleList.unshift(handle), handle);
			const localsHandle = wrapValue(locals, makeDisposable);
			// wrap the rest of the arguments
			const escapeFnHandle = vm.newFunction('escapeFn', strHandle => {
				const str = vm.getString(strHandle);
				const res = escapeFn(str);
				return vm.newString(res);
			});
			const includeHandle = vm.undefined;
			const rethrowHandle = vm.newFunction('rethrow',
				(errHandle, strHandle, flnmHandle, linenoHandle) => {
					const str = vm.getString(strHandle);
					const flnm = vm.getString(flnmHandle);
					const lineno = vm.getNumber(linenoHandle);
					const errMsg = vm.getProp(errHandle, 'message');
					const errName = vm.getProp(errHandle, 'name');
					const errStack = vm.getProp(errHandle, 'stack');
					const err = new Error();
					if(errMsg !== vm.undefined) err.message = vm.getString(errMsg);
					if(errName !== vm.undefined) err.name = vm.getString(errName);
					if(errStack !== vm.undefined) err.stack = vm.getString(errStack);
					rethrow(err, str, flnm, lineno, escapeFn);
				}
			);
			// execute our function
			const ret = vm.callFunction(
				newFunc, vm.undefined, localsHandle,
				escapeFnHandle, includeHandle, rethrowHandle
			);
			// dispose of everything to prevent memory leaks
			escapeFnHandle.dispose();
			rethrowHandle.dispose();
			for(const handle of handleList){
				handle.dispose();
			}
			newFunc.dispose();
			// return or throw an error
			const res = vm.unwrapResult(ret);
			const str = vm.getString(res);
			res.dispose();
			return str;
		};
	};

	const wrapValue = function(value, makeDisposable){
		if(value === undefined) return vm.undefined;
		if(value === null) return vm.null;
		if(typeof value === "boolean") return value ? vm.true : vm.false;
		if(typeof value === "number") return makeDisposable( vm.newNumber(value) );
		if(Array.isArray(value)){
			const arr = makeDisposable( vm.newArray() );
			for(let i = (value.length - 1); i >= 0; i--)
				vm.setProp(arr, i, wrapValue(value[i], makeDisposable));
			return arr;
		}
		if(typeof value === "object"){
			const obj = makeDisposable( vm.newObject() );
			for(const key in value)
				vm.setProp(obj, key, wrapValue(value[key], makeDisposable));
			return obj;
		}
		if(value.toString) return makeDisposable( vm.newString(value.toString()) );
		return vm.undefined;
	};
}

async function LoadSafeEJS(){
	return new SafeEJS(await getQuickJS());
}

export default LoadSafeEJS;

@ralyodio
Copy link

ralyodio commented Sep 3, 2023

can we get this merged?

Copy link

@ralyodio ralyodio left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@teoboley
Copy link

Can this get merged? I would love to use quickjs-sandboxed templates in my app

@teoboley
Copy link

@mde pls

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants