- Explica en detalle, con el ejemplo real de un solver de Rubik’s Cube, el proceso de portar código C/C++ a WebAssembly con Emscripten para crear una web app que funcione en el navegador
- Recorre paso a paso dificultades concretas y cómo resolverlas en el entorno navegador/WebAssembly, desde Hello World hasta multithreading, callbacks, almacenamiento persistente y modularización
- Se enfoca en el troubleshooting práctico, incluyendo inicialización asíncrona en JavaScript, exportación de funciones, Web Workers y el problema de Spectre, y almacenamiento persistente en IndexedDB mediante IDBFS
- Recalca repetidamente que las abstracciones de Emscripten suelen “tener fugas” (leaky abstractions), y subraya la necesidad de entender los límites y la estructura interna de la plataforma web
- Es una guía basada en experiencia real que ofrece ayuda práctica y know-how a desarrolladores que quieren portar bibliotecas C/C++ existentes a la web, a partir de una experiencia real de migrar una base de código C compleja con solo conocimientos mínimos de JavaScript/HTML en frontend
Introducción
- Recientemente llevó adelante un proyecto para implementar como web app un algoritmo de solución óptima de Rubik’s Cube
- Documenta el proceso de compilar con Emscripten un solver de optimización de Rubik’s Cube desarrollado en C para ejecutarlo como WebAssembly en el navegador web
- La razón principal para usar WebAssembly es poder obtener en la web un rendimiento casi nativo en comparación con JavaScript
- Este texto no es un tutorial tradicional de desarrollo web, sino un “viaje de sufrimiento” para desarrolladores que quieren portar código C/C++ existente a la web
- Aunque no se tenga mucha experiencia en desarrollo web, se puede seguir con solo conocer la estructura básica de HTML y JavaScript y cómo usar las herramientas de desarrollador del navegador
Configuración del entorno
- Todo el código de ejemplo puede consultarse en el repositorio git y en GitHub
- Es necesario instalar Emscripten (para ello, consultar el sitio oficial) y usar un servidor web como darkhttpd o Python
http.server
- Los ejemplos del tutorial fueron probados en sistemas Linux y tipo UNIX. Para usuarios de Windows, se recomienda WSL (Windows Subsystem for Linux)
Hello World
- Si se compila un Hello World en C con el comando
emcc -o index.html hello.c, se generan tres archivos: index.html (página web), index.wasm (bytecode de WebAssembly) e index.js (código glue de JavaScript)
- Puede ejecutarse tanto en el navegador como en Node.js, y cada entorno tiene formas de uso distintas
- Para generar solo
.wasm, se usa la opción -sSTANDALONE_WASM
- Aunque Emscripten puede generar solo
.wasm, en la mayoría de los casos el código glue de JavaScript es indispensable
Intermezzo I: ¿Qué es WebAssembly?
- WebAssembly (WASM) es un lenguaje de bajo nivel que se ejecuta dentro de una máquina virtual de alto rendimiento en el navegador web
- WASM es compatible con todos los navegadores principales desde 2017
- Originalmente, Emscripten convertía código C/C++ a un subconjunto de JavaScript llamado asm.js, pero eso cambió con la llegada de WASM
- También existe una representación textual y su estructura está basada en pila. Hasta hace poco solo soportaba arquitectura de 32 bits, por lo que no podía usar más de 4 GB de memoria, pero WASM64 se está incorporando gradualmente en los navegadores
Compilación de bibliotecas
- Presenta un ejemplo básico de compilar la función C
multiply() a WASM y llamarla desde JavaScript
- En la compilación por defecto, Emscripten agrega un guion bajo (_) al nombre de la función (por ejemplo,
_multiply)
- Para exponer funciones al exterior, es necesario especificar la opción
-sEXPORTED_FUNCTIONS
- Como la inicialización al cargar la biblioteca es asíncrona, hace falta manejar asincronía con
onRuntimeInitialized o await
- El código de práctica está en la carpeta
01_library del repositorio
Intermezzo II: JavaScript y el DOM
- Para acceder y modificar elementos de HTML desde JavaScript, hay que usar el Document Object Model (DOM)
- Se puede construir una UI dinámica con event listeners (
addEventListener) y operadores/funciones integrados
- Explica una estructura básica de integración HTML/JavaScript con entrada, botón y visualización de resultados
- También guía sobre métodos prácticos para separar/unir scripts y problemas habituales (por ejemplo, el uso de
defer y el orden de carga de elementos del DOM)
Modularización y carga de bibliotecas
- Para incluir múltiples bibliotecas WASM o reutilizarlas tanto en Node.js como en la web, se pueden compilar en formato modular con las opciones
MODULARIZE y EXPORT_NAME
- Se recomienda la extensión
.mjs (módulo ES6) por compatibilidad con Node.js
- El mismo módulo puede usarse tanto en web como en Node con una sintaxis tipo
import MyLibrary from ...
Multithreading
- En WebAssembly se puede portar código multihilo basado en pthreads para mejorar el rendimiento
- Dentro de una función se crean múltiples hilos para ejecutar tareas de cálculo en paralelo (por ejemplo, contar números primos)
- Al compilar, hacen falta las opciones
-pthread y -sPTHREAD_POOL_SIZE=
- En navegadores reales también se deben agregar encabezados HTTP como
Cross-Origin-Opener-Policy: same-origin y Cross-Origin-Embedder-Policy: require-corp
- Todos los ejemplos pueden verse en la carpeta
03_threads del repositorio
Intermezzo III: Web Workers y Spectre
- El multithreading en Emscripten se implementa con Web Workers (los Web Workers son procesos separados y se comunican mediante mensajes)
- El uso de memoria compartida (
SharedArrayBuffer) tiene restricciones de seguridad
- Tras la aparición de la vulnerabilidad Spectre en 2018, pasaron a ser obligatorios los requisitos de aislamiento de origen cruzado (cross-origin isolated) y los encabezados relacionados
Cuidado con bloquear el hilo principal
- Si una tarea larga BLOQUEA el hilo principal de UI del navegador, la experiencia de usuario se degrada drásticamente
- Para evitarlo, se introducen Web Workers: se separa claramente el manejo de UI/entrada del procesamiento de cómputo
- La comunicación basada en eventos entre el hilo principal y el worker se implementa con
postMessage y onmessage
- Dentro del Web Worker se carga el módulo Emscripten-WASM para encargarse solo del cómputo asíncrono
Funciones callback
- Al pasar punteros a función (callbacks) como parámetro de una función C, no es posible integrarlos automáticamente con objetos función de JavaScript
- Hay que usar utilidades provistas por Emscripten como
addFunction() y UTF8ToString(), y al compilar agregar las opciones -sEXPORTED_RUNTIME_METHODS y -sALLOW_TABLE_GROWTH
- Para funcionar de forma estable, los callbacks deben invocarse solo desde el hilo principal (no son accesibles desde Web Workers)
Almacenamiento persistente
- Para guardar datos de forma persistente en el navegador del usuario, se usa IDBFS (sistema de archivos basado en IndexedDB) de Emscripten
- En la compilación se requiere configuración inicial con flags como
--lidbfs.js y --pre-js
- En el código C se pueden seguir usando funciones de entrada/salida de archivos como
fopen, fread y fwrite, pero para reflejar y sincronizar realmente los datos es indispensable hacer mapeo y sincronización explícitos en JavaScript
- Por las políticas de sandbox y seguridad del navegador, el acceso directo al sistema de archivos local solo es posible en Node.js; en el navegador, para guardar datos persistentes de forma segura, hay que usar backends como IDBFS
Conclusión
- A lo largo de todo el tutorial se puede aprender en detalle una forma práctica de ejecutar en el navegador código C/C++ nativo complejo de manera segura y sin degradación de rendimiento, usando solo JavaScript y HTML mínimos
- También permite experimentar, en un entorno real, las dificultades y soluciones de todos los frentes clave, como multithreading, callbacks, procesamiento asíncrono e integración de almacenamiento, además de aprender configuraciones y restricciones actuales de los navegadores
- Los ejemplos del repositorio Git pueden tomarse como referencia para aplicarlos y extenderlos en proyectos propios
1 comentarios
Comentarios de Hacker News
.jsa.mjs; en realidad, se siente muy identificable esa realidad de toparse con problemas uses la extensión que uses. Habiendo pasado por varios sistemas de módulos como dojo, CommonJS, AMD, ESM, webpack, esbuild y rollup, este comentario me representa al 100%.versionsde npm, elige la versión más descargada del último mes y hay altas probabilidades de que esa sea la última versión con commonjs. Sin duda esm puede considerarse un sistema de módulos más avanzado, pero honestamente no entiendo por qué tc39 hizo que fuera casi deliberadamente incompatible con commonjs, por ejemplo contop-level await.import, eso me ha servido muchísimo como una especie de salvavidas. Tal vez no sea algo muy necesario en el ecosistema JS, pero para mí ha sido de gran ayuda..esm.js?letoconsten lugar de la palabra clavevar.varsigue funcionando, pero hoy la mayoría de los desarrolladores de JS prohíben su uso con el linter. Además,varsolo soporta scope de función, y ese suele ser un punto que termina confundiendo a desarrolladores de casi cualquier otro lenguaje. Sobre los problemas al portar apps nativas, se menciona el ejemplo de hardcodear copiar/pegar en tiempo de compilación con Ctrl-C y Ctrl-V, algo que funciona en Linux y Windows pero no en Mac. En la web eso debe manejarse detectando eventos de copy y paste; incluso he visto frameworks como Unity donde, por esas teclas hardcodeadas, copiar y pegar no funciona en Mac. En la mayoría de los juegos no hace falta, pero cuando exportas a la web una función que sí necesita copiar y pegar, casi siempre termina siendo un problema.SharedArrayBuffer, que casi no sirve de nada. La sincronización entre hilos termina estructurándose alrededor de thunking y copias de datos a través de una capa RPC. La app de producción de nuestra empresa es una aplicación gigantesca que usa entre 70 y 100GB de RAM, y ya era así antes de que yo la hiciera, así que estamos buscando una solución rarísima basada en código nativo para manejar directamente páginas de memoria y estructuras de datos personalizadas, minimizando la serialización y deserialización. Además, como en v8 las cadenas usan codificación utf16, manipular valores de JS desde la capa nativa sale caro.httpdyrelaydde OpenBSD, o incluso moverlo a un dominio separado.