7 Punkte von GN⁺ 2025-12-24 | 2 Kommentare | Auf WhatsApp teilen
  • MicroQuickJS (MQuickJS) ist eine ultraleichte JavaScript-Engine für Embedded-Systeme und läuft mit nur etwa 10 kB RAM und 100 kB ROM
  • Um bei ähnlicher Geschwindigkeit wie QuickJS den Speicherverbrauch zu senken, verwendet sie einen Tracing Garbage Collector und UTF-8 als String-Speicherformat
  • Unterstützt wird eine eingeschränkte JavaScript-Teilmenge nahe ES5; zugelassen ist nur der strict mode, der fehleranfällige Syntax verbietet
  • Mit dem REPL-Tool mqjs lassen sich Skripte ausführen, Bytecode speichern und Speicherlimits setzen; erzeugter Bytecode kann direkt aus dem ROM ausgeführt werden
  • Die gesamte Engine und die Standardbibliothek liegen im ROM und ermöglichen schnelle Initialisierung bei geringem Speicherverbrauch, was die Effizienz der JavaScript-Ausführung in Embedded-Umgebungen erhöht

Einführung

  • MicroQuickJS (MQuickJS) ist eine JavaScript-Engine für Embedded-Systeme und läuft mit 10 kB RAM und 100 kB ROM (einschließlich ARM-Thumb-2-Code)
    • Die Geschwindigkeit ist ähnlich wie bei QuickJS
  • Es wird nur eine Teilmenge nahe ES5 unterstützt; die Engine arbeitet ausschließlich im strict mode, der ineffiziente oder fehleranfällige Syntax verbietet
  • Zwar teilt sie einen Teil des Codes mit QuickJS, die interne Struktur wurde aber vollständig anders entworfen, um Speicher zu sparen
    • Verwendet werden ein Tracing Garbage Collector, keine Nutzung des CPU-Stacks und UTF-8-Stringspeicherung

REPL

  • Das REPL-Kommando lautet mqjs und unterstützt Skriptausführung, Auswertung, interaktiven Modus, Setzen von Speicherlimits und Speichern von Bytecode
    • Beispiel: ./mqjs --memory-limit 10k tests/mandelbrot.js
  • Mit der Option -o kann kompilierter Bytecode in einer Datei gespeichert werden
    • Gespeicherter Bytecode kann mit ./mqjs mandelbrot.bin ausgeführt werden
  • Bytecode hängt von der Endianness und Wortlänge (32/64 Bit) der CPU ab; mit der Option -m32 lässt sich Bytecode für 32 Bit erzeugen
  • Mit der Option --no-column lassen sich Spaltennummern in Debuginformationen entfernen

Strict Mode

  • Es ist nur der strict mode erlaubt; das Schlüsselwort with kann nicht verwendet werden, und globale Variablen müssen zwingend mit var deklariert werden
  • Arrays mit Lücken (holes) sind nicht erlaubt
    • Beispiel: a[10] = 2 löst einen TypeError aus
    • Wenn ein Array mit Lücken benötigt wird, soll stattdessen ein normales Objekt verwendet werden
  • Nur globales eval wird unterstützt, kein Zugriff auf lokale Variablen
  • Value Boxing wird nicht unterstützt (new Number(1) usw.)

JavaScript-Teilmenge

  • Basierend auf strict mode, mit Fokus auf ES5-Kompatibilität
  • Array-Objekte enthalten keine Lücken; Zugriffe auf Indizes außerhalb des Bereichs führen zu Fehlern
  • for in iteriert nur über eigene Eigenschaften eines Objekts, for of wird nur für Arrays unterstützt
  • Ein globales Objekt existiert, aber ohne Getter/Setter; direkt angelegte Eigenschaften werden nicht als globale Variablen sichtbar
  • Reguläre Ausdrücke (Regexp) behandeln Groß-/Kleinschreibung nur für ASCII; /./ matched Unicode-Codepoints statt UTF-16-Einheiten
  • String-Funktionen verarbeiten nur ASCII (toLowerCase, toUpperCase)
  • Date unterstützt nur Date.now()
  • Zusätzlich unterstützt:
    • for of, Typed Arrays, String-Literale mit \u{hex}
    • Math-Funktionen: imul, clz32, fround, trunc, log2, log10
    • Exponentiationsoperator, Regexp-Flags (s, y, u), String-Funktionen (replaceAll, trimStart, trimEnd), globalThis

C-API

  • Minimale Abhängigkeit von der C-Bibliothek; malloc, free, printf werden nicht verwendet
  • Ein Speicherpuffer muss direkt bereitgestellt werden; die Engine allokiert Speicher ausschließlich innerhalb dieses Puffers
    • Beispiel: ctx = JS_NewContext(mem_buf, sizeof(mem_buf), &js_stdlib)
  • Durch die Garbage-Collection-Methode ist kein Aufruf von JS_FreeValue() nötig
  • Objektadressen können sich bei jeder Allokation verschieben; daher wird die Verwendung von JSValue-Zeigern empfohlen
    • Sichere Referenzverwaltung mit JS_PushGCRef() / JS_PopGCRef()
  • Die Standardbibliothek wird als im ROM speicherbare C-Struktur kompiliert und ermöglicht schnelle Initialisierung bei geringem RAM-Verbrauch
  • Bytecode-Ausführung ist aus dem ROM möglich, nach Relokation mit JS_RelocateBytecode() und Ausführung über JS_LoadBytecode() und JS_Run()
  • Mathematikbibliothek (libm.c) und Fließkomma-Emulator sind integriert

Interne Struktur und Vergleich mit QuickJS

  • Garbage Collection: Statt Referenzzählung wird ein Tracing- und komprimierender GC verwendet
    • Verhindert Speicherfragmentierung und verkleinert Objekte
  • Wertdarstellung: Auf die CPU-Wortgröße (32/64 Bit) abgestimmt
    • Kann 31-Bit-Integer, Unicode-Codepoints, Fließkommazahlen und Zeiger auf Speicherblöcke speichern
  • Strings werden in UTF-8 gespeichert und sind effizienter als QuickJS' 8/16-Bit-Array-Ansatz
  • C-Funktionen können als Einzelwert gespeichert werden; zusätzliche Eigenschaften sind nicht möglich
  • Die Standardbibliothek liegt im ROM; durch minimale RAM-Objekte ist eine schnelle Engine-Initialisierung möglich
  • Bytecode ist stackbasiert und wird über eine indirekte Referenztabelle schreibgeschützt behandelt
    • Golomb-Code komprimiert Zeilen- und Spaltennummern
  • Der Compiler ist QuickJS ähnlich, verwendet aber einen nichtrekursiven Parser, um den C-Stack-Verbrauch zu begrenzen
    • Bytecode-Erzeugung in einem einzigen Durchlauf ohne Parse-Tree

Tests und Benchmarks

  • Grundlegende Tests: make test
  • QuickJS-Mikrobenchmarks: make microbench
  • Der Octane-Benchmark (angepasste Version für strict mode) kann separat heruntergeladen werden
    • Ausführung: make octane

Lizenz

  • Veröffentlicht unter der MIT-Lizenz
  • Das Urheberrecht am Quellcode liegt bei Fabrice Bellard und Charlie Gordon

2 Kommentare

 
GN⁺ 2025-12-24
Hacker-News-Kommentare
  • Hätte es so etwas 2010 gegeben, wäre die Skriptsprache von Redis vermutlich nicht Lua, sondern JavaScript geworden.
    Lua wurde nicht aus sprachlichen Gründen gewählt, sondern wegen Implementierungsbeschränkungen: klein, schnell und auf ANSI-C basierend.
    Einige Ideen von Lua sind gut, aber persönlich fand ich die Abkehr von der Algol-artigen Syntax unnötig.
    Bei SmallTalk oder FORTH lohnt sich die Verwirrung als Preis dafür, neue Abstraktionskonzepte zu lernen, aber bei Lua sehe ich keinen vergleichbaren Grund für diese Änderungen.

    • Die Syntax von Lua gefällt mir zwar nicht besonders, aber ich finde die Gründe der Entwickler für die Wahl durchaus nachvollziehbar.
      Lua ist die einzige leichtgewichtige Sprache mit Tail-Call-Optimierung (TCO), wodurch man Programme rein rekursiv und ohne Schleifen schreiben kann.
      JavaScript hat diese Optimierung nicht, daher geht das dort nicht auf dieselbe Weise.
      Lua eignet sich auch besonders gut für das Schreiben von Compilern, weil dort viele rekursive Strukturen vorkommen.
      Für Redis-Skripting könnte JS besser passen, aber es ist schade, Lua deshalb abzuwerten.
    • Wenn man bedenkt, dass Lua erstmals 1993 erschienen ist, war die Syntax für die damalige Zeit ziemlich traditionell.
      In Brasilien waren Pascal und Ada weiter verbreitet als C, daher kommt dieser Einfluss.
      Ruby und Perl erschienen in einer ähnlichen Zeit, versuchten aber deutlich radikalere Syntaxänderungen.
    • Ich wollte erst schreiben, dass ich Lua mit 13 leicht gelernt habe, hielt dann aber inne, als mir klar wurde, dass der Kommentator tatsächlich antirez selbst ist.
    • Das löst zwar keine Syntaxprobleme, aber das Konzept von „language skins“ ist interessant.
      Parser und Lexer werden getrennt gehalten, trotzdem gab es kaum Versuche, einfach Tokens wie {} gegen then/end auszutauschen.
      Verwandte Diskussionen: HN-Thread, Reddit-Diskussion
    • Ich frage mich, ob als Skriptsprache für Redis vielleicht auch Tcl in Betracht gezogen wurde, schließlich ist das die ursprüngliche Embedded-Sprache.
  • Diese Engine beschränkt JS auf eine Weise, die ich mir früher bei der Arbeit an JSC gewünscht hätte.
    Im Web sind solche Einschränkungen wegen der Kompatibilität unmöglich, aber in Embedded-Umgebungen kann genau das ein erfreuliches Design sein.

    • Ich habe bereits eine JS-Engine ohne solche Beschränkungen.
    • Ich frage mich, was aus der Multithreading-Arbeit in JSC geworden ist. Wurde sie nach deinem Weggang von Apple eingestellt, oder existiert der Code noch?
  • Ich habe einen Playground gebaut, in dem man MicroQuickJS direkt im Browser ausprobieren kann.
    MicroQuickJS-WebAssembly-Version
    Zur Referenz gibt es auch die ursprüngliche QuickJS-Version.
    QuickJS ist 2,28 MB groß, MicroQuickJS dagegen nur 303 KB und damit deutlich leichter.

    • Wahrscheinlich ist es größer, weil im Build Namensinformationen und Ähnliches enthalten sind.
      Mit der Option emcc -O3 oder zusätzlich --closure 1 könnte man es vermutlich noch weiter verkleinern.
      QuickJS ist bereits optimiert, und nur bei MicroQuickJS gibt es noch Spielraum.
  • Wie Jeff Atwood berühmt sagte: „Jede Anwendung, die in JavaScript geschrieben werden kann, wird irgendwann in JavaScript geschrieben.“
    Das scheint jetzt sogar für Embedded-Systeme zu gelten.
    Jeff-Atwood-Wiki

    • Stimme zu. Passender Vortrag: The Birth and Death of JavaScript
    • Fabrice Bellard hat auch schon einmal eine JS-basierte VM gebaut, die Linux im Browser ausführen kann.
      JSLinux-Link
    • Das fühlt sich fast wie Rule 35 des Internets an.
  • Schade, dass es ohne Commit-Historie hochgeladen wurde.
    Ich hätte gern gesehen, wie schnell jemand auf diesem Niveau so ein Projekt fertigstellt.
    Da es ohnehin auf QuickJS basiert, wäre ein Vergleich vermutlich aber nur begrenzt aussagekräftig.

    • Die Formulierung „public repository of…“ deutet darauf hin, dass die eigentliche Arbeitshistorie in einem privaten Repository liegen könnte.
    • Vielleicht wurde es aber auch einfach in einem Rutsch fertiggestellt.
  • Ich frage mich, ob das die leichtgewichtigste Methode sein könnte, um die YouTube-JS-Challenge von yt-dlp zu lösen.
    Siehe dazu die yt-dlp-EJS-Dokumentation.
    QuickJS wird bereits unterstützt.

    • Eher unwahrscheinlich, weil nur eine teilweise Implementierung auf ES5-Niveau unterstützt wird.
      Die JS-Puzzles von YouTube sind so komplex, dass sogar ein in Python geschriebener JS-Emulator aufgegeben wurde.
    • Da bisher nur ES5 implementiert ist, halte ich das praktisch für schwierig.
  • Ich kenne mich mit Embedded-Systemen nicht gut aus, aber ich frage mich, ob so eine Engine ESP32 oder Arduino in JavaScript programmierbar machen könnte.
    So ähnlich wie MicroPython.

    • Es gibt bereits ähnliche Projekte.
    • Die XS-Engine von Moddable/Kinoma unterstützt ES6 und mehr.
      MicroQuickJS implementiert nur Teile von ES5 und bietet keine Umgebungs-Bindings.
    • Früher gab es ein JS-Programmierboard namens Tessel.
      Dort wurde JS-Code in Lua-VM-Bytecode umgewandelt und dann ausgeführt, was ein ziemlich cleverer Ansatz war.
      Vor Kurzem habe ich sogar das alte Node-0.8-CLI in Rust neu geschrieben, aber am Ende wanderte die Hardware doch wieder in die Schublade.
    • Entscheidend ist, dass die Architektur ohne malloc() auskommt. Das eröffnet Möglichkeiten.
  • Timing ist wirklich entscheidend. Als es gestern Abend gepostet wurde, gab es überhaupt keine Reaktion.

    • Wahrscheinlich einfach Pech.
    • Jemand anderes hat es auch versucht, aber ebenfalls keine Reaktion bekommen.
      Es gibt die Strategie, es morgens in den USA erneut zu posten oder es in regelmäßigen Abständen wieder einzureichen.
  • Fabrice Bellard ist einer der produktivsten und vielseitigsten Programmierer, die heute leben.
    Bekannte Werke: FFmpeg, QEMU, JSLinux, TCC, QuickJS
    Eine absolute Legende.

    • So sehr er auch geschätzt wird, interessieren sich vergleichsweise wenige Menschen für seine Arbeitsweise bei der Entwicklung.
      Beeindruckend ist dieser Ansatz, mit minimalen Abhängigkeiten und Werkzeugen vollständige Programme zu bauen.
    • Inzwischen denke ich fast, dass er nicht eine einzelne Person ist, sondern der Codename eines Teams erfahrener Hacker.
      Wenn es ein echter Mensch ist, muss er schließlich auch irgendwann schlafen.
    • Er entwickelt auch selbst eine LLM-Inferenz-Engine und pflegt sie schon seit den GPT-2-Tagen.
      ts_server, TextSynth
    • Interessant ist, dass die meisten seiner Programme keine GUI-zentrierten Benutzeroberflächen behandeln.
      Es wirkt so, als bevorzuge er Programme, bei denen der Nutzer Parameter setzt und das Programm dann in sich geschlossen läuft.
    • Er hat außerdem dreimal den International Obfuscated C Code Contest (IOCCC) gewonnen.
      Liste der IOCCC-Preisträger
  • Beeindruckend ist, dass man „JS sogar mit nur 10 kB RAM kompilieren und ausführen kann“.
    Das ist perfektes Timing in einer Phase, in der RAM wieder teurer wird.
    Ich frage mich, ob man das in Chromium oder Electron einbauen könnte.

    • Wegen der Web-Kompatibilität dürfte das schwierig sein, aber selbst dann wäre der Speicherspareffekt in Chromium vermutlich nicht besonders groß.
 
xguru 2025-12-24

Für eine Vorstellung von Fabrice Bellard siehe bitte meinen früheren Kommentar. Was für ein erstaunliches Monster, und dabei so beständig..
https://de.news.hada.io/topic?id=59#cid51