Essayez le moteur JavaScript léger «QuickJS» qui peut être intégré dans C / C ++

introduction

** QuickJS ** est un moteur JavaScript léger qui peut être intégré en C / C ++. La licence est une licence MIT. Cela semble utile lorsque vous souhaitez intégrer du JavaScript, mais que V8 est trop surpuissant.

Cliquez ici pour la page officielle, qui est trop simple dans la conception et a un sens de la fiabilité.

QuickJS is a small and embeddable Javascript engine. It supports the ES2020 specification including modules, asynchronous generators, proxies and BigInt. (Traduction: QuickJS est un petit moteur JavaScript intégrable. Il prend en charge les spécifications ES2020, y compris les modules, les générateurs asynchrones, les proxies et BigInt.)

À propos, l'auteur de QuickJS Fabrice Bellard est également l'auteur de qemu et ffmpeg. L'incarnation du pouvoir comportemental ... (Image omise)

Il existe également un miroir non officiel sur GitHub. Ceci est lié à partir de la page officielle en tant que «miroir git non officiel». Cependant, au moment de la rédaction de l'article, la dernière version (05/01/2020) n'est pas reflétée et reste la version 2019-10-27.

Cet article explique comment incorporer QuickJS dans C / C ++. C'est comme mon mémorandum, pas systématique et exhaustif, alors veuillez comprendre à l'avance.

Environnement de vérification de fonctionnement

Installation

Vous pouvez décompresser l'archive tar et l'installer comme d'habitude avec make install. Par défaut, il est installé sous / usr / local, mais il peut être modifié en spécifiant prefix. Ce qui suit est écrit en supposant qu'il sera installé dans ~ / .local, alors lisez-le comme il convient.

#Télécharger la source
curl -LO https://bellard.org/quickjs/quickjs-2020-01-05.tar.xz
#Développer l'archive tar
tar axvf quickjs-2020-01-05.tar.xz
#Construire~/.Exemple d'installation sous local
# -j (--jobs)Est le nombre d'exécutions parallèles, donc ajustez en conséquence
make -C quickjs-2020-01-05 -j 2 prefix="${HOME}/.local" install

Si vous souhaitez utiliser la commande qjs, transmettez la variable d'environnement PATH de manière appropriée.

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

Il peut également être installé via AUR pour les utilisateurs d'Arch Linux et Homebrew pour les utilisateurs de macOS.

Utilisez la commande qjs / qjsc

Démarrer REPL

L'appel de la commande qjs sans argument lance 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 >

Exécutez le fichier JS

Si vous donnez un nom de fichier à la commande qjs, ce fichier sera exécuté. Vous pouvez également ʻ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!

Compilez le fichier JS

Vous pouvez faire de JavaScript un fichier exécutable en utilisant la commande qjsc.

$ 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!

Incorporer QuickJS dans un programme C / C ++

Le sujet principal est d'ici.

La documentation officielle de l'API C ne comporte que quelques dizaines de lignes de sections:

Eh bien, le comportement peut être déduit de l'en-tête (quickjs.h), et cela fonctionne tel quel. Après cela, si vous lisez la source REPL (qjs.c) et l'implémentation de la bibliothèque elle-même ( quickjs.c), vous pouvez comprendre comment l'utiliser.

L'ensemble du code source illustré cette fois est placé dans le référentiel suivant.

Appeler une fonction JS du côté C

Pour commencer, appelons la fonction foo définie dans JS à l'aide de l'API C. Le code est le suivant (En passant, cela correspond à l'exemple de code de Lua (langage de programmation) --C API de 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;
}

Construisez la source ci-dessus avec la commande suivante.

#compiler(-Optimisé pour la taille avec Os)
gcc -c -Os -Wall -I"${HOME}/.local/include" simple.c
#Lien(-Wl,-bande avec s)
gcc -Wl,-s -L"${HOME}/.local/lib/quickjs" simple.o -l quickjs -l m -o simple

Une fois exécuté, le résultat de «5 + 3» s'affiche.

$ ./simple
Result: 8

Ce code est traité dans le flux suivant.

Appeler une fonction C du côté JS

Un exemple de création d'une application qui exécute le JavaScript donné comme argument de ligne de commande et affiche le résultat dans la sortie standard. L'image de l'opération est la suivante.

$ ./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

Le code est comme suit. console.log / console.error est implémenté en C pour pouvoir être utilisé à partir de 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);

  //Ajouter une console à globalThis
  JSValue console = JS_NewObject(ctx);
  JS_SetPropertyStr(ctx, global, "console", console);
  // console.définir le journal
  JS_SetPropertyStr(ctx, console, "log", JS_NewCFunction(ctx, jsPrint, "log", 1));
  // console.Définir l'erreur
  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;
}

Laisser JS gérer les données C / C ++

À partir de là, l'exemple de code devient soudainement C ++ au lieu de C. J'ai eu beaucoup de mal ...

Par exemple, vous souhaitez utiliser C ++ std :: mt19937 (générateur de nombres pseudo aléatoires) du côté JavaScript comme suit. Pense.

const mt = new Mt19937();
for (let i = 0; i < 10; ++i) {
  console.log(mt.generate());  //nombre aléatoire(BigInt)Pour sortir
}

production


3499211612
581869302
3890346734
3586334585
545404204
4161255391
3922919429
949333985
2715962298
1323567403

Cela peut être écrit comme suit:

mt19937.cc


//ID unique de la classe Mt19937 (initialisé ultérieurement)
//Il est défini globalement pour des raisons de simplicité, mais il se cassera lorsque plusieurs Runtimes sont exécutés en même temps, il est donc nécessaire de le faire de manière appropriée.
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),
};

//Constructeur MT19937
JSValue jsMt19937New(JSContext *ctx, JSValueConst jsThis, int argc, JSValueConst *argv) {
  //Créer une instance
  JSValue obj = JS_NewObjectClass(ctx, jsMt19937ClassID);
  bool fail = false;
  if (argc == 0) {
    JS_SetOpaque(obj, new std::mt19937());
  } else if (argc == 1) {
    // ... (Si vous souhaitez définir une valeur de départ. Omis car ce n'est pas essentiel)
  } else {
    fail = true;
  }
  if (fail) {
    JS_FreeValue(ctx, obj);  //J'ai tendance à oublier
    return JS_EXCEPTION;
  }
  return obj;
}

//Appelé lorsque l'objet Mt19937 est récupéré par le GC
void jsMt19937Finalizer(JSRuntime *rt, JSValue val) {
  std::mt19937 *p = static_cast<std::mt19937 *>(JS_GetOpaque(val, jsMt19937ClassID));
  delete p;
}

//Définition de la classe Mt19937
//Lorsqu'il existe une dépendance entre des objets qui ne sont pas exposés côté JS.gc_Il semble que la marque doit également être collectée
JSClassDef jsMt19937Class = {
  "Mt19937",
  .finalizer = jsMt19937Finalizer,
};

void initContext(JSContext *ctx) {
  // ...
  //Initialiser l'ID de classe Mt19937
  JS_NewClassID(&jsMt19937ClassID);
  //Enregistrer la classe au moment de l'exécution
  JS_NewClass(JS_GetRuntime(ctx), jsMt19937ClassID, &jsMt19937Class);
  //réglage du prototype
  JSValue mt19937Proto = JS_NewObject(ctx);
  JS_SetPropertyFunctionList(ctx, mt19937Proto, jsMt19937ProtoFuncs, std::extent_v<decltype(jsMt19937ProtoFuncs)>);
  JS_SetClassProto(ctx, jsMt19937ClassID, mt19937Proto);
  //Ajouté Mt19937 à globalThis
  JS_SetPropertyStr(ctx, global, "Mt19937", JS_NewCFunction2(ctx, jsMt19937New, "Mt19937", 1, JS_CFUNC_constructor, 0));
  // ...
}

Le cœur est «JS_NewClass» / «JS_NewObjectClass» et «JS_GetOpaque» / «JS_SetOpaque». De cette façon, il est possible de laisser le GC gérer la durée de vie de std :: mt19937 *.

Utilisez le module

Si vous voulez faire ʻimport / ʻexport d'un module, vous avez besoin de l'indicateur JS_EVAL_TYPE_MODULE au moment de JS_Eval. Si cet indicateur est activé, la valeur de retour «ret» sera soit «JS_UNDEFINED» ou «JS_EXCEPTION».

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

Si vous voulez charger un fichier JS en tant que module du système de fichiers comme la commande qjs, vous devez enregistrer une fonction comme js_module_loader dans quickjs-libc.c avec JS_SetModuleLoaderFunc.

Définir un module côté C / C ++

Considérez le cas où vous voulez que le Mt19937 dans la section précédente soit ʻimport du module rand` au lieu d'être donné comme une propriété globale.

import { Mt19937 } from "rand";

Cela peut être écrit comme suit:

rand.cc


//Liste des fonctions du module rand
static const JSCFunctionListEntry randFuncs[] = {
  JS_CFUNC_SPECIAL_DEF("Mt19937", 1, constructor, jsMt19937New),  // new Mt19937()Ça peut être ainsi
  // JS_CFUNC_DEF("Mt19937", 1, jsMt19937New),  // Mt19937()Si tu veux
};

//Initialisation du module rand (celui appelé lors de l'importation de JS)
int initRand(JSContext *ctx, JSModuleDef *m) {
  JS_NewClassID(&jsMt19937ClassID);
  JS_NewClass(JS_GetRuntime(ctx), jsMt19937ClassID, &jsMt19937Class);
  //réglage du prototype
  JSValue mt19937Proto = JS_NewObject(ctx);
  JS_SetPropertyFunctionList(ctx, mt19937Proto, jsMt19937ProtoFuncs, std::extent_v<decltype(jsMt19937ProtoFuncs)>);
  JS_SetClassProto(ctx, jsMt19937ClassID, mt19937Proto);
  //Le dernier argument est sizeof(randFuncs) / sizeof(JSCFunctionListEntry)Sens
  return JS_SetModuleExportList(ctx, m, randFuncs, std::extent_v<decltype(randFuncs)>);
}

//Définition du module rand
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_Seul le MODULE peut être importé
    JSValue ret = JS_Eval(ctx, argv[i], strlen(argv[i]), "<input>", JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_STRICT);
    // ...
}

Vous pouvez enregistrer le module dans le contexte en créant JSCFunctionListEntry [] et en faisant JS_NewCModule.

Faites du module une bibliothèque partagée et rendez-le exécutable avec qjs

Si vous nommez la fonction d'initialisation du module js_init_module et la compilez comme une bibliothèque partagée, vous pouvez ʻimport depuis le script exécuté par la commande qjs. Cette fonctionnalité est implémentée dans la fonction js_module_loader_so de quickjs-libc.c`.

Image d'utilisation


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

rand.cc


//Définition du module rand
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
#sur macOS-undefined dynamic_Ajouter également une recherche
g++ -shared -Wl,-s -L"${HOME}/.local/lib/quickjs" rand.o -o librand.so

en conclusion

QuickJS est incroyable car le système de traitement JavaScript complet est complété avec seulement quelques fichiers source C (impression). Cependant, il y a des milliers à des dizaines de milliers de lignes par fichier. Le contenu semble être relativement facile à lire, alors j'aimerais également essayer la lecture de code.

Les autres langages légers pouvant être incorporés dans C / C ++ sont Lua, mruby, [Squirrel](http: // squirrel] -lang.org/) etc., mais je pense que l'utilisation de JavaScript (QuickJS) à la place est une option. Par exemple, si vous pouvez écrire des fichiers de paramètres d'application et des scripts de jeu dans JS, vous pouvez utiliser eslint + joli, ou vous pouvez convertir de TypeScript / JSX avec tsc ou babel, donc cela peut être utile et intéressant (approprié).

Après cela, j'ai besoin d'une personne capable de lier facilement C / C ++ ⇆ JavaScript pour une utilisation pratique. Couches équivalentes à tolua ++ et luabind dans Lua (je ne l'ai pas utilisé, mais maintenant Est-ce que sol est le courant dominant?). Personnellement, je veux un gars qui fait du bon travail avec les macros de Rust. Actuellement, il existe un wrapper appelé quick-js, mais il manque certaines fonctionnalités, donc j'aimerais l'implémenter moi-même ou non.

Informations de référence

Recommended Posts

Essayez le moteur JavaScript léger «QuickJS» qui peut être intégré dans C / C ++
Essayez d'enregistrer les données pouvant être lues par JavaFX au format PNG
À propos du problème que hidden_field peut être utilisé de manière insensée
Quatre rangées avec gravité pouvant être jouées sur la console
Problèmes facilement confondus avec Java et JavaScript