Probieren Sie die leichtgewichtige JavaScript-Engine „QuickJS“ aus, die in C / C ++ integriert werden kann

Einführung

** QuickJS ** ist eine leichte JavaScript-Engine, die in C / C ++ eingebettet werden kann. Die Lizenz ist MIT-Lizenz. Es scheint nützlich zu sein, wenn Sie JavaScript einbetten möchten, aber V8 ist zu spezifiziert.

Klicken Sie hier, um zur offiziellen Seite zu gelangen, die zu einfach im Design ist und ein Gefühl der Zuverlässigkeit aufweist.

QuickJS is a small and embeddable Javascript engine. It supports the ES2020 specification including modules, asynchronous generators, proxies and BigInt. (Übersetzung: QuickJS ist eine kleine, einbettbare JavaScript-Engine. Sie unterstützt ES2020-Spezifikationen, einschließlich Module, asynchrone Generatoren, Proxys und BigInt.)

Der QuickJS-Autor Fabrice Bellard ist übrigens auch Autor von qemu und ffmpeg. Die Inkarnation von Verhaltenskraft ... (Bild weggelassen)

Es gibt auch einen inoffiziellen Spiegel auf GitHub. Dies wird von der offiziellen Seite als "Inoffizieller Git-Spiegel" verlinkt. Zum Zeitpunkt des Schreibens des Artikels wird die neueste Version (2020-01-05) jedoch nicht berücksichtigt und bleibt die Version 2019-10-27.

Dieser Artikel beschreibt, wie QuickJS in C / C ++ integriert wird. Es ist wie mein Memorandum und nicht systematisch oder erschöpfend. Bitte verstehen Sie es im Voraus.

Betriebsüberprüfungsumgebung

Installation

Sie können es installieren, indem Sie den Tarball auspacken und wie gewohnt "make install" ausführen. Standardmäßig wird es unter "/ usr / local" installiert, kann jedoch durch Angabe von "Präfix" geändert werden. Das Folgende wird unter der Annahme geschrieben, dass es in ~ / .local installiert wird. Lesen Sie es daher entsprechend.

#Quelle herunterladen
curl -LO https://bellard.org/quickjs/quickjs-2020-01-05.tar.xz
#Erweitern Sie Tarball
tar axvf quickjs-2020-01-05.tar.xz
#Bauen~/.Beispiel für die Installation unter local
# -j (--jobs)Ist die Anzahl der parallelen Ausführungen, also entsprechend anpassen
make -C quickjs-2020-01-05 -j 2 prefix="${HOME}/.local" install

Wenn Sie den Befehl qjs verwenden möchten, übergeben Sie die Umgebungsvariable PATH entsprechend.

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

Es kann auch über AUR für Arch Linux-Benutzer und Homebrew für MacOS-Benutzer installiert werden.

Verwenden Sie den Befehl qjs / qjsc

Starten Sie REPL

Wenn Sie den Befehl qjs ohne Argumente aufrufen, wird REPL gestartet.

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

Führen Sie die JS-Datei aus

Wenn Sie dem Befehl qjs einen Dateinamen geben, wird diese Datei ausgeführt. Sie können auch "importieren" / "exportieren".

greeter.js


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

index.js


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

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

Kompilieren Sie die JS-Datei

Sie können JavaScript mit dem Befehl qjsc zu einer ausführbaren Datei machen.

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

Betten Sie QuickJS in ein C / C ++ - Programm ein

Das Hauptthema ist von hier.

Die offizielle Dokumentation für die C-API enthält nur wenige Dutzend Abschnittszeilen:

Nun, das Verhalten kann aus dem Header (quickjs.h) abgeleitet werden und es funktioniert so wie es ist. Wenn Sie danach die REPL-Quelle (qjs.c) und die Implementierung der Bibliothek selbst ( quickjs.c) lesen, können Sie verstehen, wie sie verwendet wird.

Der gesamte diesmal dargestellte Quellcode wird im folgenden Repository abgelegt.

Rufen Sie eine JS-Funktion von der C-Seite auf

Rufen Sie zunächst die in JS definierte Funktion "foo" mithilfe der C-API auf. Der Code lautet wie folgt (Dies entspricht übrigens dem Codebeispiel von Lua (Programmiersprache) - C API von 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;
}

Erstellen Sie die obige Quelle mit dem folgenden Befehl.

#kompilieren(-Optimiert für Größe mit Os)
gcc -c -Os -Wall -I"${HOME}/.local/include" simple.c
#Verknüpfung(-Wl,-Streifen mit s)
gcc -Wl,-s -L"${HOME}/.local/lib/quickjs" simple.o -l quickjs -l m -o simple

Bei der Ausführung wird das Ergebnis von "5 + 3" angezeigt.

$ ./simple
Result: 8

Dieser Code wird im folgenden Ablauf verarbeitet.

Rufen Sie eine C-Funktion von der JS-Seite auf

Ein Beispiel für die Erstellung einer Anwendung, die das als Befehlszeilenargument angegebene JavaScript ausführt und das Ergebnis in der Standardausgabe anzeigt. Das Operationsbild ist wie folgt.

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

Der Code lautet wie folgt. console.log / console.error ist in C implementiert, damit es von JS aus verwendet werden kann.

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);

  //Konsole zu globalThis hinzufügen
  JSValue console = JS_NewObject(ctx);
  JS_SetPropertyStr(ctx, global, "console", console);
  // console.Protokoll setzen
  JS_SetPropertyStr(ctx, console, "log", JS_NewCFunction(ctx, jsPrint, "log", 1));
  // console.Fehler einstellen
  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;
}

Lassen Sie JS C / C ++ - Daten verwalten

Von hier aus wird der Beispielcode plötzlich zu C ++ anstelle von C. Ich habe viel Ärger gehabt ...

Beispielsweise möchten Sie C ++ std :: mt19937 (Pseudozufallszahlengenerator) auf der JavaScript-Seite wie folgt verwenden. Überlegen.

const mt = new Mt19937();
for (let i = 0; i < 10; ++i) {
  console.log(mt.generate());  //Zufallszahl(BigInt)Ausgabe
}

Ausgabe


3499211612
581869302
3890346734
3586334585
545404204
4161255391
3922919429
949333985
2715962298
1323567403

Dies kann geschrieben werden als:

mt19937.cc


//Eindeutige ID der Klasse Mt19937 (später initialisiert)
//Es ist der Einfachheit halber global definiert, wird jedoch unterbrochen, wenn mehrere Runtimes gleichzeitig ausgeführt werden. Daher ist es erforderlich, dies entsprechend durchzuführen.
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 Konstruktor
JSValue jsMt19937New(JSContext *ctx, JSValueConst jsThis, int argc, JSValueConst *argv) {
  //Erstellen Sie eine Instanz
  JSValue obj = JS_NewObjectClass(ctx, jsMt19937ClassID);
  bool fail = false;
  if (argc == 0) {
    JS_SetOpaque(obj, new std::mt19937());
  } else if (argc == 1) {
    // ... (Wenn Sie einen Startwert festlegen möchten. Weggelassen, weil es nicht wesentlich ist)
  } else {
    fail = true;
  }
  if (fail) {
    JS_FreeValue(ctx, obj);  //Neigen Sie dazu zu vergessen
    return JS_EXCEPTION;
  }
  return obj;
}

//Wird aufgerufen, wenn das Mt19937-Objekt vom GC abgerufen wird
void jsMt19937Finalizer(JSRuntime *rt, JSValue val) {
  std::mt19937 *p = static_cast<std::mt19937 *>(JS_GetOpaque(val, jsMt19937ClassID));
  delete p;
}

//Definition der Klasse Mt19937
//Wenn es eine Abhängigkeit zwischen Objekten gibt, die auf der JS-Seite nicht verfügbar sind.gc_Es scheint, dass die Marke auch gesammelt werden muss
JSClassDef jsMt19937Class = {
  "Mt19937",
  .finalizer = jsMt19937Finalizer,
};

void initContext(JSContext *ctx) {
  // ...
  //Initialisieren Sie die Klassen-ID Mt19937
  JS_NewClassID(&jsMt19937ClassID);
  //Klasse zur Laufzeit registrieren
  JS_NewClass(JS_GetRuntime(ctx), jsMt19937ClassID, &jsMt19937Class);
  //Prototypeinstellung
  JSValue mt19937Proto = JS_NewObject(ctx);
  JS_SetPropertyFunctionList(ctx, mt19937Proto, jsMt19937ProtoFuncs, std::extent_v<decltype(jsMt19937ProtoFuncs)>);
  JS_SetClassProto(ctx, jsMt19937ClassID, mt19937Proto);
  //Mt19937 zu globalThis hinzugefügt
  JS_SetPropertyStr(ctx, global, "Mt19937", JS_NewCFunction2(ctx, jsMt19937New, "Mt19937", 1, JS_CFUNC_constructor, 0));
  // ...
}

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

import { Mt19937 } from "rand";

Wenn Sie ein Modul, das ein Modul verwendet, importieren / exportieren möchten, benötigen Sie zum Zeitpunkt von JS_Eval das Flag JS_EVAL_TYPE_MODULE. Wenn dieses Flag aktiviert ist, lautet der Rückgabewert "ret" entweder "JS_UNDEFINED" oder "JS_EXCEPTION". Wenn Sie eine JS-Datei wie den Befehl qjs als Modul aus dem Dateisystem laden möchten, müssen Sie eine Funktion wie js_module_loader in quickjs-libc.c mit JS_SetModuleLoaderFunc registrieren. Stellen Sie sich den Fall vor, in dem Sie aus dem Rand-Modul "importieren" möchten, anstatt es als globale Eigenschaft für "Mt19937" im vorherigen Abschnitt anzugeben, in dem das Modul auf der C / C ++ - Seite definiert ist. Dies kann geschrieben werden als:

rand.cc


//Liste der Funktionen im Rand-Modul
static const JSCFunctionListEntry randFuncs[] = {
  JS_CFUNC_SPECIAL_DEF("Mt19937", 1, constructor, jsMt19937New),  // new Mt19937()Es kann so sein
  // JS_CFUNC_DEF("Mt19937", 1, jsMt19937New),  // Mt19937()Wenn du möchtest
};

//Initialisierung des Rand-Moduls (das beim Importieren von JS aufgerufene)
int initRand(JSContext *ctx, JSModuleDef *m) {
  JS_NewClassID(&jsMt19937ClassID);
  JS_NewClass(JS_GetRuntime(ctx), jsMt19937ClassID, &jsMt19937Class);
  //Prototypeinstellung
  JSValue mt19937Proto = JS_NewObject(ctx);
  JS_SetPropertyFunctionList(ctx, mt19937Proto, jsMt19937ProtoFuncs, std::extent_v<decltype(jsMt19937ProtoFuncs)>);
  JS_SetClassProto(ctx, jsMt19937ClassID, mt19937Proto);
  //Das letzte Argument ist sizeof(randFuncs) / sizeof(JSCFunctionListEntry)Bedeutung
  return JS_SetModuleExportList(ctx, m, randFuncs, std::extent_v<decltype(randFuncs)>);
}

//Definition des Rand-Moduls
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_Es kann nur MODUL importiert werden
    JSValue ret = JS_Eval(ctx, argv[i], strlen(argv[i]), "<input>", JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_STRICT);
    // ...
}

Sie können das Modul im Kontext registrieren, indem Sie "JSCFunctionListEntry []" erstellen und "JS_NewCModule" ausführen.

Machen Sie das Modul zu einer gemeinsam genutzten Bibliothek und machen Sie es mit "qjs" ausführbar

Wenn Sie die Modulinitialisierungsfunktion "js_init_module" benennen und als gemeinsam genutzte Bibliothek kompilieren, können Sie sie aus dem mit dem Befehl "qjs" ausgeführten Skript "importieren". Diese Funktion ist in der Funktion js_module_loader_so von quickjs-libc.c implementiert.

Nutzungsbild


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

rand.cc


//Definition des Rand-Moduls
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
#unter macOS-undefined dynamic_Fügen Sie auch Lookup hinzu
g++ -shared -Wl,-s -L"${HOME}/.local/lib/quickjs" rand.o -o librand.so

abschließend

QuickJS ist erstaunlich, da das gesamte JavaScript-Verarbeitungssystem mit nur wenigen C-Quelldateien (Impression) abgeschlossen ist. Es gibt jedoch Tausende bis Zehntausende von Zeilen pro Datei. Der Inhalt scheint relativ einfach zu lesen zu sein, daher möchte ich auch versuchen, Code zu lesen.

Andere leichtgewichtige Sprachen, die in C / C ++ integriert werden können, sind Lua, mruby, [Squirrel](http: // squirrel] -lang.org/) usw., aber ich denke, dass die Verwendung von JavaScript (QuickJS) stattdessen eine Option ist. Wenn Sie beispielsweise Anwendungseinstellungsdateien und Spielskripte in JS schreiben können, können Sie eslint + prettier verwenden oder von TypeScript / JSX mit tsc oder babel konvertieren, sodass dies nützlich und interessant (angemessen) sein kann.

Danach brauche ich eine Person, die C / C ++ ⇆ JavaScript für den praktischen Gebrauch leicht binden kann. Ebenen, die tolua ++ oder luabind in Lua entsprechen (ich habe es aber jetzt nicht verwendet) Ist sol der Mainstream?). Persönlich möchte ich einen Mann, der gute Arbeit mit Rusts Makros leistet. Derzeit gibt es einen Wrapper namens "quick-js", dem jedoch einige Funktionen fehlen. Daher möchte ich ihn selbst implementieren oder nicht.

Referenzinformationen

Recommended Posts

Probieren Sie die leichtgewichtige JavaScript-Engine „QuickJS“ aus, die in C / C ++ integriert werden kann
Speichern Sie die Daten, die von JavaFX gelesen werden können, als PNG
Über die Sache, dass hidden_field wahnsinnig benutzt werden kann
Vier Reihen mit Schwerkraft, die auf der Konsole gespielt werden können
Probleme, die leicht mit Java und JavaScript verwechselt werden können