- MicroQuickJS (MQuickJS) é um motor JavaScript ultraleve projetado para sistemas embarcados, capaz de rodar com cerca de 10 kB de RAM e 100 kB de ROM
- Adota um coletor de lixo por tracing e armazenamento de strings em UTF-8 para reduzir o uso de memória, mantendo velocidade semelhante à do QuickJS
- A linguagem suportada é um subconjunto limitado de JavaScript próximo ao ES5, permitindo apenas o modo estrito (strict mode), que proíbe sintaxes com maior chance de erro
- Com a ferramenta REPL
mqjs, é possível executar scripts, salvar bytecode e definir limites de memória; o bytecode gerado pode ser executado diretamente da ROM
- O motor completo e a biblioteca padrão ficam residentes na ROM, viabilizando inicialização rápida e baixo consumo de memória, aumentando a eficiência da execução de JavaScript em ambientes embarcados
Introdução
- MicroQuickJS (MQuickJS) é um motor JavaScript voltado para sistemas embarcados, funcionando com 10 kB de RAM e 100 kB de ROM (incluindo código ARM Thumb-2)
- A velocidade é semelhante à do QuickJS
- Suporta apenas um subconjunto próximo ao ES5 e opera somente em modo estrito (strict mode), que proíbe sintaxes ineficientes ou propensas a erro
- Compartilha parte do código com o QuickJS, mas sua estrutura interna foi projetada de forma completamente diferente para economizar memória
- Usa coletor de lixo por tracing, não utiliza a pilha da CPU e adota armazenamento de strings em UTF-8
REPL
- O comando do REPL é
mqjs e suporta execução de scripts, avaliação, modo interativo, configuração de limite de memória e salvamento de bytecode
- Exemplo:
./mqjs --memory-limit 10k tests/mandelbrot.js
- Com a opção
-o, é possível salvar o bytecode compilado em um arquivo
- O bytecode salvo pode ser executado com
./mqjs mandelbrot.bin
- O bytecode varia conforme o endianness e o tamanho da palavra da CPU (32/64 bits); com a opção
-m32, é possível gerar bytecode para 32 bits
- Com a opção
--no-column, é possível remover o número da coluna das informações de depuração
Modo estrito
- Apenas strict mode é permitido; o uso da palavra-chave
with não é possível, e variáveis globais devem obrigatoriamente ser declaradas com var
- Buracos (holes) em arrays não são permitidos
- Exemplo:
a[10] = 2 gera TypeError
- Se precisar de um array com buracos, use um objeto comum
- Apenas eval global é suportado, sem acesso a variáveis locais
- Value boxing não é suportado (
new Number(1) etc.)
Subconjunto de JavaScript
- Baseado em strict mode, com foco em compatibilidade com ES5
- O objeto Array não possui buracos, e acessos a índices fora do intervalo geram erro
for in percorre apenas as propriedades próprias do objeto, e for of é suportado apenas para arrays
- O objeto global existe, mas getter/setter não são permitidos, e propriedades criadas diretamente não são expostas como variáveis globais
- Expressões regulares (Regexp) tratam diferenciação entre maiúsculas e minúsculas apenas em ASCII, e
/./ faz correspondência por code point Unicode em vez de UTF-16
- Funções de string tratam apenas ASCII (
toLowerCase, toUpperCase)
- Date suporta apenas
Date.now()
- Funcionalidades adicionais suportadas:
for of, Typed arrays, literais de string \u{hex}
- Funções de Math:
imul, clz32, fround, trunc, log2, log10
- Operador de exponenciação, flags de regexp (s, y, u), funções de string (
replaceAll, trimStart, trimEnd), globalThis
API C
- Dependência mínima da biblioteca C, sem uso de
malloc, free, printf
- É necessário fornecer diretamente o buffer de memória, e o motor só faz alocação dentro desse buffer
- Exemplo:
ctx = JS_NewContext(mem_buf, sizeof(mem_buf), &js_stdlib)
- Devido ao método de coleta de lixo, não é necessário chamar
JS_FreeValue()
- Como o endereço dos objetos pode mudar a cada alocação, é recomendável usar ponteiros para
JSValue
JS_PushGCRef() / JS_PopGCRef() permitem gerenciar referências com segurança
- A biblioteca padrão é compilada como uma estrutura C que pode ser armazenada em ROM, permitindo inicialização rápida e baixo uso de RAM
- A execução de bytecode pode ocorrer a partir da ROM, realocando com
JS_RelocateBytecode() e executando com JS_LoadBytecode() e JS_Run()
- Inclui biblioteca matemática (
libm.c) e emulador de ponto flutuante integrados
Estrutura interna e comparação com o QuickJS
- Coleta de lixo: usa GC por tracing e compactação em vez de contagem de referências
- Evita fragmentação de memória e reduz o tamanho dos objetos
- Representação de valores: projetada de acordo com o tamanho da palavra da CPU (32/64 bits)
- Pode armazenar inteiros de 31 bits, code points Unicode, ponto flutuante e ponteiros para blocos de memória
- Strings são armazenadas em UTF-8, de forma mais eficiente que o esquema de arrays de 8/16 bits do QuickJS
- Funções C podem ser armazenadas como um único valor, sem possibilidade de adicionar propriedades
- A biblioteca padrão fica residente na ROM, permitindo inicialização rápida do motor com o mínimo de objetos em RAM
- O bytecode é baseado em pilha e tratado como somente leitura por meio de uma tabela de referências indiretas
- Usa código Golomb para comprimir números de linha e coluna
- O compilador é semelhante ao do QuickJS, mas usa um parser não recursivo para limitar o uso da pilha em C
- Geração de bytecode em passagem única, sem árvore de parsing
Testes e benchmarks
- Teste básico:
make test
- Microbenchmark do QuickJS:
make microbench
- O benchmark Octane (versão modificada para modo estrito) pode ser baixado separadamente
Licença
- Distribuído sob a licença MIT
- Os direitos autorais do código-fonte pertencem a Fabrice Bellard e Charlie Gordon
2 comentários
Para uma introdução ao Fabrice Bellard, consulte o que escrevi em um comentário anteriormente. Esse cara é realmente um monstro surpreendente e consistentemente brilhante..
https://news.hada.io/comment?id=51
Comentários do Hacker News
Se algo assim existisse em 2010, acho que a linguagem de script do Redis teria sido JavaScript, não Lua
Lua foi escolhida não por razões da linguagem em si, mas por limitações de implementação (pequena, rápida e baseada em ANSI-C)
Algumas ideias de Lua são boas, mas pessoalmente sempre achei desnecessário ela se afastar da sintaxe da família Algol
A confusão que surge ao aprender novos conceitos de abstração, como em SmallTalk ou FORTH, vale a pena, mas acho que as mudanças do Lua não têm justificativa suficiente para isso
Lua é a única linguagem leve que oferece tail call optimization (TCO), o que permite escrever programas só com recursão, sem loops
JavaScript não tem essa otimização, então isso não é possível da mesma forma
Lua também é especialmente adequado para escrever compiladores, porque há muitas estruturas recursivas
Talvez JS combine mais com scripts do Redis, mas é uma pena menosprezar o Lua
No Brasil, Pascal e Ada eram mais usados do que C, então a influência veio daí
Ruby e Perl também apareceram em época parecida, mas tentaram mudanças sintáticas bem mais radicais
Quase ninguém tenta separar parser e lexer e, ainda assim, trocar tokens como
{}porthen/endDiscussões relacionadas: thread no HN, discussão no Reddit
Esse engine restringe JS exatamente da forma que eu queria quando trabalhava no JSC
Na web, restrições assim são impossíveis por causa da compatibilidade, mas em ambientes embarcados elas podem até ser um design prazeroso
Criei um playground para rodar o MicroQuickJS direto no navegador
Versão WebAssembly do MicroQuickJS
E, por referência, também existe a versão original do QuickJS
QuickJS tem 2,28MB, enquanto MicroQuickJS tem 303KB, então é bem mais leve
Com a opção
emcc -O3ou adicionando--closure 1, talvez desse para reduzir ainda maisQuickJS já está otimizado, e só o MicroQuickJS ainda parece ter margem para melhorar
Como naquela famosa frase do Jeff Atwood, “todo aplicativo que pode ser escrito em JavaScript eventualmente será escrito em JavaScript”
Agora parece que isso também vale para sistemas embarcados
Wiki de Jeff Atwood
Link do JSLinux
É uma pena que tenha sido publicado sem histórico de commits
Eu queria ver com que rapidez alguém desse nível consegue concluir um projeto
Como é baseado em QuickJS, essa comparação provavelmente não significaria muita coisa de qualquer forma
Fico me perguntando se essa pode ser a forma mais leve de resolver o desafio de JS do YouTube no yt-dlp
Veja a documentação EJS do yt-dlp
QuickJS já é suportado
Os quebra-cabeças de JS do YouTube são complexos demais; até um emulador de JS feito em Python acabou sendo abandonado por causa disso
Não conheço muito de sistemas embarcados, mas fico curioso se um engine desses poderia permitir programar ESP32 ou Arduino em JavaScript
Algo como o MicroPython
MicroQuickJS implementa apenas parte do ES5 e também não fornece bindings de ambiente
Ela executava código JS convertendo-o para bytecode de uma VM Lua, o que era uma abordagem bem inteligente
Recentemente reescrevi aquele velho CLI do Node 0.8 em Rust, mas no fim o hardware voltou para a gaveta
Timing é realmente importante. Quando foi postado ontem à noite, não teve reação nenhuma
Existe a estratégia de repostar no horário da manhã nos EUA ou republicar periodicamente
Fabrice Bellard é um dos programadores mais produtivos e versáteis que existem hoje
Obras principais: FFmpeg, QEMU, JSLinux, TCC, QuickJS
Uma figura lendária
É impressionante a abordagem de criar programas completos com o mínimo possível de dependências e ferramentas
Porque, se for uma pessoa de verdade, em algum momento ela precisa dormir
ts_server, TextSynth
Parece preferir estruturas em que o usuário define parâmetros e o programa roda até o fim por conta própria
Lista de vencedores do IOCCC
Impressiona que seja possível compilar e executar JS com apenas 10kB de RAM
Isso chega em boa hora, justamente num momento em que RAM está ficando cara
Fico curioso se daria para colocar isso no Chromium ou no Electron