jueves, julio 05, 2018

Web Assembly en julio de 2018

Hay multitud de información sobre Web Assembly y sobre Emscripten en la red pero, por hacer referencia a diferentes versiones, a varios años de tiempo y demás, pueden acabar despistando. En este artítulo intento recopilar información sobre desarrollo práctico con Emscripten para WebAssembly a fecha de julio de 2018.

 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
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
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á 
Y con esta compilación, su carga y uso en Javascript podrían ser:
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
Con el método "set" podemos copiar de un Array con tipo en Javascript a una zona de estas vistas de la memoria.
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");
  );

No hay comentarios: