300x250 AD TOP

Search This Blog

Paling Dilihat

Powered by Blogger.

Wednesday, November 23, 2022

Embedding QuickJS in your Projects

QuickJS is small and embeddable Javascript engine made by the famous Fabrice Bellard, it supports ES2020 specifications and made available under MIT license.

You might have read about my porting of his TinyEMU to ESP32 which could boot linux.



During my hunt for scripting engine that can be integrated into a new embedded product I've encountered MicroPython and QuickJS, while the documentation is a bit lacking, the code was much easier to read and compile, there are no complicated build scripts and it was basically dropping the last version in the lib folder and selecting which files should be compiled with library.json

Once the library was compiled, integrating it into my demo was very easy and supporting multiple execution contexts is very easy since its not sharing context and variables.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
JSRuntime *rt = JS_NewRuntime();
if (!rt)
{
    fprintf(stderr, "qjs: cannot allocate JS runtime\n");
}

JS_SetMemoryLimit(rt, 80 * 1024);
JS_SetMaxStackSize(rt, 10 * 1024);

JSContext *ctx = JS_NewContext(rt);
if (!ctx)
{
    fprintf(stderr, "qjs: cannot allocate JS context\n");
}

The JSRuntime object represents the JavaScript engine, its responsible for memory allocation and C function calls. 

The JSContext object respresents the execution context where JavaScript functions and variables live.

Our demo workload

1
2
3
function transform(a, b) { 
    return (a ^ 2 / Math.sin(2 * Math.PI / b)) - a / 2; 
}

We'll eval(uate) the function to get it into the context

1
2
3
4
5
6
const char *expr = "function transform(a,b){return  (a^2/Math.sin(2*Math.PI/b))-a/2;}";
JSValue r = JS_Eval(ctx, expr, strlen(expr), "", 0);
if (JS_IsException(r))
{
    printf("Error evaluating script\r\n");
}

And once we have the function in the context, we can get it by name and execute it with the a and b arguments

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
JSValue args[2];
args[0] = JS_NewFloat64(ctx, (double)i);
args[1] = JS_NewFloat64(ctx, (double)i);
JSValue res = JS_Call(ctx, func, global, 2, args);
if (JS_IsException(res))
{
    printf("Error Executing transform\r\n");
}

if (!JS_IsNumber(res))
{
    printf("is not number!\r\n");
}

double result;
if (JS_ToFloat64(ctx, &result, res))
{
    printf("error parsing number\r\n");
}

Summary

QuickJS looks very interesting as a JavaScript runtime engine, its relatively fast and the code seems self explanatory. It has been embedded in rust, as an isolated VM in Node JS and more.

While QuickJS is different from MicroPython, its faster to execute the same workload, it was more readable for me and faster to setup and compile, one of the major drawbacks is that if you want to use it as an alternative to MicroPython, you'll need to implement your own hardware drivers for SPI, I2C etc'.

You may find Carlos Alberto's Writing native modules in C for QuickJS engine useful.

Lastly, I've attempted to modify QuickJS to use floats instead of double since ESP32 FPU is single precision only, it will probably make it non-standard and fail many JavaScript standard tests but I've included it here anyway.

In my particular use case:

  • x64 native vs QuickJS slowdown is about x9 times
  • ESP32 native vs QuickJS slowdown is about x63 times
  • ESP32 native vs QuickJS using float slowdown is about x25 times

You may view the official benchmarks here.

As always you can find the fruits of my labor at my GitHub account.







Tags: , , , ,

0 comments:

Post a Comment