- 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
Comentários do Hacker News
.jspara.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.versionsdo 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, comotop-level await.import mapschegou ao navegador, fico curioso para ver que novos problemas “divertidos” ainda vão aparecer.Functionconsegue 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 usarimport. Talvez isso não seja muito necessário no ecossistema JS em geral, mas para mim tem ajudado bastante..esm.jstambém?letouconstem vez da palavra-chavevar.varainda funciona, mas hoje a maioria dos desenvolvedores JS bloqueia seu uso no linter. Comovarsó 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 decopyepaste; 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.v8 isolates, com primitivas de sincronização comomutexourwlock, o que acabaram introduzindo foi basicamente oSharedArrayBuffer, 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.httpde dorelayddo OpenBSD, ou até mover tudo para um domínio separado.