23 pontos por GN⁺ 2025-06-08 | 1 comentários | Compartilhar no WhatsApp
  • Explica em detalhes, com base em um exemplo real de um resolvedor de Cubo de Rubik, o processo de portar código C/C++ para WebAssembly com Emscripten e criar um aplicativo web que roda no navegador
  • Aborda passo a passo dificuldades concretas e formas de resolvê-las encontradas no ambiente de navegador/WebAssembly, de Hello World a multithreading, callbacks, armazenamento persistente e modularização
  • Foca em troubleshooting prático como inicialização assíncrona em JavaScript, exportação de funções, Web Workers e a questão do Spectre, além de armazenamento persistente em IndexedDB via IDBFS
  • Enfatiza repetidamente que as abstrações do Emscripten frequentemente “vazam” (leaky abstractions), destacando a necessidade de entender os limites e a estrutura interna da plataforma web
  • É um guia baseado em experiência prática que oferece ajuda concreta e conhecimento útil para desenvolvedores que querem portar bibliotecas C/C++ existentes para a web, a partir da experiência real de levar uma base de código C complexa para a web com apenas conhecimento mínimo de JavaScript/HTML de frontend

Introdução

  • Recentemente, foi realizado um projeto para implementar como aplicativo web um algoritmo de solução ótima do Cubo de Rubik
  • O texto registra o processo de compilar com Emscripten um resolvedor de Cubo de Rubik otimizado desenvolvido em C para executá-lo no navegador como WebAssembly
  • O principal motivo para usar WebAssembly é obter na web um desempenho quase nativo em comparação com JavaScript
  • Este texto não é um tutorial tradicional de desenvolvimento web, mas sim uma ‘jornada de sofrimento’ para desenvolvedores que querem portar código C/C++ existente para a web
  • Mesmo sem muita experiência com desenvolvimento web, é possível acompanhar sabendo apenas a estrutura básica de HTML e JavaScript e como usar as ferramentas de desenvolvedor do navegador

Configuração do ambiente

  • Todo o código de exemplo pode ser visto no repositório git e no GitHub
  • É necessário instalar o Emscripten (consulte o site oficial para a instalação), e pode-se usar um servidor web como darkhttpd ou o http.server do Python
  • Os exemplos do tutorial foram testados em sistemas Linux e da família UNIX. Para usuários de Windows, recomenda-se o WSL (Windows Subsystem for Linux)

Hello World

  • Ao compilar um Hello World em C com o comando emcc -o index.html hello.c, são gerados três arquivos: index.html (página web), index.wasm (bytecode WebAssembly) e index.js (JavaScript glue code)
  • É possível rodar tanto no navegador quanto no Node.js, e cada ambiente tem formas de uso diferentes
  • Para gerar apenas o .wasm, use a opção -sSTANDALONE_WASM
  • Embora o Emscripten possa gerar apenas o .wasm, na maioria dos casos o JavaScript glue code é indispensável

Intermezzo I: O que é WebAssembly?

  • WebAssembly (WASM) é uma linguagem de baixo nível executada em uma máquina virtual de alto desempenho dentro do navegador
  • WASM é suportado por todos os principais navegadores desde 2017
  • Originalmente, o Emscripten convertia código C/C++ para um subconjunto de JavaScript chamado asm.js, mas migrou com a chegada do WASM
  • Também existe uma representação textual, e sua estrutura é baseada em pilha. Até recentemente, só havia suporte a arquitetura de 32 bits, o que impedia usar mais de 4 GB de memória, mas o WASM64 está sendo introduzido gradualmente nos navegadores

Build de biblioteca

  • Apresenta um exemplo básico de compilar a função C multiply() para WASM e chamá-la a partir de JavaScript
  • Em um build padrão, o Emscripten adiciona um underscore (_) ao nome da função (por exemplo, _multiply)
  • Para expor funções externamente, é necessário especificar a opção -sEXPORTED_FUNCTIONS
  • Como a inicialização ao carregar a biblioteca é assíncrona, é preciso lidar com isso com onRuntimeInitialized ou await
  • O código de prática está na pasta 01_library do repositório

Intermezzo II: JavaScript e DOM

  • Para acessar e modificar elementos do HTML em JavaScript, é preciso usar o Document Object Model (DOM)
  • É possível implementar uma UI dinâmica com event listeners (addEventListener) e operadores/funções embutidos
  • O texto explica uma estrutura básica de integração entre HTML e JavaScript com entrada, botão e exibição de resultado
  • Também orienta sobre formas práticas de separar/combinar scripts e os problemas envolvidos (por exemplo, uso de defer e ordem de carregamento dos elementos do DOM)

Modularização e carregamento da biblioteca

  • Para incluir várias bibliotecas WASM ou reutilizá-las tanto no Node.js quanto na web, é possível fazer o build em formato modular com as opções MODULARIZE e EXPORT_NAME
  • A extensão .mjs (módulo ES6) é recomendada para compatibilidade com Node.js
  • É possível usar módulos tanto na web quanto no Node com a forma import MyLibrary from ...

Multithreading

  • No WebAssembly, é possível portar código multithread baseado em pthreads para melhorar o desempenho
  • O texto cria várias threads dentro de uma função para executar trabalho computacional em paralelo (por exemplo, contar números primos)
  • No build, são necessárias as opções -pthread e -sPTHREAD_POOL_SIZE=
  • Em navegadores reais, também é preciso adicionar cabeçalhos HTTP como Cross-Origin-Opener-Policy: same-origin e Cross-Origin-Embedder-Policy: require-corp
  • Todos os exemplos podem ser vistos na pasta 03_threads do repositório

Intermezzo III: Web Workers e Spectre

  • O multithreading do Emscripten é implementado com Web Workers (Web Workers são processos separados com comunicação baseada em mensagens)
  • O uso de memória compartilhada (SharedArrayBuffer) tem restrições de segurança
  • Após a vulnerabilidade Spectre em 2018, passaram a ser exigidos isolamento cross-origin (cross-origin isolated) e cabeçalhos relacionados

Cuidado com o bloqueio da thread principal

  • Quando uma tarefa longa BLOQUEIA a thread principal de UI do navegador, a experiência do usuário piora drasticamente
  • Para evitar isso, o texto introduz Web Workers para separar claramente o processamento de UI/entrada do processamento computacional
  • A comunicação baseada em eventos entre thread principal e worker é implementada com postMessage e onmessage
  • Dentro do Web Worker, o módulo Emscripten-WASM é carregado para cuidar apenas das operações assíncronas

Funções de callback

  • Ao passar um ponteiro de função como parâmetro para uma função C (callback), não é possível integrá-lo automaticamente com objetos de função do JavaScript
  • É preciso usar recursos fornecidos pelo Emscripten, como addFunction() e UTF8ToString(), e adicionar no build as opções -sEXPORTED_RUNTIME_METHODS e -sALLOW_TABLE_GROWTH
  • Para funcionar de forma estável, os callbacks devem ser chamados apenas na thread principal (não podem ser acessados de Web Workers)

Armazenamento persistente

  • Para armazenar dados permanentemente no navegador do usuário, é usado o IDBFS (sistema de arquivos baseado em IndexedDB) do Emscripten
  • No build, são necessários o flag --lidbfs.js e configurações iniciais com --pre-js
  • No código C, ainda é possível usar funções de entrada/saída de arquivos (fopen, fread, fwrite), mas para refletir/sincronizar os dados de fato é necessário fazer explicitamente o mapeamento e o sync em JavaScript
  • Pelas características de sandbox e políticas de segurança do navegador, o acesso direto ao sistema de arquivos local só é possível no Node.js; no navegador, é preciso usar um backend como o IDBFS para armazenar dados persistentes com segurança

Conclusão

  • Ao longo de todo o tutorial, é possível aprender em detalhes formas práticas de executar código nativo C/C++ complexo no navegador com segurança e sem perda de desempenho, usando apenas o mínimo de JavaScript e HTML
  • Em um ambiente real, é possível vivenciar as dificuldades e soluções de todas as trilhas centrais, incluindo multithreading, callbacks, processamento assíncrono e integração com armazenamento, além de aprender as configurações relacionadas e as restrições atuais dos navegadores
  • Os exemplos do repositório Git fornecidos podem ser usados como referência para aplicar e expandir em projetos próprios

1 comentários

 
GN⁺ 2025-06-08
Comentários do Hacker News
  • Queria que dessem mais atenção ao fato de que ele mudou a extensão de .js para .mjs; passa muito a sensação de identificação com a realidade de que, qualquer que seja a extensão usada, você acaba esbarrando em algum problema. Para quem já passou por vários sistemas de módulos, de dojo a CommonJS, AMD, ESM, webpack, esbuild e rollup, é impossível não concordar 100% com isso.
    • A transição de CommonJS para ESM foi algo gigantesco, quase como a passagem de Python 2 para Python 3, mas a impressão é que, comparado à expectativa, os ganhos foram pequenos e o incômodo só aumentou. Hoje em dia muitas bibliotecas já suportam apenas ESM, então virou quase rotina abrir a aba versions do npm, escolher a versão mais baixada no último mês e torcer para que aquela seja a última versão com CommonJS. Sem dúvida o ESM pode ser chamado de um sistema de módulos mais avançado, mas continua difícil entender por que o tc39 fez coisas quase deliberadamente incompatíveis com CommonJS, como top-level await.
    • A história dos módulos em JS parece literalmente um trauma. Agora que até import maps chegou ao navegador, fico curioso para ver que novos problemas “divertidos” ainda vão aparecer.
    • Descobri recentemente que o objeto Function consegue compilar qualquer código JS em tempo de execução, e isso tem sido extremamente útil como uma espécie de salva-vidas no meu ambiente, onde eu nem posso usar import. Talvez isso não seja muito necessário no ecossistema JS em geral, mas para mim tem ajudado bastante.
    • Por isso todo mundo deveria usar bun.sh.
    • Não daria para usar .esm.js também?
  • Se eu fosse apontar mais coisas neste texto que podem causar problemas no longo prazo, recomendaria usar let ou const em vez da palavra-chave var. var ainda funciona, mas hoje a maioria dos desenvolvedores JS bloqueia seu uso no linter. Como var só suporta escopo de função, isso acaba confundindo quase todo mundo que vem de outras linguagens em algum momento. Sobre problemas de portabilidade para apps nativos, foi citado o exemplo de hardcodar copiar/colar em tempo de compilação com Ctrl-C e Ctrl-V, o que funciona em Linux e Windows, mas não no Mac. Na web, o certo é tratar isso detectando eventos de copy e paste; já vi até frameworks como Unity ficarem sem copiar e colar no Mac por causa de teclas hardcodadas. Na maioria dos jogos isso não importa, mas quando você exporta para a web algo que precisa de copiar e colar, isso quase sempre vira problema.
  • Desabafo sobre como multithreading em web/NodeJS é horrível. Em vez de permitir passar o próprio valor entre contextos, como v8 isolates, com primitivas de sincronização como mutex ou rwlock, o que acabaram introduzindo foi basicamente o SharedArrayBuffer, que na prática serve para muito pouca coisa. A sincronização entre threads acaba virando uma estrutura de RPC com thunking e cópia de dados. O app de produção da nossa empresa é um monstrengo que usa de 70 a 100 GB de RAM — e já era assim antes de eu entrar —, então estamos tendo de buscar uma solução esquisita baseada em código nativo, gerenciando diretamente páginas de memória e estruturas de dados customizadas para minimizar serialização e desserialização. Como o v8 usa UTF-16 para strings, manipular valores JS na camada nativa sai caro.
    • Fico me perguntando se esse app que usa 100 GB de RAM realmente precisava ser um webapp. Parece mais o tipo de ferramenta interna que faria sentido ser escrita em algo como C#.
  • Esse ecossistema é tão próximo do caos que chamar de “masoquista” até parece mais normal.
    • Dá até para dizer que o caos já vem embutido.
  • O texto em si foi bem escrito, e ainda mais surpreendente por ter começado por um caminho difícil e complicado. Dá para sentir que a configuração do projeto é a parte mais difícil. Parabéns por já ter batido de cara com questões de segurança e headers, embora às vezes o problema esperado seja CORS. Na nossa empresa também estamos compilando com emscripten/C++, e ainda vamos adicionar WebGPU/shaders e WebAudio, então a jornada promete ficar ainda mais pesada.
  • Antigamente eu achava vagamente que compilar código no navegador “seria lento”, mas o OP explicou bem que não é necessariamente assim. O projeto Emscripten também enfatiza que “a combinação de LLVM, Emscripten, Binaryen e WebAssembly produz um resultado pequeno e que roda em velocidade quase nativa” (emscripten.org).
    • Hoje foi um dia meio de “síndrome do ônibus amarelo” para mim. Até a semana passada eu nem conhecia Emscripten, mas ao integrar SDL ao projeto encontrei no CMake comentários sobre os alvos APPLE, MSVC e EMSCRIPTEN. E justo hoje me deparei de novo com Emscripten no HN, então acho que já passou da hora de reservar um tempo e mergulhar nisso a sério.
    • A expressão “velocidade quase nativa” parece bem subjetiva. Não consegui achar dados numéricos na documentação mostrando quão rápido isso é na prática.
  • O texto foi útil, e eu também quero compilar um compilador escrito em C para WebAssembly e transformá-lo em um playground web. Aliás, os navegadores modernos permitem usar SQLite via JavaScript; fico curioso se isso também é possível em wasm. Se o emscripten conseguisse fazer a ponte entre chamadas da API sqlite em código C e o banco SQLite do navegador, seria ideal demais, então vale investigar melhor.
  • Fiquei curioso por que foi usada a porta 48 para SSL; houve algum motivo especial?
    • A resposta foi que a porta foi escolhida aleatoriamente por causa do nome H48. Como esse webapp precisa de headers HTTP adicionais, a solução simples foi usar outra porta para implementar isso sem afetar o restante do site. Também há redirecionamento para https://h48.tronto.net, e a pessoa comentou que está pensando em melhorar depois a configuração do httpd e do relayd do OpenBSD, ou até mover tudo para um domínio separado.