2 pontos por GN⁺ 2025-02-05 | 1 comentários | Compartilhar no WhatsApp
  • Um desenvolvedor passou 6 meses dedicando noites e fins de semana para criar a VM WebAssembly Semblance, tratando o projeto como uma iniciativa de aprendizado de longo prazo focada em estudar a especificação, em vez de um side project curto
  • WebAssembly é um bytecode padrão para executar código não confiável no navegador em velocidade próxima à nativa, e seu uso fora do navegador também vem crescendo, em casos como Fastly, Shopify e Zed
  • O objetivo da Semblance era aprender a WebAssembly Core Specification e se preparar para contribuir com runtimes industriais como o Wasmtime, por isso implementar todos os opcodes ou passar em toda a core test suite ficou fora do escopo
  • O interpretador funciona verificando os magic bytes e a versão 1 de um arquivo .wasm, depois segue com decodificação de seções, resolução de imports, instanciação do módulo, busca da função exportada, inicialização da pilha e do activation frame, e execução de um opcode switch loop
  • Como não é um runtime completo, ele ainda é lento, pode ter vazamentos de memória e continua vulnerável a módulos maliciosos, mas o objetivo de aprender a especificação foi alcançado com a execução de Hello, World!

Por que escolher WebAssembly

  • WebAssembly é um conjunto padrão de instruções e um formato de bytecode para uma máquina virtual de pilha
  • O ponto de partida foi o compilador de C/C++ para JavaScript do Emscripten
    • O Emscripten convertia LLVM IR em JavaScript para permitir a execução de código C e C++ na web
    • Desenvolvedores de navegadores e o projeto Emscripten restringiram o código gerado a um subconjunto simples de JavaScript para otimização de desempenho, e esse subconjunto foi padronizado como asm.js
    • Depois, para evitar a sobrecarga de parsing do JavaScript, foi projetado o Wasm, um formato de bytecode independente
  • Mais recentemente, o uso de WebAssembly fora do navegador também aumentou
    • Edge Compute, da Fastly, e Functions, da Shopify, são construídos sobre motores WebAssembly
    • O sistema de extensões do Zed também é baseado em Wasm
  • A Bytecode Alliance lidera o esforço da indústria para apoiar o uso de WebAssembly fora do navegador, desenvolvendo o runtime Wasmtime e a padronização do WASI
    • O WASI permite que módulos WebAssembly se comuniquem com o sistema operacional hospedeiro por meio de host calls semelhantes ao POSIX
    • Ele fornece uma forma padrão de interagir com interfaces externas como entrada e saída padrão, sistema de arquivos e rede

De side project para projeto de aprendizado

  • Havia um padrão recorrente de começar novos side projects e perder o interesse após algumas semanas, migrando para outra coisa
  • Era necessário um projeto maior, que permitisse manter o foco por mais tempo e lidar com camadas mais baixas da pilha de computação do que no trabalho cotidiano
  • Como muitos side projects acabavam abandonados por falta de direção, a ideia era encontrar algo com objetivo claro e resultado visível
  • Depois de sentir que um perfil de engenharia generalista afetava a busca por um trabalho estável, surgiu a vontade de encontrar uma área de especialização capaz de formar um conjunto de habilidades em T
  • A possibilidade de projetar um ambiente computacional sob medida em WebAssembly com syscalls customizadas pareceu atraente como ferramenta de engenharia de plataforma

Objetivos e escopo da Semblance

  • O nome do projeto é Semblance, um interpretador WebAssembly escrito em C
  • O principal objetivo era se familiarizar com a WebAssembly Core Specification
  • No longo prazo, a meta também era acumular conhecimento sobre WebAssembly suficiente para contribuir com runtimes industriais como o Wasmtime
  • Como era um projeto com foco em aprendizado, não havia intenção de implementar todos os opcode nem de passar na core test suite
  • Conseguir executar “Hello, World!” já era considerado um critério de sucesso suficiente
  • O autor reconhece não ter experiência profissional especializada em C, admite que a qualidade do código não é ideal e diz que revisões são bem-vindas

Fluxo de execução do Hello, World!

  • O código C de exemplo declara externamente a função puts e imprime "Hello, World!\n" em hello()
  • O WebAssembly é gerado com Clang
    • --target=wasm32
    • -nostdlib
    • -Wl,--no-entry
    • -Wl,--export-all
    • -Wl,--allow-undefined
    • -O3
  • O hello.wasm gerado é executado com semblance hello.wasm --invoke hello
  • Ao iniciar o programa, são inicializadas na pilha a memória para argumentos de linha de comando, o módulo WebAssembly decodificado e a store que guarda o estado do runtime
  • Os argumentos de linha de comando são analisados, e em caso de erro o programa é encerrado

Decodificação binária e estrutura do módulo

  • O interpretador primeiro decodifica o formato binário do WebAssembly
  • No início do arquivo, ele verifica os magic bytes "\0asm"
  • Em seguida, valida se a versão do formato binário é 1
  • Depois disso, decodifica as sections incluídas
  • Quando a decodificação termina, a struct WasmModule é preenchida com as funções, tipos, imports, dados e outros elementos de hello.wasm
    • types
    • funcs
    • tables
    • mems
    • globals
    • elems
    • datas
    • start
    • imports
    • exports
    • customs
    • meta

Resolução de import e env::puts

  • A única função hospedeira fornecida atualmente pela Semblance é env::puts
  • O import de função hospedeira é tratado alocando um ponteiro de função nativa na store e colocando o funcaddr gerado no array imports do módulo
  • hostcall_puts usa como argumento o offset i32 recebido
    • obtém o ponteiro de dados da primeira instância de memória da store
    • imprime na saída padrão a string na posição do offset com printf("%s", ...)
  • Ao percorrer a lista de imports, se encontrar uma entrada cujo module name seja "env" e cujo item name seja "puts", o sistema registra a função hospedeira com register_hostcall_puts

Instanciação e execução de opcodes

  • Depois da resolução dos imports, o módulo é instanciado
  • A instanciação inclui uma etapa de validação
    • são executados testes para verificar os tipos e se o módulo tem a forma correta
    • memory, global e table são inicializados
    • se houver uma função start, ela é chamada
  • O resultado da instanciação é WasmModuleInst, que contém as estruturas de tempo de execução e os exports prontos para uso
  • A função exportada é localizada com base no nome especificado no argumento de linha de comando --invoke
    • o sistema percorre os exports e retorna o WasmExportInst cujo nome corresponde
    • também verifica se o export retornado é uma function
  • A chamada de função exige inicialização da pilha e do activation frame
  • O núcleo da execução é o opcode switch loop
    • ele lê em sequência as instruções Wasm do corpo da função
    • executa operações sobre a pilha e a store conforme o opcode
    • entre os exemplos tratados estão i32.const, i64.const, f32.const, f64.const, i32.ge_s, call, nop, unreachable e end
    • opcodes não tratados imprimem "unhandled opcode [...]" e retornam trap

Instrução call e chamada de função hospedeira

  • O tratamento de WasmOpCall usa o funcidx, valor imediato do opcode call, para localizar a instância de função na store
  • Com base nas informações de tipo da função, ele faz pop dos argumentos necessários da pilha
  • Depois verifica o tipo da instância de função para distinguir entre função Wasm e função hospedeira nativa
  • Se for uma função hospedeira, executa por native call o ponteiro de função hostfunc armazenado
  • Se houver valor de retorno, ele é recolocado na pilha com push
  • Ao chegar ao opcode end da função hello, o opcode switch loop termina e o fluxo volta para main

Resultado e limitações restantes

  • O resultado da execução aparece na saída padrão da seguinte forma
    • Hello, World!
    • Ok []
  • O projeto consegue executar um programa simples de “Hello, World!”
  • A cobertura de opcodes não é completa
  • O código é desorganizado, lento e tem vazamentos de memória
  • Ele pode ser vulnerável a módulos maliciosos
  • Ainda assim, houve muito aprendizado sobre a WebAssembly Core Specification, e isso ajudou o autor a sair da zona técnica mais familiar
  • Depois de construir o próprio interpretador, ele sente que adquiriu conhecimento sobre WebAssembly suficiente para contribuir com runtimes industriais como o Wasmtime
  • Ainda pode continuar adicionando alguns opcodes, mas pretende encerrar esta fase do projeto como foco principal

1 comentários

 
GN⁺ 2025-02-05
Opiniões no Hacker News
  • Há algum tempo, criei um interpretador de Wasm em Scheme, então fico feliz em ver mais gente implementando por conta própria.
    Não é tão difícil quanto parece; vale dar uma olhada na especificação e tentar. Nem é preciso implementar todas as instruções — basta fazer o suficiente para se divertir.

  • Como alguém que também implementa isso, uma dica: o spec-test tem muitos formatos Wasm em texto esquisitos cuja forma de compilação não é intuitiva, mas, usando o conversor wast2json, dá para gerar uma descrição JSON mais simples junto com arquivos Wasm binários comuns.

    • No Chicory fizemos do mesmo jeito: https://github.com/dylibso/chicory
      Quanto mais rápido você conseguir rodar essa suíte de testes, mais isso ajuda na velocidade de iteração e na precisão.
      Levou um tempo para fazer tudo funcionar, mas, depois que conseguimos, chegamos muito rapidamente ao ponto de executar praticamente qualquer coisa. Mesmo que a suíte de testes não seja completa, ela cobre algo como 95%: https://github.com/WebAssembly/testsuite
    • Também estou criando um compilador, e isso ajuda.
      Eu já tinha visto esses testes antes, mas não sabia como usá-los, então foi frustrante e confuso.
  • Sobre interpretar Wasm diretamente, achei este artigo bem interessante: https://arxiv.org/abs/2205.01183
    Com base nele, criei https://github.com/peterseymour/winter e aprendi que Wasm não é tão simples quanto parece.

  • Pergunta de iniciante: em uma situação em que você não está escrevendo código diretamente para o alvo, como se faz depuração de interpretador?
    Também fico curioso até que ponto fuzzing de strings de opcodes ajuda, qual é a diferença prática entre engines Wasm do lado do servidor e engines baseadas em navegador, e quanto trabalho daria trocar uma pela outra.

  • Abordagem interessante e excelente trabalho.
    Para quem quiser ver o núcleo, ele está em grande parte neste arquivo: https://github.com/irrio/semblance/blob/main/src/wrun.c
    Pensando bem, teria sido bom se o projeto seguisse a Wasm-C-API (https://github.com/WebAssembly/wasm-c-api) como interface padrão.
    Ela já é uma API em C e é adotada pela maioria dos runtimes Wasm, como Wasmer, V8, wasmi etc., então desenvolvedores familiarizados com essa API poderiam experimentar com mais facilidade.
    Se o autor tiver familiaridade suficiente com Wasm e quiser contribuir com o Wasmer, patches ou melhorias também são bem-vindos.

    • O autor claramente parece ter familiaridade suficiente com Wasm, e provavelmente também sabe o bastante para evitar a empresa que tentou registrar a marca WebAssembly.
      Foi dito que havia “preocupações compreensíveis com o fato de a Wasmer, financiada por VC, ter tentado registrar como marca WebAssembly, o nome de uma organização sem fins lucrativos”, o que é uma admissão de erro.
    • Installed-Size: 266 MB no Wasmer? Que diabos é isso?
    • Pelo que sei, está longe de ser correto dizer que o Wasmer adotou a Wasm-C-API.
      https://github.com/wasmerio/wasmer/issues/2615 ficou abandonada e acabou sendo fechada automaticamente.
  • Uma pergunta um pouco mais polêmica: há planos de adicionar uma instrução provisória de chamada de cauda?
    No lado da especificação do Wasm, isso foi recusado por ser “alto nível” demais, mas o comitê de C também já recusou uma proposta de Dennis Ritchie.
    Ainda assim, meu dinheiro continuaria em Ritchie, e acho que Rob Pike também apostou nessa direção.
    Caso contrário, por que ele teria criado Golang? Chamadas de cauda só são de alto nível quando chamadas também são de alto nível.

  • Seria bom dar uma olhada no Orca. Acho que você poderia contribuir bem por lá.
    [0] https://orca-app.dev

  • Eu também decidi focar em um único projeto em vez de ficar pulando entre coisas novas e brilhantes, mas, como exceção, estou colocando assistentes de IA para fazerem tarefas chatas.
    Para dizer o mínimo, é bem frustrante.
    E eu também ia sugerir usar uma biblioteca como libffi para lidar com múltiplos argumentos etc. ao chamar a partir de C.

  • Quando se usa WebAssembly como API de plugins em lugares como o zed, fico curioso sobre como os desenvolvedores de plugins depuram o código.
    Por exemplo, se é possível depurar com breakpoints e se, quando o código quebra, dá para receber um stack trace.