- MicroQuickJS (MQuickJS) es un motor de JavaScript ultraligero diseñado para sistemas embebidos, capaz de ejecutarse con apenas 10 kB de RAM y 100 kB de ROM
- Mantiene una velocidad similar a QuickJS mientras reduce el uso de memoria mediante un recolector de basura por trazado y almacenamiento de cadenas en UTF-8
- El lenguaje compatible es un subconjunto limitado de JavaScript cercano a ES5, y solo permite modo estricto (
strict mode), que prohíbe sintaxis propensa a errores
- La herramienta REPL
mqjs permite ejecutar scripts, guardar bytecode y configurar límites de memoria; el bytecode generado puede ejecutarse directamente desde ROM
- Todo el motor y la biblioteca estándar residen en ROM, logrando inicialización rápida y bajo consumo de memoria, y mejorando la eficiencia de ejecución de JavaScript en entornos embebidos
Introducción
- MicroQuickJS (MQuickJS) es un motor de JavaScript orientado a sistemas embebidos, que funciona con 10 kB de RAM y 100 kB de ROM (incluyendo código ARM Thumb-2)
- Su velocidad es similar a la de QuickJS
- Solo admite un subconjunto cercano a ES5 y funciona exclusivamente en modo estricto (
strict mode), que prohíbe sintaxis ineficiente o propensa a errores
- Comparte parte del código con QuickJS, pero su estructura interna fue diseñada de forma completamente distinta para ahorrar memoria
- Usa recolector de basura por trazado, sin uso de la pila de CPU y almacenamiento de cadenas en UTF-8
REPL
- El comando REPL es
mqjs y admite ejecución de scripts, evaluación, modo interactivo, configuración de límites de memoria y guardado de bytecode
- Ejemplo:
./mqjs --memory-limit 10k tests/mandelbrot.js
- Con la opción
-o se puede guardar el bytecode compilado en un archivo
- El bytecode guardado puede ejecutarse con
./mqjs mandelbrot.bin
- El bytecode varía según el endianness del CPU y la longitud de palabra (32/64 bits); con la opción
-m32 se puede generar bytecode para 32 bits
- La opción
--no-column permite eliminar los números de columna de la información de depuración
Modo estricto
- Solo se permite
strict mode; no se puede usar la palabra clave with, y las variables globales deben declararse obligatoriamente con var
- No se permiten huecos (
hole) en arreglos
- Ejemplo:
a[10] = 2 produce un TypeError
- Si se necesita un arreglo con huecos, debe usarse un objeto normal
- Solo se admite
eval global, sin acceso a variables locales
- No se admite value boxing (
new Number(1), etc.)
Subconjunto de JavaScript
- Basado en
strict mode, con enfoque en la compatibilidad con ES5
- El objeto Array no tiene huecos, y el acceso a índices fuera de rango genera error
for in recorre solo las propiedades propias del objeto, y for of solo es compatible con arreglos
- El objeto global existe, pero no admite getter/setter, y las propiedades creadas directamente no se exponen como variables globales
- Las expresiones regulares (Regexp) solo distinguen mayúsculas y minúsculas en ASCII, y
/./ hace coincidencia por punto de código Unicode en lugar de UTF-16
- Las funciones de cadena solo procesan ASCII (
toLowerCase, toUpperCase)
- Date solo admite
Date.now()
- Funciones adicionales compatibles:
for of, Typed arrays, literales de cadena \u{hex}
- Funciones de Math:
imul, clz32, fround, trunc, log2, log10
- Operador exponencial, flags de regexp (
s, y, u), funciones de cadena (replaceAll, trimStart, trimEnd), globalThis
API de C
- Dependencia mínima de la biblioteca estándar de C; no usa
malloc, free ni printf
- Se debe proporcionar directamente un búfer de memoria, y el motor solo asigna memoria dentro de ese búfer
- Ejemplo:
ctx = JS_NewContext(mem_buf, sizeof(mem_buf), &js_stdlib)
- Debido al método de recolección de basura, no es necesario llamar a
JS_FreeValue()
- Como la dirección de los objetos puede cambiar en cada asignación, se recomienda usar punteros a
JSValue
JS_PushGCRef() / JS_PopGCRef() permiten una gestión segura de referencias
- La biblioteca estándar se compila como estructuras en C que pueden almacenarse en ROM, logrando inicialización rápida y bajo uso de RAM
- La ejecución de bytecode puede hacerse desde ROM, reubicándolo con
JS_RelocateBytecode() y luego ejecutándolo con JS_LoadBytecode() y JS_Run()
- Incluye una biblioteca matemática (
libm.c) y un emulador de punto flotante integrados
Estructura interna y comparación con QuickJS
- Recolección de basura: usa GC por trazado y compactación en lugar de conteo de referencias
- Evita la fragmentación de memoria y reduce el tamaño de los objetos
- Representación de valores: diseñada según el tamaño de palabra del CPU (32/64 bits)
- Puede almacenar enteros de 31 bits, puntos de código Unicode, números de punto flotante y punteros a bloques de memoria
- Las cadenas se almacenan en UTF-8, de forma más eficiente que el esquema de arreglos de 8/16 bits de QuickJS
- Las funciones en C pueden almacenarse como un solo valor, pero no admiten propiedades adicionales
- La biblioteca estándar reside en ROM y minimiza los objetos en RAM, lo que permite inicialización rápida del motor
- El bytecode está basado en pila y se maneja como solo lectura mediante una tabla de referencias indirectas
- Usa código Golomb para comprimir números de línea y columna
- El compilador es similar al de QuickJS, pero usa un parser no recursivo para limitar el uso de la pila de C
- Genera bytecode en una sola pasada, sin árbol de análisis
Pruebas y benchmarks
- Pruebas básicas:
make test
- Microbenchmarks de QuickJS:
make microbench
- El benchmark Octane (versión modificada para modo estricto) puede descargarse por separado
Licencia
- Distribuido bajo la licencia MIT
- El copyright del código fuente pertenece a Fabrice Bellard y Charlie Gordon
2 comentarios
Para una introducción a Fabrice Bellard, consulten lo que escribí antes en un comentario. Este tipo es de verdad una bestia sorprendente y constante.
https://news.hada.io/comment?id=51
Comentarios en Hacker News
Si algo así hubiera existido en 2010, creo que el lenguaje de scripting de Redis habría sido JavaScript y no Lua
Lua no se eligió por razones del lenguaje, sino por limitaciones de implementación: era pequeño, rápido y basado en ANSI C
Algunas ideas de Lua están bien, pero personalmente sentía que alejarse de la sintaxis de la familia Algol era innecesario
La confusión que surge al aprender nuevas abstracciones, como en SmallTalk o FORTH, vale la pena, pero no creo que los cambios de Lua tuvieran una justificación equivalente
Lua es el único lenguaje ligero que soporta tail call optimization (TCO), y gracias a eso puedes escribir programas recursivos sin usar bucles
JavaScript no tiene esa optimización, así que no puede hacerse de la misma manera
Lua también es especialmente adecuado para escribir compiladores, porque abundan las estructuras recursivas
Puede que JS encaje mejor para scripting en Redis, pero es una pena menospreciar a Lua
En Brasil se usaban más Pascal y Ada que C, así que viene de esa influencia
Ruby y Perl también salieron por esos años, pero intentaron cambios sintácticos mucho más radicales
Casi no ha habido intentos de intercambiar tokens como
then/enden lugar de{}, manteniendo separados el parser y el lexerDiscusiones relacionadas: hilo de HN, debate en Reddit
Este motor restringe JS de la forma en que yo quería cuando trabajaba en JSC
En la web esas limitaciones son imposibles por la compatibilidad, pero en entornos embebidos ese tipo de restricciones incluso puede ser un diseño placentero
Hice un playground para ejecutar MicroQuickJS directamente en el navegador
Versión WebAssembly de MicroQuickJS
Como referencia, también está la versión original de QuickJS
QuickJS pesa 2.28MB y MicroQuickJS 303KB, así que es mucho más liviano
Si agregas
emcc -O3o--closure 1, probablemente podría reducirse másQuickJS ya parece estar optimizado, y donde todavía habría margen de mejora es en MicroQuickJS
Como dijo Jeff Atwood en su famosa frase, “cualquier aplicación que pueda escribirse en JavaScript, eventualmente se escribirá en JavaScript”
Ahora parece que eso también aplica a los sistemas embebidos
Wiki de Jeff Atwood
Enlace a JSLinux
Es una lástima que se haya subido sin historial de commits
Me habría gustado ver qué tan rápido alguien de ese nivel puede terminar un proyecto
De todos modos, como está basado en QuickJS, la comparación probablemente no tendría mucho sentido
Me pregunto si esta podría ser la forma más ligera de resolver el desafío JS de YouTube en yt-dlp
Ver la documentación EJS de yt-dlp
QuickJS ya está soportado
Los acertijos JS de YouTube son tan complejos que hasta un emulador de JS hecho en Python terminó dándose por vencido
No sé mucho de sistemas embebidos, pero me pregunto si algo así podría permitir programar ESP32 o Arduino en JavaScript
Algo al estilo de MicroPython
MicroQuickJS solo implementa parte de ES5 y tampoco ofrece bindings del entorno
Ejecutaba el código JS convirtiéndolo a bytecode de una VM de Lua, y era un enfoque bastante ingenioso
Hace poco reescribí en Rust aquella vieja CLI de Node 0.8, pero al final el hardware volvió al cajón
malloc(). Eso es lo que abre posibilidadesEl timing realmente importa. Cuando lo publicaron anoche no tuvo ninguna reacción
Hay quien lo vuelve a publicar en la mañana de EE. UU. o lo repostea periódicamente como estrategia
Fabrice Bellard es uno de los programadores más productivos y versátiles que existen hoy
Obras destacadas: FFmpeg, QEMU, JSLinux, TCC, QuickJS
Es una figura legendaria
Impresiona su manera de construir programas completos con dependencias y herramientas mínimas
Porque si fuera una persona real, tendría que dormir
ts_server, TextSynth
Parece preferir estructuras donde el usuario ajusta parámetros y luego el programa se ejecuta de forma autosuficiente
Lista de ganadores del IOCCC
Impresiona eso de que “se puede compilar y ejecutar JS incluso con 10kB de RAM”
Llega justo en una época en la que la RAM se está encareciendo
Me pregunto si se podría meter en Chromium o Electron