02/06/2023 | Tecnologías

Ejecutar C++ en páginas web – Ejemplos prácticos con WebAssembly

En un anterior post del blog explicamos qué era WebAssembly y los beneficios que podría traer esta tecnología en el uso de aplicaciones web. 

Una de las principales características que se comentó acerca de WebAssembly es su portabilidad, es decir la posibilidad de utilizar otros lenguajes de programación como C++ o C en aplicaciones web, de tal manera que se pueden portar o aprovechar aplicaciones desarrolladas en estos lenguajes, para posteriormente integrar estas en páginas web que se estén desarrollando.

La portabilidad de WebAssembly es una de las características clave de esta tecnología. WebAssembly, o wasm, es un formato de código binario que se puede ejecutar en navegadores web modernos y en otros entornos fuera del navegador.

La portabilidad de WebAssembly también se beneficia de su compatibilidad con las APIs del navegador. Los programas wasm pueden interactuar con el DOM (Document Object Model), acceder a APIs de JavaScript y realizar llamadas a funciones escritas en JavaScript. Esto permite que los programas wasm accedan a características y recursos adicionales de los navegadores web.

En este post hablaremos de cómo trabajar con C++ y WebAssembly, centrándonos en sencillos ejemplos ejecutados en una máquina Ubuntu 22.04, que servirán para comenzar a trabajar con esta tecnología. Estos ejemplos se podrán encontrar en el siguiente repositorio: https://github.com/TribalyteTechnologies/WasmTrainingJun2023.

WebAssembly: compilación y su utilización

Antes de entrar de lleno con cada uno de los ejemplos que serán mostrados en este post, vamos a hacer un paréntesis para explicar una herramienta que será utilizada en cada uno de estos ejemplos.

Y es que en cada uno de los ejemplos, al igual que cuando trabajemos con C++ y WebAssembly, será necesario utilizar un compilador para convertir nuestra aplicación C++ al lenguaje WebAssembly.

Cuando estamos trabajando con C++ una de las soluciones por la que nos podemos decantar es la utilización de Emscripten[1]. 

Emscripten es un conjunto de herramientas que incluye un compilador que permitirá pasar nuestro código C++ a WebAssembly. Para utilizar Emscripten tenemos dos alternativas. La primera de ellas será instalar Emscripten en nuestra máquina. Para ello hay que asegurarse de tener instalado python3, cmake y git. Podemos verificar que tenemos instaladas estas herramientas comprobando sus versiones con los siguientes comandos:

</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>python3 --version</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>cmake --version</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>git --version</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>

Si tenemos respuesta es que ya los tenemos instalados. En el caso de que no se tenga alguno de éstos se puede instalar a través del siguiente comando:

</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>sudo apt-get install python3 cmake git</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>

Una vez se tienen las herramientas anteriores el siguiente paso es instalar Emscripten. Para ello se puede seguir los siguientes pasos:

Clonar el repositorio para su instalación

</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>git clone https://github.com/emscripten-core/emsdk.git</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>

Entrar en el directorio descargado y hace un pull para bajar los últimos cambios que puedan haber:

</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>cd emsdk</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>git pull</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>

Descargar e instalar las últimas versiones:

</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>./emsdk install latest</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>

En este paso destacar que se podría instalar una versión en concreto de Emscripten. Para ello se debe sustituir “latest” por la versión que se quiere instalar.

Activar la versión de Emscripten indicada, en el caso mostrado la última disponible:

</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>./emsdk activate latest</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>

Activar el PATH y otras variables de entorno relacionadas:

</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>source ./emsdk_env.sh</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>

Destacar que lo anterior se activará en el terminal que se está ejecutando.  Si se quiere activar para cada vez que abramos un terminal se puede añadir el anterior comando referenciando la ruta donde está ubicado “emsdk_env.sh” y añadirla al “.bashrc”.

Una vez seguidos los pasos anteriores ya se podrá utilizar Emscripten en nuestra máquina. Sin embargo, puede que no queramos descargar todo lo anterior ya que, como hemos visto, además de Emscripten hace falta tener otras herramientas. Entonces, ¿podríamos ejecutar Emscripten de otra manera sin tener que instalar nada? 

Pues la respuesta es sí, y es que existe una imagen Docker de Emscripten[2] que podemos utilizar para compilar nuestros programas de C++. Por lo tanto, para el método anterior simplemente tendremos que tener Docker instalado en nuestra máquina y ejecutar la imagen de Emscripten con un “docker run”.

A partir del apartado siguiente empezaremos a ver algunos ejemplos donde podremos ver la sentencia a ejecutar para cada caso, tanto si tenemos Emscripten instalado, como si se está utilizando la imagen de Docker.

Antes de ponernos manos a la obra con cada uno de los ejemplos,  llegados a este punto nos puede surgir una pregunta. Hasta el momento conocemos que existe un compilador para C++ que es el que utilizaremos, y que transformará la aplicación C++ que se compila a WebAssembly. Pero… ¿qué se genera exactamente? 

La mayoría de las veces que compilemos será con el objetivo de añadir el WebAssembly a una página web escrita en HTML. Pues bien, a la hora de compilar, normalmente se generarán dos ficheros asociados a WebAssembly. Por una parte, un fichero “.wasm” que contendrá las clases, funciones, variables, etc… que estaban definidas en C++, y por otra parte un “.js” que servirá de enlace para que desde Javascript se pueda llamar a las diferentes clases, funciones, variables… ya definidas en el “.wasm”.

Con los dos ficheros anteriores se podrá servir una aplicación web que utilice WebAssembly añadiendo en el HTML asociado el fichero JavaScript generado tras compilar. 

Esto último también se podrá ver en los ejemplos, así que sin más dilación vamos con el primero, un ejercicio básico, pero a la vez muy útil, un ejemplo basado en un “Hello World”.

Hello World con WebAssembly

En el mundo de la programación, independientemente de la tecnología o lenguaje que estemos empezando a utilizar, lo primero por lo que se suele comenzar es por el simple pero útil “Hello World”, que nos permitirá conocer los conceptos básicos para trabajar con las herramientas disponibles. 

Para comenzar con WebAssembly no vamos a ser menos, y vamos a empezar con un simple ejemplo de un “Hello World”. Para ello vamos a comenzar con un fichero denominado “helloWorldWebassembly.cpp”, que contendrá un programa en C++ que imprimirá una traza:

#include <iostream>

int main() {

   std::cout << «Hello World from C++!» << std::endl;

   return 0;

}

Una vez se tiene el código C++ listo, el siguiente paso es pasarlo al lenguaje WebAssembly. Para ello y, como se introdujo en el apartado anterior, se hará uso del compilador de Emscripten, con su comando “emcc”.

En función de si tenemos Emscripten instalado en nuestra máquina o no, la diferencia será ejecutar la sentencia de compilación directa o hacerla a través de un “docker run”. Por ejemplo, partiendo del caso en el que se tiene Emscripten instalado, se podría compilar el código anterior a través del siguiente comando:

</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>emcc helloWorldWebassembly.cpp -o helloWorldWebassembly.js</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>

De la sentencia anterior se puede destacar “emcc”, que sirve para invocar al compilador de Emscripten, al cual a continuación se le pasa la ruta al fichero C++ a compilar. Además de esto, se utiliza el flag “-o” para especificar al compilador el fichero de salida. Esto generará un fichero “helloWorldWebassembly.js” y, además, aunque no se le indique, generará también un fichero “helloWorldWebassembly.wasm”, que es el objetivo de este proceso.

Respecto al último comando podemos hacer un paréntesis para comentar una cosa acerca de la salida que se le indica al compilador de Emscripten. En el caso mostrado se le ha indicado que la salida fuese un fichero Javascript, lo que posteriormente generará ese fichero Javascript y el WebAssembly. Sin embargo, se pueden indicar otras salidas, como por ejemplo, indicar que se genere solo el “.wasm” (-o helloWorldWebassembly.wasm), útil si utilizamos un runtime de WebAssembly[3].

También se podría omitir la indicación de la salida. Ante este caso se generarian los ficheros JavaScript y WebAssembly al igual que el primer caso comentado, con la diferencia que como no se le está indicando la salida se generarán con los nombres por defecto “a.out.js” y “a.out.wasm” (similares a los por defecto de GCC)

Hemos visto cómo compilar nuestro código C++ teniendo Emscripten instalado en nuestra máquina pero, anteriormente comentamos que podríamos utilizar una imagen de Docker de Emscripten, imagen que contendrá todo lo necesario para la compilación

Bien, para compilar con la imagen Docker de Emscripten primero tendremos que ver cual se va a utilizar de las que están disponibles: https://hub.docker.com/r/emscripten/emsdk. Por ejemplo, supongamos que cogemos la 3.1.36. El siguiente paso es ejecutar la sentencia anterior de compilación en la imagen Docker a través de un “docker run”:

</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>docker run --rm -v $(pwd):/src -u $(id -u):$(id -g) emscripten/emsdk:3.1.36 emcc helloWorldWebassembly.cpp -o helloWorldWebassembly.js</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>

De la última sentencia quizás se pueden destacar algunas indicaciones como que se monte el directorio actual en la ruta “/src” de la imagen de Emscripten “-v $(pwd):/src” o indicar que se trabajará con el mismo usuario y grupo con el que se lanza el “docker run” “-u $(id -u):$(id -g)”.

Lo anterior, al igual que en el caso previo, generará dos ficheros, el “helloWorldWebassembly.js” y el “helloWorldWebassembly.wasm”.

Una vez tenemos el equivalente de nuestra aplicación C++ en WebAssembly el siguiente paso será integrarlo en un HTML. Para ello se puede construir un simple HTML que añada el fichero Javascript generado:

</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&lt;!DOCTYPE html&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&lt;html lang="en"&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&lt;head&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&lt;meta charset="UTF-8"&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&lt;title&gt;Document&lt;/title&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&lt;script src="helloWorldWebassembly.js"&gt;&lt;/script&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&lt;/head&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&lt;body&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;Check the console</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&lt;/body&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&lt;/html></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>

Partiendo del fichero HTML creado se puede visualizar la web para verificar el pequeño programa que hemos realizado con WebAssembly. Para ello, se puede hacer uso de una imágen Docker de Apache para servir la web, y ejecutar ésta con un “docker run”. Esto se podría hacer a través del siguiente comando que serviría la página web en el puerto 2345:

</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>docker run -d --name apacheWebAssembly</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p> -v $(pwd):/usr/local/apache2/htdocs -p 2345:80 httpd:2.4</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>

Otra opción, si se está utilizando VSCode podría ser utilizar la extensión LiveServer.

Tanto si utilizamos la imágen Docker de Apache, como la extensión Live Server de VSCode, u otra solución para servir la página web, se podría acceder posteriormente a un navegador e ir a la página. Por ejemplo, se podría utilizar Google Chrome, acceder a la página y abrir la consola del navegador. En la consola del navegador se podrá ver la traza asociada al “Hello World” descrita en C++, de tal manera que se comprueba la integración de C++ en una página web.

Embind

En el ejemplo anterior, hemos visto cómo se prepara el entorno para trabajar con WebAssembly y cómo se puede interactuar entre JavaScript y  C++. En el ejemplo presentado en este apartado vamos a ver cómo se hacen accesibles a JavaScript, clases, funciones o estructuras de datos complejas de C++ entre otros.

Para que lo definido en C++ sea accesible desde JavaScript, teniendo en cuenta que vamos a seguir utilizando Emscripten, es necesario utilizar la herramienta Embind.

Embind permite la comunicación entre C++ y JavaScript[4], facilitando a los desarrolladores utilizar código C++ en JavaScript sin necesidad de reescribirlo en este lenguaje, lo que será muy útil cuando ya se tiene un código fiable escrito en C++.

La base de utilizar Embind será comunicarle a JavaScript qué funciones y estructuras de datos del codigo C++ están a su disposición para usarse. Para ello, se usará la macro «EMSCRIPTEN_BINDINGS» que, añadida a nuestro código C++, indica al compilador Emscripten que genere un módulo que pueda ser cargado por JavaScript.

Veamos lo anterior con un ejemplo.

Este ejemplo recogerá los datos introducidos por el usuario y los empaqueta en una estructura que enviará al código C++ para su tratamiento. Básicamente serán guardados en un vector y cada vez que se introduzca un nuevo dato, serán mostrados por consola, todos los datos disponibles hasta ese momento.

Esta pequeña aplicación se compone de los siguientes ficheros:

  • embindExample.h

Fichero donde se define la clase MyMap y la estructura “PersonalData“,  que es la que se va a compartir con JavaScript.

#ifndef EMBINDEXAMPLE_EMBINDEXAMPLE_H_

#define EMBINDEXAMPLE_EMBINDEXAMPLE_H_

#include <iostream>

#include <string>

#include <map>

#include <vector>

struct PersonalData {

   std::string name;

   std::string lastName;

   int   age;

};

std::vector<std::map<std::string, std::map<std::string, PersonalData>>> tribaIdeas;

std::vector<std::map<std::string, std::map<std::string, PersonalData>>> tribaTech;

class MyMap {

public:

   MyMap();

   ~MyMap();

   void set(std::string key1, std::string key2, PersonalData value);

   PersonalData get(const std::string key1, const std::string key2) {

       return data[key1][key2];

   }

private:

   PersonalData person;

   std::map<std::string, std::map<std::string, PersonalData>> data;

};

#endif  // EMBINDEXAMPLE_EMBINDEXAMPLE_H_

  • embindExample.cpp

En el fichero denominado embindExample.cpp se puede ver cómo se ha declarado la macro «EMSCRIPTEN_BINDINGS».

</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>#include &lt;map&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>#include &lt;string&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>#include &lt;emscripten/bind.h&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>#include "embindExample.h"</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>using namespace emscripten;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>MyMap::MyMap() {</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>}</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>MyMap::~MyMap() {</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>}</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>void showVector(std::vector&lt;std::map&lt;std::string, std::map&lt;std::string, PersonalData&gt;&gt;&gt; workVector) {</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;for (const auto&amp; outer_map : workVector) {</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for (const auto&amp; inner_map : outer_map) {</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for (const auto&amp; inner_inner_map : inner_map.second) {</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;std::cout &lt;&lt; "Name: " &lt;&lt; inner_inner_map.second.name</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;&lt; ", Last Name: " &lt;&lt; inner_inner_map.second.lastName</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;&lt; ", Age: " &lt;&lt; inner_inner_map.second.age &lt;&lt; std::endl;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;}</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>}</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>void MyMap::set(const std::string key1, const std::string key2, const PersonalData value) {</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;MyMap::data[key1][key2] = value;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;PersonalData person;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;if (key1 == "Tribalyte.Ideas") {</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tribaIdeas.push_back(data);</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;std::cout&nbsp; &lt;&lt; "department: Tribalyte Ideas"&lt;&lt; std::endl;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;showVector(tribaIdeas);</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;} else {</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tribaTech.push_back(data);</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;std::cout &lt;&lt; "department: Tribalyte Tech"&lt;&lt; std::endl;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;showVector(tribaTech);</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;}</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>}</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>EMSCRIPTEN_BINDINGS(MyMap) {</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;class_&lt;MyMap&gt;("MyMap")</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.constructor&lt;&gt;()</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.function("set", &amp;MyMap::set)</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.function("get", &amp;MyMap::get);</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;value_object&lt;PersonalData&gt;("personalData")</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.field("name", &amp;PersonalData::name)</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.field("lastName", &amp;PersonalData::lastName)</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.field("age", &amp;PersonalData::age);</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>}</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>}</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>

Esta macro indica que existe una clase en C++ llamada “MyMap” que tiene las funciones “set”  y         “get”, y que existe una estructura de datos “PersonalData” con los siguientes campos: name, lastName y age.

Con la declaración de esta macro hemos puesto a disposición de JavaScript un módulo “MyMap” con el que puede trabajar.

Para compilar este ejemplo debemos añadir el siguiente flag a nuestro comando de compilación: -lembind. El comando completo quedaría de la siguiente manera.

</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>emcc -lembind embindExample.cpp -o embindExample.js</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>

Si se utiliza la imagen Docker sería:

</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>docker run --rm -v $(pwd):/src -u $(id -u):$(id -g) emscripten/emsdk:3.1.36 emcc -lembind embindExample.cpp -o embindExample.js</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>

En JavaScript, tenemos que declarar la estructura con sus campos e instanciar el módulo MyMap. Finalmente se podrá llamar a las funciones “set”  y  “get”.

  • embindExample.html

</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&lt;!DOCTYPE html&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&lt;html lang="en"&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&lt;head&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&lt;meta charset="UTF-8"&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&lt;title&gt;Advanced Example Embind (C++ - Javascript)&lt;/title&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&lt;/head&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&lt;body&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;ul&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;select id="departament"&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;option&gt;Tribalyte Tech&lt;/option&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;option&gt;Tribalyte Ideas&lt;/option&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;/select&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;li&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;label for="name"&gt;Name: &lt;/label&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;input type="text" maxlength="15" id="name" name="userName"&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;/li&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;li&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;label for="lastname"&gt;Last Name: &lt;/label&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;input type="text" id="lastname" name="userLastname"&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;/li&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;li&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;label for="age"&gt;Age: &lt;/label&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;input type="number" id="age" name="userAge"&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;/li&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;/ul&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;button id="sendbtn" onclick="sendMessage()"&gt;Send&lt;/button&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;script src="embindExample.js"&gt;&lt;/script&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;script src="helper.js"&gt;&lt;/script&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&lt;/body&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&lt;/html></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>

Aquí se puede ver la función sendMessage, que es llamada desde el html al pulsar el botón “Send Message”. Esta función se encuentra en un fichero helper.js incluido en el fichero .html.

  • helper.js:

</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>function sendMessage(){</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;const nameIn = document.getElementById("name").value;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;const lastNameIn = document.getElementById("lastname").value;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;let departamentIdVar = document.getElementById("departament");</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;const departmentId = (departamentIdVar.selectedIndex)?("Tribalyte.Ideas"):("Tribalyte.Tech");</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;let ageIn = parseInt(document.getElementById("age").value);</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;const myStruct = { name: nameIn, lastName: lastNameIn , age: ageIn };</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;const MyMap = new Module.MyMap();</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;MyMap.set(departmentId,nameIn, myStruct);</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;console.log(MyMap.get(departmentId,nameIn));</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>}</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>

En este ejemplo hemos visto cómo compartir una clase y su estructura de datos, entre C++ y JavaScript; es importante tener en cuenta que solo debemos compartir lo necesario, es decir, declarar en la macro las funciones y datos que se vayan a utilizar. Posteriormente, en C++ se empaqueta esa estructura en unos mapas anidados, y se continúa procesando esos datos al margen de JavaScript.

Depuración con WebAssembly

Hasta ahora hemos visto cómo trabajar con WebAssembly partiendo de un programa en C++. Sin embargo, a la hora de desarrollar aplicaciones se nos pueden hacer necesarias algunas herramientas para verificar que el programa que hemos hecho funcione correctamente. Para esta acción, normalmente, se utilizan depuradores a través de los cuales se puede ejecutar el programa poco a poco, viendo los valores de las diferentes variables, funciones, etc.

Cuando hablamos de depuración en entornos web desde un navegador podemos realizar estas funciones. Entonces, aquí la pregunta podría ser, si estamos utilizando WebAssembly una aplicación web, ¿se puede depurar con el navegador?

Pues si por ejemplo estamos trabajando con el navegador Google Chrome sí que tendremos la posibilidad de depurar aplicaciones web que utilicen WebAssembly. Esto será posible gracias a la utilización de las Chrome DevTools y la extensión para Google Chrome, DWARF[5].

Veamos un ejemplo de cómo depurar WebAssembly con Chrome DevTools. Para ello, vamos a partir de un simple ejemplo que llama a una función de delay.

En este ejemplo tendremos los siguientes ficheros:

  • utils/debugExampleUtils.h

#ifndef DEBUGGINGEXAMPLE_UTILS_DEBUGEXAMPLEUTILS_H_

#define DEBUGGINGEXAMPLE_UTILS_DEBUGEXAMPLEUTILS_H_

#include <unistd.h>

#include <iostream>

class DebugExampleUtils {

public:

   DebugExampleUtils();

   ~DebugExampleUtils();

   void delay(int time);

};

#endif  // DEBUGGINGEXAMPLE_UTILS_DEBUGEXAMPLEUTILS_H_

  • utils/debugExampleUtils.cpp

#include «debugExampleUtils.h»

DebugExampleUtils::DebugExampleUtils() {

}

DebugExampleUtils::~DebugExampleUtils() {

}

void DebugExampleUtils::delay(int time) {

   sleep(time);

   printf(«Sleep finished»);

}

  • debugExample.cpp:

#include <iostream>

#include «utils/debugExampleUtils.h»

int main() {

   std::cout << «Starting the program» << std::endl;

   DebugExampleUtils debugExampleUtils;

   for (int i = 0; i < 10; i++) {

       debugExampleUtils.delay(1);

       printf(«Sleep %d\n», i);

   }

   return 0;

}

Al igual que en los casos anteriores, llegado en este punto es hora de compilar. Para ello, como anteriormente, se utilizará Emscripten pero con dos detalles. 

Cuando queremos que nuestro código WebAssembly incluya la información necesaria de compilación se debe agregar el flag “-g” cuando compilamos. 

Además de eso, si también queremos poder ver los ficheros anteriores de C++ en el navegador para poder interactuar con ellos habrá que añadir el flag “-fdebug-compilation-dir”, indicando que el directorio de debug es el mismo directorio en el que se está realizando la compilación, de manera que los ficheros C++ anteriores se podrán visualizar e interactuar con ellos posteriormente.

La compilación de código anterior se realizaría de la siguiente manera:

</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>emcc -g -fdebug-compilation-dir='.' debugExample.cpp utils/debugExampleUtils.cpp -o helloWorldWebassembly.js</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>

En el caso de que no se tenga Emscripten instalado y se quisiera hacer con la imagen Docker de Emscripten se ejecutaría:

</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>docker run --rm -v $(pwd):/src -u $(id -u):$(id -g) emscripten/emsdk:3.1.36 emcc -g -fdebug-compilation-dir='.' debugExample.cpp utils/debugExampleUtils.cpp -o helloWorldWebassembly.js</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>

Una vez compilado ya se habrán generado los ficheros asociados a WebAssembly. El siguiente paso será añadir a un fichero HTML el Javascript generado:

</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&lt;!DOCTYPE html&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&lt;html lang="en"&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&lt;head&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&lt;meta charset="UTF-8"&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&lt;title&gt;Document&lt;/title&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&lt;/head&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&lt;body&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;&lt;script src="debugExample.js"&gt;&lt;/script&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;&nbsp;&nbsp;Open the JS console in the browser DevTools (usually by pressing F12)</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&lt;/body&gt;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&lt;/html></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>

Tras esto se puede lanzar la web asociada al HTML, a través de una imagen Docker de Apache o si estamos utilizando VSCode, a través de la extensión Live Server, por ejemplo. 

Lanzada la aplicación web si la abrimos en Google Chrome donde previamente se ha tenido que instalar la extensión de DWARF (https://chrome.google.com/webstore/detail/cc%20%20-devtools-support-dwa/pdcpmagijalfljmkmjngeonclgbbannb) podremos ver algo como lo siguiente si abrimos la consola:

En la imagen anterior se puede observar que los ficheros asociados a C++ se encuentran visibles y, no solo eso, si no que se podrá interactuar con ellos, por ejemplo, poniendo puntos de ruptura (break points) para ir siguiendo la ejecución paso por paso, viendo valores de variables, funciones, etc, tal  y como se comentó con anterioridad.

Además de la opción de depuración también se podrán realizar otras funciones que permiten las Chrome DevTools como el profiling.

Pruebas unitarias (Unit Testing)

Las pruebas unitarias o unit testing son una forma de comprobar que un fragmento de código funciona correctamente. Éstas consisten en aislar una parte del código (por ej. una función, un método, una clase, un módulo) y comprobar que funciona como se espera.

Las pruebas unitarias facilitan que el programador cambie el código para mejorar su estructura​ (refactorización), puesto que permiten hacer pruebas sobre los cambios y así asegurarse de que los nuevos cambios no han introducido defectos en características que ya funcionaban correctamente.

Las funciones escritas en C++ se pueden testear usando frameworks bien conocidos como “Catch2” o “Google Test”, pero, ¿estamos seguros de que esas funciones compiladas con Emscripten y ejecutadas en un navegador, siguen cumpliendo nuestras expectativas?. 

Para resolver esa duda vamos a ejecutar las pruebas unitarias como si fueran funciones escritas en JavaScript, ya preparadas para ejecutarse en un navegador. Para ello utilizaremos Jest[6], un clásico framework para pruebas JavaScript.

Antes de comenzar destacar que será necesario tener instalado Node.js. Para comprobar que está instalado ejecutar:

</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>node --version</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>

En el caso de que no se tenga respuesta sobre la versión instalar node a través del siguiente comando:

</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>sudo apt install nodejs</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>

Con Node instalado, el primer paso que se debe hacer es inicializar nuestro proyecto:

</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>npm init </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>

El comando anterior nos creará un fichero llamado package.json.

Seguidamente, se instalará Jest, con el siguiente comando, dónde se crearán todas las carpetas necesarias para la ejecución de Jest.

</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>npm install --save-dev Jest</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>

Solo nos falta añadir a nuestro fichero package.json lo siguiente:

 «scripts»: {

   «test»: «jest»

 },

Un ejemplo de cómo quedaría este fichero sería:

{

 «type»: «module»,

 «devDependencies»: {

   «jest»: «^29.5.0»

 },

 «scripts»: {

   «test»: «jest»

 }

}

Las funciones que queremos testear se encuentran en nuestro fichero unitTest.cpp que tiene el siguiente aspecto:

  • unitTest.cpp

#include <emscripten/bind.h>

#include <emscripten.h>

#include <string>

#include <iostream>

int EMSCRIPTEN_KEEPALIVE addInt(int a, int b) asm(«addInt»);

int addInt(int a, int b) {

 return a + b;

}

int EMSCRIPTEN_KEEPALIVE multInt(int a, int b) asm(«multInt»);

int multInt(int a, int b) {

 return a * b;

}

int  EMSCRIPTEN_KEEPALIVE square(int a) asm(«square»);

int  square(int a) {

 return a * a;

}

Destacar que no queremos hacer el test sobre este fichero, sino sobre el .wasm creado una vez compilado con Emscripten, para compilar se puede ejecutar cualquiera de los siguientes comandos:

Con Emscripten instalado localmente:

</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>emcc unitTest.cpp -o unitTest.wasm</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>

Con la imagen Docker de Emscripten se ejecutaría:

</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>docker run --rm -v $(pwd):/src -u $(id -u):$(id -g) emscripten/emsdk:3.1.36 emcc unitTest.cpp -o unitTest.wasm</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>

La siguiente tarea será definir las pruebas. Esto lo haremos en un fichero .js que necesariamente deberemos incluir en un nuevo directorio denominado “ __test__”  dentro de nuestra carpeta de proyecto.

Como ya dijimos anteriormente, no queremos testear los ficheros que contienen las funciones en C++, ni siquiera usaremos el fichero .js que genera Emscripten, cargaremos el módulo desde el fichero .wasm y haremos los test, sobre el.

El citado fichero de test, tiene el siguiente aspecto:

  • __test__/index.test.js

const fs = require(‘fs’);

let nath;

const env = {

   memoryBase: 0,

   tableBase: 0,

   memory: new WebAssembly.Memory({

     initial: 256

   }),

   table: new WebAssembly.Table({

     initial: 0,

     element: ‘anyfunc’

   })

 }

beforeAll(async () => {

   const mathWasm = fs.readFileSync(‘./unitTest.wasm’);

   math = await WebAssembly.instantiate(new Uint8Array(mathWasm),{env: env})

                   .then(result => result.instance.exports);

})

test(‘Add operation 6 + 30 = 36’, () => expect(math.addInt(6,30)).toBe(36) );

test(‘Add operation 2 + 2  =  4’, () => expect(math.addInt(2,2)).toBe(4) );

test(‘Mult operation  9 * 2 = 18’, () => expect(math.multInt(9,2)).toBe(18) );

test(‘Square operation 5 = 25’, () => expect(math.square(5)).toBe(25) );

A destacar cómo se hace referencia al fichero wasm, y cómo se crea una instancia del módulo antes de realizar los test propiamente dichos. Al final del fichero podemos ver cómo se construyen los test.

Para pasar los test tan solo deberemos escribir en el terminal  “npm test”. Tras esto se mostrarán los resultados por el terminal.

> test

> jest

PASS __test__/index.test.js

  ✓ Add operation 6 + 30 = 36 (2 ms)

  ✓ Add operation 2 + 2  =  4 (1 ms)

  ✓ Mult operation  9 * 2 = 18

  ✓ Square operation 5 = 25

Test Suites: 1 passed, 1 total

Tests:       4 passed, 4 total

Snapshots:   0 total

Time:        0.294 s

Ran all test suites.

Conclusión 

En este post hemos podido ver algunos ejemplos que nos introducen al mundo de WebAssembly, en concreto a cómo dar nuestros primeros pasos con esta tecnología, con el objetivo de partir de una aplicación desarrollada en C++, que será posteriormente integrada en una página web para ser ejecutada por un navegador moderno. Además, se ha introducido cómo trabajar con un depurador y cómo hacer test unitarios con WebAssembly, acciones que nos pueden ser bastante útiles cuando se está desarrollando una aplicación web con esta tecnología.

Referencias 

[1] https://emscripten.org/

[2] https://hub.docker.com/r/emscripten/emsdk

[3] https://medium.com/wasm/webassembly-wasm-runtimes-522bcc7478fd

[4] https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html#

[5] https://chrome.google.com/webstore/detail/cc%20%20-devtools-support-dwa/pdcpmagijalfljmkmjngeonclgbbannb

[6] https://jestjs.io/

Compartir en:

Relacionados