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

chore: quickjs PoC #488

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = {};
Copy link
Contributor Author

Choose a reason for hiding this comment

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


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) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

keep in mind that aoGlobal is probably no longer neede - and all the stuff that was inside in aoGlobal should be kept in ao.env

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