Collega JS al tuo WASM.
Nel mio ultimo articolo su Wasm, ho parlato di come compilare una libreria C in Wasm in modo da poterla utilizzare sul web. Una cosa che mi ha colpito (e molti lettori) è il modo grezzo e un po' goffo in cui devi dichiarare manualmente quali funzioni del tuo modulo Wasm stai utilizzando. Per rinfrescarti la memoria, ecco lo snippet di codice di cui parlo:
const api = {
version: Module.cwrap('version', 'number', []),
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};
Qui dichiariamo i nomi delle funzioni che abbiamo contrassegnato con
EMSCRIPTEN_KEEPALIVE
, i loro tipi di ritorno e i tipi dei loro
argomenti. Successivamente, possiamo utilizzare i metodi sull'oggetto api
per richiamare
queste funzioni. Tuttavia, l'utilizzo di wasm in questo modo non supporta le stringhe e
richiede di spostare manualmente i blocchi di memoria, il che rende molto noioso l'utilizzo di molte API
delle librerie. Non esiste un modo migliore? Certo che sì, altrimenti
di cosa parlerebbe questo articolo?
C++ name mangling
Sebbene l'esperienza degli sviluppatori sia un motivo sufficiente per creare uno strumento che aiuti
con questi binding, in realtà esiste un motivo più urgente: quando compili codice C
o C++, ogni file viene compilato separatamente. Poi, un linker si occupa di
combinare tutti questi cosiddetti file oggetto e trasformarli in un file
wasm. Con C, i nomi delle funzioni sono ancora disponibili nel file oggetto
per l'utilizzo da parte del linker. Tutto ciò che ti serve per poter chiamare una funzione C è il nome,
che forniamo come stringa a cwrap()
.
C++ supporta invece l'overload delle funzioni, il che significa che puoi implementare
la stessa funzione più volte, purché la firma sia diversa (ad es.
parametri di tipo diverso). A livello di compilatore, un nome semplice come add
verrebbe modificato in modo da codificare la firma nel nome della funzione
per il linker. Di conseguenza, non potremmo più cercare la nostra funzione
con il suo nome.
Inserisci embind
embind fa parte della toolchain Emscripten e fornisce una serie di macro C++ che consentono di annotare il codice C++. Puoi dichiarare quali funzioni, enumerazioni, classi o tipi di valori prevedi di utilizzare da JavaScript. Iniziamo in modo semplice con alcune funzioni di base:
#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);
}
Rispetto al mio articolo precedente, non includiamo più emscripten.h
, in quanto
non dobbiamo più annotare le nostre funzioni con EMSCRIPTEN_KEEPALIVE
.
Abbiamo invece una sezione EMSCRIPTEN_BINDINGS
in cui elenchiamo i nomi con cui vogliamo esporre le nostre funzioni a JavaScript.
Per compilare questo file, possiamo utilizzare la stessa configurazione (o, se vuoi, la stessa
immagine Docker) dell'articolo
precedente. Per utilizzare embind,
aggiungiamo il flag --bind
:
$ emcc --bind -O3 add.cpp
Ora non resta che creare un file HTML che carichi il modulo Wasm appena creato:
<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
console.log(Module.add(1, 2.3));
console.log(Module.exclaim("hello world"));
};
</script>
Come puoi vedere, non utilizziamo più cwrap()
. Funziona subito. Ma, soprattutto, non dobbiamo preoccuparci di copiare manualmente
blocchi di memoria per far funzionare le stringhe. embind ti offre tutto questo senza costi,
insieme ai controlli dei tipi:

È un'ottima cosa perché possiamo rilevare alcuni errori in anticipo, invece di dover gestire gli errori wasm, a volte piuttosto difficili da gestire.
Oggetti
Molti costruttori e funzioni JavaScript utilizzano oggetti di opzioni. È un bel pattern in JavaScript, ma estremamente noioso da realizzare manualmente in wasm. Anche embind può essere d'aiuto in questo caso.
Ad esempio, ho creato questa funzione C++ incredibilmente utile che elabora le mie stringhe e voglio usarla urgentemente sul web. Ecco come ho fatto:
#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);
}
Sto definendo uno struct per le opzioni della mia funzione processMessage()
. Nel blocco
EMSCRIPTEN_BINDINGS
posso utilizzare value_object
per fare in modo che JavaScript consideri
questo valore C++ come un oggetto. Potrei anche utilizzare value_array
se preferissi
utilizzare questo valore C++ come array. Associo anche la funzione processMessage()
e
il resto è magia di embind. Ora posso chiamare la funzione processMessage()
da
JavaScript senza alcun codice boilerplate:
console.log(Module.processMessage(
"hello world",
{
reverse: false,
exclaim: true,
repeat: 3
}
)); // Prints "hello world!hello world!hello world!"
Corsi
Per completezza, devo anche mostrarti come embind ti consente di esporre intere classi, il che porta a una grande sinergia con le classi ES6. Probabilmente ormai avrai notato un pattern:
#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);
}
Dal lato JavaScript, questa classe sembra quasi 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>
E C?
embind è stato scritto per C++ e può essere utilizzato solo nei file C++, ma ciò non
significa che non puoi collegarti ai file C. Per combinare C e C++, devi solo
separare i file di input in due gruppi: uno per i file C e uno per i file C++ e
aumentare i flag della CLI per emcc
come segue:
$ emcc --bind -O3 --std=c++11 a_c_file.c another_c_file.c -x c++ your_cpp_file.cpp
Conclusione
embind offre notevoli miglioramenti nell'esperienza degli sviluppatori quando lavorano con wasm e C/C++. Questo articolo non copre tutte le opzioni offerte da embind. Se ti interessa, ti consiglio di continuare con la documentazione di embind. Tieni presente che l'utilizzo di embind può aumentare le dimensioni del modulo Wasm e del codice di collegamento JavaScript fino a 11 KB quando viene compresso con gzip, in particolare nei moduli di piccole dimensioni. Se hai solo una superficie wasm molto piccola, embind potrebbe costare più di quanto valga in un ambiente di produzione. Tuttavia, dovresti assolutamente provarla.