Try the lightweight JavaScript engine “QuickJS” that can be incorporated into C / C ++

Introduction

** QuickJS ** is a lightweight JavaScript engine that can be embedded in C / C ++. The license is MIT license. It seems to be useful when you want to embed JavaScript but V8 is too over-engineered.

Click here for the official page, which is too simple in design and has a sense of trust.

QuickJS is a small and embeddable Javascript engine. It supports the ES2020 specification including modules, asynchronous generators, proxies and BigInt. QuickJS is a small, embeddable JavaScript engine that supports the ES2020 specification, including modules, asynchronous generators, proxies, and BigInt.

By the way, QuickJS author Fabrice Bellard is also the author of qemu and ffmpeg. Incarnation of behavioral power ... (Image omitted)

There is also an unofficial mirror on GitHub. This is linked from the official page as “Unofficial git mirror”. However, as of the writing of the article, the latest release (2020-01-05) is not reflected and it remains the 2019-10-27 version.

This article outlines how to incorporate QuickJS into C / C ++. It's like my memorandum, not systematic and exhaustive, so please understand in advance.

Operation check environment

Installation

You can install it by unpacking the tarball and doing make install as usual. By default, it is installed under / usr / local, but you can change it by specifying prefix. The following is written on the assumption that it will be installed in ~ / .local, so read it as appropriate.

#Download source
curl -LO https://bellard.org/quickjs/quickjs-2020-01-05.tar.xz
#Unpack the tarball
tar axvf quickjs-2020-01-05.tar.xz
#Build~/.Example of installing under local
# -j (--jobs)Is the number of parallel executions, so adjust accordingly
make -C quickjs-2020-01-05 -j 2 prefix="${HOME}/.local" install

If you want to use the qjs command, pass the PATH environment variable appropriately.

PATH="${HOME}/.local/bin:${PATH}"
export PATH

It can also be installed via AUR for Arch Linux users and Homebrew for macOS users.

Use the qjs / qjsc command

Launch the REPL

Calling the qjs command with no arguments launches the REPL.

$ qjs
QuickJS - Type "\h" for help
qjs > \h
\h          this help
\x          hexadecimal number display
\d         *decimal number display
\t          toggle timing display
\clear      clear the terminal
\q          exit
qjs > 3**2 + 4**2
25
qjs > 2n ** 256n
115792089237316195423570985008687907853269984665640564039457584007913129639936n
qjs > const name = "world"
undefined
qjs > `Hello, ${name}`
"Hello, world"
qjs > /^(a)(b*)(c+)(d?)$/.exec("abbbcc")
[ "abbbcc", "a", "bbb", "cc", "" ]
qjs >

Run the JS file

If you give a file name to the qjs command, that file will be executed. You can also ʻimport / ʻexport.

greeter.js


export function greet(name) {
  console.log(`Hello, ${name}!`);
}

index.js


import { greet } from "./greeter.js";

greet("Alice");
$ qjs index.js 
Hello, Alice!

Compile the JS file

You can make JavaScript an executable file using the qjsc command.

$ qjsc index.js
$ strip a.out
$ file a.out 
a.out: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=315625503ecf284b44cab3b6f1d3dea6df4dedc7, for GNU/Linux 3.2.0, stripped
$ stat -c '%s' a.out | numfmt --to=iec-i
832Ki
$ ./a.out 
Hello, Alice!

Embed QuickJS in a C / C ++ program

The main subject is from here.

The official documentation for the C API has only a few dozen lines of sections:

Well, from the header (quickjs.h), you can see the behavior, and it works as it is. After that, if you read the REPL source (qjs.c) and the implementation of the library itself (quickjs.c), you can understand how to use it.

The entire source code illustrated this time is placed in the following repository.

Call a JS function from the C side

To get started, let's call the foo function defined in JS using the C API. The code is as follows (By the way, this corresponds to the code example of Lua (programming language) --C API of Wikipedia (en). ing).

simple.c


#include <stddef.h>
#include <stdio.h>
#include <string.h>

#include <quickjs.h>

int main(void) {
  JSRuntime *rt = JS_NewRuntime();
  JSContext *ctx = JS_NewContext(rt);

  char *const fooCode = "function foo(x, y) { return x + y; }";
  if (JS_IsException(JS_Eval(ctx, fooCode, strlen(fooCode), "<input>", JS_EVAL_FLAG_STRICT))) {
    JS_FreeContext(ctx);
    JS_FreeRuntime(rt);
    return -1;
  }

  JSValue global = JS_GetGlobalObject(ctx);
  JSValue foo = JS_GetPropertyStr(ctx, global, "foo");
  JSValue argv[] = { JS_NewInt32(ctx, 5), JS_NewInt32(ctx, 3) };
  JSValue jsResult = JS_Call(ctx, foo, global, sizeof(argv) / sizeof(JSValue), argv);
  int32_t result;
  JS_ToInt32(ctx, &result, jsResult);
  printf("Result: %d\n", result);

  JSValue used[] = { jsResult, argv[1], argv[0], foo, global };
  for (int i = 0; i < sizeof(used) / sizeof(JSValue); ++i) {
    JS_FreeValue(ctx, used[i]);
  }

  JS_FreeContext(ctx);
  JS_FreeRuntime(rt);
  return 0;
}

Build the above source with the following command.

#compile(-Optimized for size with Os)
gcc -c -Os -Wall -I"${HOME}/.local/include" simple.c
#Link(-Wl,-strip with s)
gcc -Wl,-s -L"${HOME}/.local/lib/quickjs" simple.o -l quickjs -l m -o simple

When executed, the result of 5 + 3 is displayed.

$ ./simple
Result: 8

This code is processed in the following flow.

Call a C function from the JS side

An example of creating an application that executes the JavaScript given as a command line argument and displays the result on standard output. The operation image is as follows.

$ ./jseval '3**2 + 4**2'
25
$ ./jseval foo
ReferenceError: foo is not defined
$ ./jseval 'undefined'
$ ./jseval '[3, 4, 5].map(x => x ** 10).forEach(x => console.log(x))' 
59049
1048576
9765625

The code is as follows. console.log / console.error is implemented in C so that it can be used from JS.

jseval.c


#include <stdio.h>
#include <string.h>

#include <quickjs.h>

JSValue jsFprint(JSContext *ctx, JSValueConst jsThis, int argc, JSValueConst *argv, FILE *f) {
  for (int i = 0; i < argc; ++i) {
    if (i != 0) {
      fputc(' ', f);
    }
    const char *str = JS_ToCString(ctx, argv[i]);
    if (!str) {
      return JS_EXCEPTION;
    }
    fputs(str, f);
    JS_FreeCString(ctx, str);
  }
  fputc('\n', f);
  return JS_UNDEFINED;
}

JSValue jsPrint(JSContext *ctx, JSValueConst jsThis, int argc, JSValueConst *argv) {
  return jsFprint(ctx, jsThis, argc, argv, stdout);
}

JSValue jsPrintErr(JSContext *ctx, JSValueConst jsThis, int argc, JSValueConst *argv) {
  return jsFprint(ctx, jsThis, argc, argv, stderr);
}

void initContext(JSContext *ctx) {
  JSValue global = JS_GetGlobalObject(ctx);

  //Add console to globalThis
  JSValue console = JS_NewObject(ctx);
  JS_SetPropertyStr(ctx, global, "console", console);
  // console.set log
  JS_SetPropertyStr(ctx, console, "log", JS_NewCFunction(ctx, jsPrint, "log", 1));
  // console.Set error
  JS_SetPropertyStr(ctx, console, "error", JS_NewCFunction(ctx, jsPrintErr, "error", 1));

  JS_FreeValue(ctx, global);
}

int main(int argc, char const *argv[]) {
  int exitCode = 0;

  JSRuntime *rt = JS_NewRuntime();
  JSContext *ctx = JS_NewContext(rt);

  initContext(ctx);

  for (int i = 1; i < argc; ++i) {
    JSValue ret = JS_Eval(ctx, argv[i], strlen(argv[i]), "<input>", JS_EVAL_FLAG_STRICT);
    if (JS_IsException(ret)) {
      JSValue e = JS_GetException(ctx);
      jsPrintErr(ctx, JS_NULL, 1, &e);
      JS_FreeValue(ctx, e);
      exitCode = 1;
      break;
    } else if (JS_IsUndefined(ret)) {
      // nop
    } else {
      jsPrint(ctx, JS_NULL, 1, &ret);
    }
    JS_FreeValue(ctx, ret);
  }

  JS_FreeContext(ctx);
  JS_FreeRuntime(rt);
  return exitCode;
}

Let JS manage C / C ++ data

From here, the example code suddenly becomes C ++ instead of C. I've had a lot of trouble ...

For example, you want to use C ++ std :: mt19937 (pseudo-random number generator) on the JavaScript side as follows. Think.

const mt = new Mt19937();
for (let i = 0; i < 10; ++i) {
  console.log(mt.generate());  //random number(BigInt)To output
}

output


3499211612
581869302
3890346734
3586334585
545404204
4161255391
3922919429
949333985
2715962298
1323567403

This can be written as:

mt19937.cc


//Unique ID of Mt19937 class (initialized later)
//It is defined globally for the sake of simplicity, but it will break when multiple Runtimes are run at the same time, so it is necessary to do it appropriately.
static JSClassID jsMt19937ClassID;

// Mt19937.prototype.generate
JSValue jsMt19937Generate(JSContext *ctx, JSValueConst jsThis, int argc, JSValueConst *argv) {
  std::mt19937 *p = static_cast<std::mt19937 *>(JS_GetOpaque(jsThis, jsMt19937ClassID));
  return JS_NewBigUint64(ctx, (*p)());
}

// Mt19937.prototype
const JSCFunctionListEntry jsMt19937ProtoFuncs[] = {
  JS_CFUNC_DEF("generate", 1, jsMt19937Generate),
};

//Mt19937 constructor
JSValue jsMt19937New(JSContext *ctx, JSValueConst jsThis, int argc, JSValueConst *argv) {
  //Create an instance
  JSValue obj = JS_NewObjectClass(ctx, jsMt19937ClassID);
  bool fail = false;
  if (argc == 0) {
    JS_SetOpaque(obj, new std::mt19937());
  } else if (argc == 1) {
    // ... (If you want to set a seed value. Omitted because it is not essential)
  } else {
    fail = true;
  }
  if (fail) {
    JS_FreeValue(ctx, obj);  //Tend to forget
    return JS_EXCEPTION;
  }
  return obj;
}

//Called when the Mt19937 object is retrieved by the GC
void jsMt19937Finalizer(JSRuntime *rt, JSValue val) {
  std::mt19937 *p = static_cast<std::mt19937 *>(JS_GetOpaque(val, jsMt19937ClassID));
  delete p;
}

//Definition of Mt19937 class
//When there is a dependency between objects that are not exposed on the JS side.gc_It seems that mark also needs to be collected
JSClassDef jsMt19937Class = {
  "Mt19937",
  .finalizer = jsMt19937Finalizer,
};

void initContext(JSContext *ctx) {
  // ...
  //Initialize Mt19937 class ID
  JS_NewClassID(&jsMt19937ClassID);
  //Register class at runtime
  JS_NewClass(JS_GetRuntime(ctx), jsMt19937ClassID, &jsMt19937Class);
  //prototype setting
  JSValue mt19937Proto = JS_NewObject(ctx);
  JS_SetPropertyFunctionList(ctx, mt19937Proto, jsMt19937ProtoFuncs, std::extent_v<decltype(jsMt19937ProtoFuncs)>);
  JS_SetClassProto(ctx, jsMt19937ClassID, mt19937Proto);
  //Added Mt19937 to globalThis
  JS_SetPropertyStr(ctx, global, "Mt19937", JS_NewCFunction2(ctx, jsMt19937New, "Mt19937", 1, JS_CFUNC_constructor, 0));
  // ...
}

The liver is JS_NewClass / JS_NewObjectClass and JS_GetOpaque / JS_SetOpaque. In this way, the GC can take care of the lifetime of std :: mt19937 *.

Use the module

If you want to do ʻimport / ʻexport of a module, you need the JS_EVAL_TYPE_MODULE flag at JS_Eval. If this flag is in effect, the return value ret will be either JS_UNDEFINED or JS_EXCEPTION.

    JSValue ret = JS_Eval(ctx, argv[i], strlen(argv[i]), "<input>", JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_STRICT);

If you want to load a JS file as a module from the filesystem like the qjs command, you need to register a function like js_module_loader in quickjs-libc.c with JS_SetModuleLoaderFunc.

Define a module on the C / C ++ side

Consider the case where you want to make Mt19937 in the previous section ʻimport from the rand` module instead of giving it as a global property.

import { Mt19937 } from "rand";

This can be written as:

rand.cc


//List of functions in rand module
static const JSCFunctionListEntry randFuncs[] = {
  JS_CFUNC_SPECIAL_DEF("Mt19937", 1, constructor, jsMt19937New),  // new Mt19937()It can be so
  // JS_CFUNC_DEF("Mt19937", 1, jsMt19937New),  // Mt19937()If you want to
};

//Initialization of rand module (the one called when importing JS)
int initRand(JSContext *ctx, JSModuleDef *m) {
  JS_NewClassID(&jsMt19937ClassID);
  JS_NewClass(JS_GetRuntime(ctx), jsMt19937ClassID, &jsMt19937Class);
  //prototype setting
  JSValue mt19937Proto = JS_NewObject(ctx);
  JS_SetPropertyFunctionList(ctx, mt19937Proto, jsMt19937ProtoFuncs, std::extent_v<decltype(jsMt19937ProtoFuncs)>);
  JS_SetClassProto(ctx, jsMt19937ClassID, mt19937Proto);
  //The last argument is sizeof(randFuncs) / sizeof(JSCFunctionListEntry)Meaning
  return JS_SetModuleExportList(ctx, m, randFuncs, std::extent_v<decltype(randFuncs)>);
}

//Definition of rand module
JSModuleDef *initRandModule(JSContext *ctx, const char *moduleName) {
  JSModuleDef *m = JS_NewCModule(ctx, moduleName, initRand);
  if (!m) {
    return nullptr;
  }
  JS_AddModuleExportList(ctx, m, randFuncs, std::extent_v<decltype(randFuncs)>);
  return m;
}

void initContext(JSContext *ctx) {
  // ...
  initRandModule(ctx, "rand");
  // ...
}

int main(int argc, char const *argv[]) {
    // ...
    // JS_EVAL_TYPE_Only MODULE can be imported
    JSValue ret = JS_Eval(ctx, argv[i], strlen(argv[i]), "<input>", JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_STRICT);
    // ...
}

You can register the module in the context by creating JSCFunctionListEntry [] and doing JS_NewCModule.

Make the module a shared library and make it executable with qjs

If you name the module initialization function js_init_module and compile it as a shared library, you can ʻimport from the script executed by the qjscommand. This feature is implemented in thejs_module_loader_so function of quickjs-libc.c`.

Usage image


import { Mt19937 } from "./librand.so";

rand.cc


//Definition of rand module
extern "C" JSModuleDef *js_init_module(JSContext *ctx, const char *moduleName) {
  JSModuleDef *m = JS_NewCModule(ctx, moduleName, initRand);
  if (!m) {
    return nullptr;
  }
  JS_AddModuleExportList(ctx, m, randFuncs, std::extent_v<decltype(randFuncs)>);
  return m;
}
g++ -c -fPIC -Os -std=c++17 -Wall -I"${HOME}/.local/include/quickjs" rand.cc
#on macOS-undefined dynamic_Also add lookup
g++ -shared -Wl,-s -L"${HOME}/.local/lib/quickjs" rand.o -o librand.so

in conclusion

QuickJS is amazing because the complete JavaScript processing system is completed with only a few C source files (impression). However, there are thousands to tens of thousands of lines per file. The contents seem to be relatively easy to read, so I would like to try code reading as well.

Other lightweight languages that can be incorporated into C / C ++ are Lua, mruby, Squirrel -lang.org/) etc., but I feel that using JavaScript (QuickJS) instead is an option. For example, if you can write application configuration files and game scripts in JS, you can use eslint + prettier, or you can convert from TypeScript / JSX with tsc or babel, so it may be useful and interesting (appropriate).

After that, I need a person who can easily bind C / C ++ ⇆ JavaScript for practical use. Layers equivalent to tolua ++ or luabind in Lua (I haven't used it, but now Is sol the mainstream?). Personally, I want a Rust macro that does a good job. Currently, there is a wrapper called quick-js, but it lacks some functionality, so I'd like to implement it myself or not.

Reference information

Recommended Posts

Try the lightweight JavaScript engine “QuickJS” that can be incorporated into C / C ++
Try to save the data that can be read by JavaFX as PNG
About the matter that hidden_field can be used insanely
Four-in-a-row with gravity that can be played on the console
Problems that can easily be mistaken for Java and JavaScript