Skip to content

Commit

Permalink
chore: quickjs PoC
Browse files Browse the repository at this point in the history
  • Loading branch information
ppedziwiatr committed Jan 20, 2024
1 parent 5801737 commit c4f8985
Show file tree
Hide file tree
Showing 4 changed files with 395 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
"json-schema-to-typescript": "^11.0.1",
"node-stdlib-browser": "^1.2.0",
"prettier": "^2.3.2",
"quickjs-emscripten": "^0.26.0",
"rimraf": "^3.0.2",
"smartweave": "0.4.48",
"ts-jest": "^28.0.7",
Expand Down
264 changes: 264 additions & 0 deletions tools/quickjs-handle.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
import { DefaultIntrinsics, getQuickJS } from "quickjs-emscripten";

const globalsCode = `
class ProcessError extends Error {
constructor(message) {
super(message);
this.name = "ProcessError";
}
}
// add all stuff from https://cookbook_ao.g8way.io/references/ao.html
let ao = {
_version: "0.0.3",
id: "",
_module: "",
authorities: [],
_ref: 0,
outbox: {
Messages: [],
Spawns: []
},
env: {}
}
let currentState = {};
BigInt.prototype.toJSON = function () {
return this.toString();
};
// to be called by SDK when evaluating for already cached state - same as WASM handlers
function __initState(newState) {
console.log('__initState', newState);
currentState = newState;
}
// to be called by SDK after evaluating a message - same as WASM handlers
function __currentState() {
return JSON.stringify(currentState);
}
function __getOutbox() {
return JSON.stringify(ao.outbox);
}
`;

// the example smart contract code loaded from Arweave
// note: processCode MUST HAVE 'function handle(state, message, aoGlobals)'
const processCode = `
function handle(state, message, aoGlobals) {
console.log('handle', message, aoGlobals, state);
if (!state.hasOwnProperty('counter')) {
state.counter = 0;
}
if (message.action == 'increment') {
console.log('inside increment', state.counter);
state.counter++;
return;
}
if (message.action == 'currentValue') {
return {
result: state.counter
}
}
throw new ProcessError('unknown action');
}
`.trim();

/*const decorateProcessFn = (processCode) => {
return `
${processCode}
function __handleDecorator() {
return function(messageStringified, aoGlobalsStringified) {
console.log('handleDecorator');
// TODO: handle BigInt during parse - but how? maybe introduce some custom type "Amount"?
const message = JSON.parse(messageStringified);
const aoGlobals = JSON.parse(aoGlobalsStringified);
console.log('calling original handle');
const result = handle(currentState, message, aoGlobals);
return JSON.stringify(result);
}
}
__handleDecorator();
`;
};*/

const decorateProcessFnEval = (processCode) => {
return `
${processCode}
function __handleDecorator(message, aoGlobals) {
console.log('handleDecorator');
const result = handle(currentState, message, aoGlobals);
return JSON.stringify(result);
}
`;
};

async function main() {
const QuickJS = await getQuickJS();

// 1. creating the QJS runtime with proper memory/cycles limits
const runtime = QuickJS.newRuntime();
// TODO: memoryLimit, stack size and interrupt cycles should be configurable?
runtime.setMemoryLimit(1024 * 640);
// Limit stack size
runtime.setMaxStackSize(1024 * 320);
// Interrupt computation after 1024 calls to the interrupt handler
let interruptCycles = 0;
runtime.setInterruptHandler(() => ++interruptCycles > 1024);

// 2. creating the QJS context with proper intrinsics
const vm = runtime.newContext({
intrinsics: {
...DefaultIntrinsics,
Date: false,
Proxy: false,
Promise: false,
MapSet: false,
BigFloat: false,
BigInt: true,
BigDecimal: false
}
});

// 3. example of registering functions from Host: registering "console.log" API
const logHandle = vm.newFunction("log", (...args) => {
const nativeArgs = args.map(vm.dump);
console.log("QuickJS:", ...nativeArgs);
});
const consoleHandle = vm.newObject();
vm.setProp(consoleHandle, "log", logHandle);
vm.setProp(vm.global, "console", consoleHandle);
consoleHandle.dispose();
logHandle.dispose();

// 4. evaluating globals
console.log("evaluating globals");
const globalsResult = vm.evalCode(globalsCode);
if (globalsResult.error) {
console.log("Globals eval failed:", vm.dump(globalsResult.error));
globalsResult.error.dispose();
} else {
globalsResult.value.dispose();
}

const initStateResult = vm.evalCode(`__initState(${JSON.stringify({ "counter": 666 })})`);
if (initStateResult.error) {
console.log("initState failed:", vm.dump(initStateResult.error));
initStateResult.error.dispose();
} else {
initStateResult.value.dispose();
}

// 5. evaluating decorated process function

// version with function
/*const handleFnResult = vm.evalCode(decorateProcessFn(processCode));
if (handleFnResult.error) {
console.log("HandleFn eval failed:", vm.dump(handleFnResult.error));
handleFnResult.error.dispose();
} else {
// note: this is a handle to the wrapped process function
const handleFn = vm.unwrapResult(handleFnResult);
// actually calling process function
doCall(handleFn, vm, "increment");
doCall(handleFn, vm, "increment");
doCall(handleFn, vm, "increment");
doCall(handleFn, vm, "increment");
const currentCounterValue = doCall(handleFn, vm, "currentValue");
console.log(currentCounterValue);
handleFn.dispose();
}*/

// version with evalCode
const handleFnResult = vm.evalCode(decorateProcessFnEval(processCode));
if (handleFnResult.error) {
console.log("HandleFn eval failed:", vm.dump(handleFnResult.error));
handleFnResult.error.dispose();
} else {
handleFnResult.value.dispose();
}

// actually calling process function
doCallEval(vm, "increment");
doCallEval(vm, "increment");
doCallEval(vm, "increment");
doCallEval(vm, "increment");


const currentCounterValue = doCallEval(vm, "currentValue");
console.log(currentCounterValue);

// 6. test error handling
try {
doCallEval(vm, "foobar");
} catch (e) {
console.error(e);
}

function doCallEval(vm, processFunction) {
const evalResult = vm.evalCode(
`__handleDecorator(
${JSON.stringify({ "action": processFunction })},
${JSON.stringify({ "owner": "just_ppe", "id": 123 })}
)`);

if (evalResult.error) {
const error = vm.dump(evalResult.error);
console.log("eval failed", error);
evalResult.error.dispose();
throw new Error('Eval error', { cause: error });
} else {
const resultValue = evalResult.value;
const stringValue = vm.getString(resultValue);
const result = stringValue === "undefined" ?
undefined
: JSON.parse(vm.getString(resultValue));
resultValue.dispose();
return result;
}
}

function doCall(handleFn, vm, processFunction) {
const evalResult = vm.callFunction(
handleFn,
vm.undefined,
vm.newString(JSON.stringify({ "action": processFunction })),
vm.newString(JSON.stringify({ "owner": "just_ppe" })));

if (evalResult.error) {
const error = vm.dump(evalResult.error);
console.log("eval failed", error);
evalResult.error.dispose();
throw new Error(error);
} else {
const resultValue = evalResult.value;
const stringValue = vm.getString(resultValue);
const result = stringValue === "undefined" ?
undefined
: JSON.parse(vm.getString(resultValue));
resultValue.dispose();
return result;
}
}

// btw: if the below throws an error, this means some
// of earlier values was not properly disposed
vm.dispose();
runtime.dispose();
}

main().finally(() => console.log("I'm done here."));
79 changes: 79 additions & 0 deletions tools/quickjs.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { getQuickJS, newQuickJSAsyncWASMModule } from "quickjs-emscripten";

// the example smart contract code loaded from Arweave blockchain
const code = `
function handle(state, action) {
console.log('handle before timeout');
const timeoutResult = timeout(100); // no 'await' here, because fuck logic
console.log('handle after timeout:', timeoutResult);
const someShit = {};
for (i = 0; i < 100000; i++) {
someShit[""+i] = i*i;
}
return 1;
}
`.trim();

async function main() {

// 1. creating the QJS context
const QuickJS = await newQuickJSAsyncWASMModule();
const runtime = QuickJS.newRuntime();
// "Should be enough for everyone" -- attributed to B. Gates
// runtime.setMemoryLimit(1024 * 640);
// Limit stack size
runtime.setMaxStackSize(1024 * 320);
let interruptCycles = 0
runtime.setInterruptHandler((runtime) => { interruptCycles++ });

const vm = runtime.newContext();

// 2. registering APIs
const logHandle = vm.newFunction("log", (...args) => {
const nativeArgs = args.map(vm.dump)
console.log("QuickJS:", ...nativeArgs)
});

const consoleHandle = vm.newObject();
vm.setProp(consoleHandle, "log", logHandle);
vm.setProp(vm.global, "console", consoleHandle);
consoleHandle.dispose();
logHandle.dispose();

const timeoutHandle = vm.newAsyncifiedFunction("timeout", async (msHandle) => {
const ms = vm.getNumber(msHandle);
console.log("omg, that's an async shit!");
await timeout(1000);
return vm.newString("check your head!");
})
timeoutHandle.consume((fn) => vm.setProp(vm.global, "timeout", fn))

// 4. calling the "handle" function
const result = await vm.evalCodeAsync(`(() => {
${code}
return handle();
})()`);

if (result.error) {
console.log('Execution failed:', vm.dump(result.error));
result.error.dispose();
} else {
const parsedResult = vm.unwrapResult(result).consume(vm.getNumber);
console.log("result", parsedResult);
console.log("Cycles", interruptCycles);
}

vm.dispose();
runtime.dispose();
}

main().finally();

function timeout(delay) {
return new Promise(function (resolve) {
setTimeout(resolve, delay);
});
}
Loading

0 comments on commit c4f8985

Please sign in to comment.