Vincula JS a tu wasm.
En mi último artículo sobre wasm, hablé sobre cómo compilar una biblioteca C en wasm para que puedas usarla en la Web. Una cosa que me llamó la atención (y a muchos lectores) es la forma cruda y un poco incómoda en la que tienes que declarar manualmente qué funciones de tu módulo wasm estás usando. Para refrescar tu memoria, este es el fragmento de código del que hablo:
const api = {
version: Module.cwrap('version', 'number', []),
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};
Aquí declaramos los nombres de las funciones que marcamos con EMSCRIPTEN_KEEPALIVE
, cuáles son sus tipos de devolución y cuáles son los tipos de sus argumentos. Luego, podemos usar los métodos del objeto api
para invocar estas funciones. Sin embargo, usar wasm de esta manera no admite cadenas y requiere que muevas manualmente fragmentos de memoria, lo que hace que muchas APIs de bibliotecas sean muy tediosas de usar. ¿No hay una mejor manera? Sí, de lo contrario, ¿de qué trataría este artículo?
Codificación de nombres en C++
Si bien la experiencia del desarrollador sería motivo suficiente para crear una herramienta que ayude con estas vinculaciones, en realidad hay un motivo más apremiante: cuando compilas código en C o C++, cada archivo se compila por separado. Luego, un vinculador se encarga de combinar todos estos archivos objeto y convertirlos en un archivo wasm. Con C, los nombres de las funciones siguen disponibles en el archivo de objeto para que el vinculador los use. Todo lo que necesitas para llamar a una función en C es el nombre, que proporcionamos como una cadena a cwrap()
.
Por otro lado, C++ admite la sobrecarga de funciones, lo que significa que puedes implementar la misma función varias veces, siempre y cuando la firma sea diferente (p.ej., parámetros con diferentes tipos). A nivel del compilador, un nombre descriptivo como add
se codificaría en algo que codifica la firma en el nombre de la función para el vinculador. Como resultado, ya no podríamos buscar nuestra función por su nombre.
Ingresa embind
embind forma parte de la cadena de herramientas de Emscripten y te proporciona una gran cantidad de macros de C++ que te permiten anotar código C++. Puedes declarar qué funciones, enumeraciones, clases o tipos de valores planeas usar desde JavaScript. Comencemos con algunas funciones simples:
#include <emscripten/bind.h>
using namespace emscripten;
double add(double a, double b) {
return a + b;
}
std::string exclaim(std::string message) {
return message + "!";
}
EMSCRIPTEN_BINDINGS(my_module) {
function("add", &add);
function("exclaim", &exclaim);
}
En comparación con mi artículo anterior, ya no incluimos emscripten.h
, ya que no tenemos que anotar nuestras funciones con EMSCRIPTEN_KEEPALIVE
.
En su lugar, tenemos una sección EMSCRIPTEN_BINDINGS
en la que enumeramos los nombres con los que queremos exponer nuestras funciones a JavaScript.
Para compilar este archivo, podemos usar la misma configuración (o, si lo deseas, la misma imagen de Docker) que en el artículo anterior. Para usar embind, agregamos la marca --bind
:
$ emcc --bind -O3 add.cpp
Ahora, solo queda crear un archivo HTML que cargue nuestro módulo wasm recién creado:
<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
console.log(Module.add(1, 2.3));
console.log(Module.exclaim("hello world"));
};
</script>
Como puedes ver, ya no usamos cwrap()
. Esto funciona de inmediato. Pero, lo que es más importante, no tenemos que preocuparnos por copiar manualmente fragmentos de memoria para que funcionen las cadenas. embind te ofrece eso de forma gratuita, junto con verificaciones de tipos:

Esto es muy útil, ya que podemos detectar algunos errores de forma anticipada en lugar de lidiar con los errores de wasm, que a veces son bastante difíciles de manejar.
Objetos
Muchos constructores y funciones de JavaScript usan objetos de opciones. Es un patrón agradable en JavaScript, pero extremadamente tedioso de implementar en wasm de forma manual. ¡Embind también puede ayudar aquí!
Por ejemplo, se me ocurrió esta función de C++ increíblemente útil que procesa mis cadenas, y quiero usarla en la Web con urgencia. Así es como lo hice:
#include <emscripten/bind.h>
#include <algorithm>
using namespace emscripten;
struct ProcessMessageOpts {
bool reverse;
bool exclaim;
int repeat;
};
std::string processMessage(std::string message, ProcessMessageOpts opts) {
std::string copy = std::string(message);
if(opts.reverse) {
std::reverse(copy.begin(), copy.end());
}
if(opts.exclaim) {
copy += "!";
}
std::string acc = std::string("");
for(int i = 0; i < opts.repeat; i++) {
acc += copy;
}
return acc;
}
EMSCRIPTEN_BINDINGS(my_module) {
value_object<ProcessMessageOpts>("ProcessMessageOpts")
.field("reverse", &ProcessMessageOpts::reverse)
.field("exclaim", &ProcessMessageOpts::exclaim)
.field("repeat", &ProcessMessageOpts::repeat);
function("processMessage", &processMessage);
}
Estoy definiendo una struct para las opciones de mi función processMessage()
. En el bloque EMSCRIPTEN_BINDINGS
, puedo usar value_object
para que JavaScript vea este valor de C++ como un objeto. También podría usar value_array
si prefiriera usar este valor de C++ como un array. También vinculo la función processMessage()
, y el resto es magia de embind. Ahora puedo llamar a la función processMessage()
desde JavaScript sin ningún código estándar:
console.log(Module.processMessage(
"hello world",
{
reverse: false,
exclaim: true,
repeat: 3
}
)); // Prints "hello world!hello world!hello world!"
Clases
Para completar la explicación, también debería mostrarte cómo embind te permite exponer clases completas, lo que genera mucha sinergia con las clases de ES6. Probablemente, ya puedas ver un patrón:
#include <emscripten/bind.h>
#include <algorithm>
using namespace emscripten;
class Counter {
public:
int counter;
Counter(int init) :
counter(init) {
}
void increase() {
counter++;
}
int squareCounter() {
return counter * counter;
}
};
EMSCRIPTEN_BINDINGS(my_module) {
class_<Counter>("Counter")
.constructor<int>()
.function("increase", &Counter::increase)
.function("squareCounter", &Counter::squareCounter)
.property("counter", &Counter::counter);
}
En el lado de JavaScript, esto se siente casi como una clase nativa:
<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
const c = new Module.Counter(22);
console.log(c.counter); // prints 22
c.increase();
console.log(c.counter); // prints 23
console.log(c.squareCounter()); // prints 529
};
</script>
¿Qué pasa con la letra C?
embind se escribió para C++ y solo se puede usar en archivos C++, pero eso no significa que no puedas vincularte con archivos C. Para combinar C y C++, solo debes separar tus archivos de entrada en dos grupos: uno para los archivos de C y otro para los archivos de C++, y aumentar las marcas de la CLI para emcc
de la siguiente manera:
$ emcc --bind -O3 --std=c++11 a_c_file.c another_c_file.c -x c++ your_cpp_file.cpp
Conclusión
embind ofrece grandes mejoras en la experiencia del desarrollador cuando se trabaja con wasm y C/C++. Este artículo no abarca todas las opciones que ofrece embind. Si te interesa, te recomiendo que sigas con la documentación de embind. Ten en cuenta que usar embind puede aumentar el tamaño de tu módulo wasm y tu código de vinculación de JavaScript hasta en 11 KB cuando se comprimen con gzip, sobre todo en módulos pequeños. Si solo tienes una superficie de wasm muy pequeña, embind podría costar más de lo que vale en un entorno de producción. Sin embargo, deberías probarlo.