23 puntos por GN⁺ 2025-06-08 | 1 comentarios | Compartir por WhatsApp
  • 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

 
GN⁺ 2025-06-08
Comentarios de Hacker News
  • Ojalá se destacara más el hecho de que cambió la extensión de .js a .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%.
    • La transición de commonjs a esm fue un cambio enorme, casi como pasar de python2 a python3, pero comparado con lo que se esperaba, da la impresión de que trajo pocos beneficios y mucha más incomodidad. Hoy en día muchas librerías ya solo soportan esm, así que últimamente la realidad es que uno va a la pestaña versions de 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 con top-level await.
    • La historia de los módulos en js se siente literalmente como un trauma. Ahora hasta se introdujeron los import maps en el navegador, así que da curiosidad ver qué nuevos problemas “divertidos” van a aparecer después.
    • Hace poco descubrí que el objeto Function puede compilar cualquier código JS en tiempo de ejecución, y como en mi entorno ni siquiera puedo usar 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.
    • Por eso todos deberían usar bun.sh.
    • ¿No se podría usar también .esm.js?
  • Si quisiera señalar más partes de este texto que podrían causar problemas a largo plazo, recomendaría usar let o const en lugar de la palabra clave var. var sigue funcionando, pero hoy la mayoría de los desarrolladores de JS prohíben su uso con el linter. Además, var solo 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.
  • Qué fastidio el multithreading en web/NodeJS. Es una lástima que, en lugar de introducir primitivas de sincronización como mutex o rwlock que permitan transferir el valor mismo entre contextos, por ejemplo entre v8 isolates, al final lo que metieron fue 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.
    • Dan ganas de preguntarse si una app que usa 100GB de RAM realmente tenía que ser una webapp. Suena más bien como algo que debería ser una herramienta interna escrita en un lenguaje como C#.
  • Este ecosistema está tan cerca del caos que hasta lo de “masoquista” ya suena más razonable.
    • De hecho, se podría decir que el caos ya viene implícito.
  • El texto en sí está bien escrito, y además sorprende que hayan elegido empezar por una ruta tan difícil y compleja. Se nota que la parte más dura es configurar el proyecto. Bien por toparse de inmediato con temas de seguridad/headers, aunque a veces el problema esperado es CORS. En nuestra empresa también estamos compilando con emscripten/C++, y encima vamos a sumar WebGPU/shaders y WebAudio, así que se viene un camino todavía más pesado.
  • Antes asumía vagamente que compilar código en el navegador “iba a ser lento”, pero el OP explica muy bien que no necesariamente es así. El proyecto Emscripten también enfatiza que “gracias a la combinación de LLVM, Emscripten, Binaryen y WebAssembly, el resultado es pequeño y corre a una velocidad casi nativa” (emscripten.org).
    • Hoy estoy teniendo uno de esos días de “síndrome del autobús amarillo”. Hasta la semana pasada ni conocía Emscripten, pero al integrar SDL en un proyecto me topé en CMake con comentarios sobre targets APPLE, MSVC y EMSCRIPTEN, y justo hoy volví a encontrarme con Emscripten en hn. Ya siento que llegó el momento de apartar tiempo y meterme de lleno a investigarlo.
    • La expresión “velocidad casi nativa” suena bastante subjetiva. No encontré en la documentación datos numéricos concretos sobre qué tan rápido es en la práctica.
  • El texto fue útil, y yo también quiero compilar a WebAssembly un compilador escrito en C para convertirlo en un playground web. Como referencia, los navegadores modernos ya permiten usar SQLite desde JavaScript, y me pregunto si eso también es posible desde wasm. Si emscripten pudiera conectar las llamadas a la API de sqlite en código C con una base de datos sqlite del navegador, sería ideal, así que vale la pena investigar más.
  • Me da curiosidad por qué usaron el puerto 48 para SSL; pregunto si hubo alguna razón especial.
    • La respuesta fue que el puerto se eligió al azar a partir del nombre H48. Como esta webapp necesitaba headers HTTP adicionales, la razón de usar un puerto distinto fue implementarlo de forma simple sin afectar al resto del sitio. También redirige a https://h48.tronto.net, y explican que más adelante están pensando en mejorar la configuración de httpd y relayd de OpenBSD, o incluso moverlo a un dominio separado.