¡Vincula JS a tu wasm!
En mi último artículo de Wasm, hablé sobre cómo compilar una biblioteca de C en wasm para poder usarla en la Web. Una cosa que me llamó la atención (y para muchos lectores) es la forma grosera y un poco incómoda debes declarar manualmente qué funciones del módulo de wasm usas. Para que lo recuerdes, este es el fragmento de código al que me refiero:
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 datos que se muestran y cuáles son 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 bibliotecas
Las APIs son muy tediosas de usar. ¿No hay una manera mejor? ¿Por qué sí?, de lo contrario
¿de qué trataría este artículo?
Modificación de nombres de C++
Aunque la experiencia del desarrollador es suficiente para crear una herramienta que ayude
estas vinculaciones, hay una razón más apremiante: cuando compilas C
o C++, cada archivo se compila por separado. Luego, un vinculador se encarga de
organizando todos estos archivos
de objetos y convirtiéndolos en un archivo wasm
. Con C, los nombres de las funciones aún están disponibles en el archivo de objeto
para que use el vinculador. Todo lo que necesitas para llamar a una función C es el nombre,
que proporcionamos como una cadena a cwrap()
.
C++, por otro lado, admite la sobrecarga de funciones, lo que significa que puedes implementar
la misma función varias veces, siempre que la firma sea diferente (p.ej.,
parámetros con tipos diferentes). En el nivel del compilador, un nombre agradable como add
se modificaría en algo que codifique la firma en la función
nombre del vinculador. Como resultado, no podríamos buscar nuestra función
por su nombre.
Ingresar embind
embinar es parte de la cadena de herramientas de Emscripten y te proporciona un montón de macros de C++ que te permiten anotar código C++. Puedes declarar qué funciones, enumeraciones, clases o tipos de valores que planeas usar de JavaScript. Comencemos simple con algunas funciones sin formato:
#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
ya no tenemos que anotar nuestras funciones con EMSCRIPTEN_KEEPALIVE
.
En su lugar, tenemos una sección EMSCRIPTEN_BINDINGS
en la que mostramos los nombres en
con el que queremos exponer nuestras funciones a JavaScript.
Para compilar este archivo, podemos usar la misma configuración (o, si quieres, la misma
imagen de Docker) como en la imagen anterior
artículo. Para usar embind,
agregamos la marca --bind
:
$ emcc --bind -O3 add.cpp
Ahora solo falta crear un archivo HTML que cargue nuestro archivo Se creó el módulo de Wasm:
<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 simplemente funciona
de la caja. Pero lo más importante es que no tenemos que preocuparnos por copiar manualmente
memoria para que las cadenas funcionen. embind te da eso de forma gratuita, junto con
con verificaciones de tipo:
Esto es muy útil, ya que podemos detectar algunos errores con anticipación en lugar de lidiar con los errores ocasionalmente difíciles de manejar.
Objetos
Muchos constructores y funciones de JavaScript usan objetos de opciones. Es un en JavaScript, pero es muy tedioso de comprenderlo manualmente. embind también puede ser de ayuda aquí.
Por ejemplo, se me ocurrió esta función de C++ increíblemente útil que procesa mis y quiero usarlas con urgencia en la Web. 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 un struct para las opciones de la función processMessage()
. En la
EMSCRIPTEN_BINDINGS
, puedo usar value_object
para que JavaScript pueda ver
este valor de C++ como un objeto. También podría usar value_array
si así lo quisiera
usa este valor de C++ como un array. También enlace la función processMessage()
, y
el resto es magia. Ahora puedo llamar a la función processMessage()
desde
JavaScript sin 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 ser exhaustivo, también debo mostrarte cómo embind te permite exponer a clases enteras, lo que aporta mucha sinergia a las clases de ES6. Probablemente puedas empezar a ver un patrón a esta altura:
#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 casi parece 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é sucede con C?
embind se escribió para C++ y solo puede usarse en archivos C++, pero eso no es obligatorio.
esto significa que no puede vincularse con archivos C. Para combinar C y C++, solo necesitas
separar tus archivos de entrada en dos grupos: uno para C y otro para C++, y
aumenta 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 brinda grandes mejoras en la experiencia de los desarrolladores a la hora de trabajar con wasm y C/C++. En este artículo, no se abordan todas las opciones que ofrece. Si te interesa, te recomiendo que continúes con embind's documentación. Ten en cuenta que usar embind puede hacer que tanto el módulo wasm como El tamaño del código de adhesión de JavaScript es de hasta 11,000 cuando se comprime con gzip, sobre todo en archivos pequeños. módulos. Si solo tienes una superficie de Wasm muy pequeña, embind podría costar más de que vale la pena en un entorno de producción. No obstante, definitivamente debes dar una prueba.