Lo primero de todo un repaso de conceptos. WebAssembly surgió como evolución de los experimentos que hubo de pasar código C a Javascript simplificado (asm.js). Con esto tan sencillo ya se lograron algunos éxitos de rendimiento. Se evolucionó el concepto, hacia algo casi "código máquina para la web, multiplataforma). Por fin, en 2017 hubo versiones en todos los navegadores más populares que soportaban este nuevo formato de ejecución.
WebAssembly |
Siempre hubo discusiones respecto a si WebAsembly venía a sustituir Javascript o no. En mi opinión, esta muy bien para labores que supongan mucha CPU o para los que quieran ofuscar su código, pero para intregración con navegador, sus APIs y demás, es mejor seguir con Javascript u otros lenguajes soportados directamente por el navegador de turno (opinión a julio de 2018).
Y aquí aparece Emscripten. Este paquete / framework / producto fue el más famoso que pasaba código de C/C++ a asm.js, y luego ha evolucionado hasta ser el rey en pasar código a WebAssembly.
Emscripten |
Y como es un producto que ha estado siempre entre experimental y producción, introduciendo nueva funcionalidad, dependencias,... su documentación y formas de uso han ido cambiando, de ahí uno de los problemas que he visto, que al buscar ayuda por Internet sobre algún error, las soluciones pueden variar mucho, dependiendo de la época del producto que mencionen.
Fase 1 - Instalación de entorno
En su estado, a día de hoy basta con ejecutar los siguientes comandos para tener una instalación de Emscripten totalmente funcional, y activarla:
git clone https://github.com/juj/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
En el entorno de Windows tuve problemas para que funcionara correctamente, por lo que al final me generé una imagen Docker:
FROM ubuntu:bionic MAINTAINER Marcos Perez <alguien@aqui.es> RUN apt-get update && apt-get install -y git wget python2.7 nodejs cmake default-jre RUN apt-get install -y python g++ RUN cd opt && git clone https://github.com/juj/emsdk.git && cd emsdk && ./emsdk install latest && ./emsdk activate latest
Fase 2 - Generación de WebAssembly
Originalmente se indicaba en la documentación que, para compilar un proyecto normal C/C++ nativo a el sistema de Emscripten, el procedimiento sería algo como:cd emsdk ./emsdk activate latest cd proyecto ./emconfigure ./configure ./emmake make
Sin embargo, hoy en día, en un proyecto normal se podría user directamente "configure" y make, en lugar de los específicos emconfigure y emmake.
Como resultado podríamos tener un fichero .wasm para usar.
Fase 3 - Usar en navegador
Una vez que tenemos el fichero WASM, su uso en un navegador moderno debería ser sencillo.Un código como este es muy habitual ver en las webs:
fetch('simple.wasm').then(response => response.arrayBuffer() ).then(bytes => WebAssembly.instantiate(bytes, importObject) ).then(results => { results.instance.exports.exported_func(); });
Y, de hecho, el concepto es sencillo: cargar el fichero WASM, pasarlo a binario, compilar, instanciar y usar las funciones que vinieran dentro.
Sin embargo, en cuanto no es muy óbvio, esto no es tan sencillo, principalmente por la memoria. Hace falta declarar la memoria que podrá hacer uso el wasm, y luego la que podría ser compartida con Javascript. En el fondo, son multitud de detalles, que podrían depender de implementaciones internas.
Por ello, mi recomendación sería dar un paso atrás (volver a Fase 2), y crear un comando que nos genere el objeto final.
emcc -lmibiblioteca micodigo.c -Os \
-s WASM=1 -s ALLOW_MEMORY_GROWTH=1 \
-s MODULARIZE=1 -s EXPORT_NAME='MIMODULO' \
-s EXPORTED_FUNCTIONS="['_mifuncion1', '_mifuncion2']" -o misalida.js
En este caso, hemos supuesto que tenemos creada una biblioteca de algún otro producto, y luego hemos hecho la interfaz de comunicaciones ad hoc micodigo.c.
Todas las opciones especiales de emscripten va con "-s":
- WASM=1 para que genere WebAssembly
- ALLOW_MEMORY_GROWTH=1 para que pueda reservar más memoria según la vaya necesitando
- MODULARIZE=1 para que se puedan juntar varios módulos, y meter todas sus funciones en su espacio de nombres
- EXPORT_NAME sería para dar nombre el módulo que generar
- EXPORTED_FUNCTIONS indica las funciones que se podrán llamar desde Javascript. Importante recalcar que sus nombres serán con la convención de C, y de ahí que añada un "_" al principio del nombre de la función
- Finalmente el -o misalida.js . De este modo nos generará un misalida.wasm y un misalida.js que se encargará
importScripts("misalida.js"); console.log("Cargado"); console.log(MIMODULO); // Modulo WASM var mimodulo=null; MIMODULO().then(function(Module) { mimodulo=Module; mimodulo._mifuncion1(); console.log("Inicializado"); });
De esta manera, la preparación de la memoria y demás correría a cargo del código javascript que ha preparado antes Emscripten.
Como dicho webassembly hay que compilar, antes hay que esperar a que se prepare todo, y de ahí la importancia de MIMODULO().then(), para así realizar las tareas cuando esté disponible, NO ANTES.
Fase 4 - Comunicación
Llamar a las funciones con parámetros básicos como números o texto es directo, por ejemplo:var resultado = mimodulo._mifuncion2(4, "mi texto");Sin embargo, la cosa se complica cuando hay "punteros" de por medio, y es que en este caso el sistema no hace una conversión automática de los parámetros.
Por ejemplo, para acceder a la memoria de wasm, se puede hacer mediante las vistas:
- HEAP8 memoria compartimentada como 8 bits con signo
- HEAP32 memoria compartimentada como 32 bits con signo
- HEAPU32 memoria compartimentada como 32 bits sin signo
- HEAPF32 memoria compartimentada como punto flotante de 32 bits
Y para traer datos de WASM a Javascript, sería con métodos "from" y un subarray. Por ejemplo:
var vector = Float32Array.from( MiModulo.HEAPF32.subarray(desde>>2, (desde + salida*4) >>2) );
Importante señalar que, a la hora de indicar los comienzos / fin, hay que modificarlos por los tamaños de lo apuntado. En este caso, lo dividimos entre 4, por ser Punto flotante de 32 bits.
Para ejecutar código Javascript desde dentro del C / C++:
EM_ASM(
console.log("Pintar salida");
);