Associa JS al tuo wasm.
Nel mio ultimo articolo su Wasm ho parlato su come compilare una libreria C su wasm per usarla sul web. Una cosa che secondo me (e per molti lettori) è il modo grezzo e un po' imbarazzante devi dichiarare manualmente quali funzioni del modulo wasm stai utilizzando. Per ripensarti, questo è lo snippet di codice di cui sto parlando:
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 contrassegnate
EMSCRIPTEN_KEEPALIVE
, quali sono i tipi restituiti e quali sono i tipi
. In seguito, possiamo usare i metodi sull'oggetto api
per richiamare
queste funzioni. Tuttavia, l'utilizzo di wasm in questo modo non supporta le stringhe e
richiede lo spostamento manuale di blocchi di memoria, il che rende
API molto noiose da usare. Non c'è un modo migliore? Perché sì, altrimenti
di cosa parlerebbe questo articolo?
Gestione nomi C++
Sebbene l'esperienza degli sviluppatori sia sufficiente per creare uno strumento che aiuti
con queste associazioni, c'è un motivo più urgente: quando compili C
o C++, ogni file viene compilato separatamente. Poi, un linker si occupa
distruggendo tutti questi cosiddetti file oggetto e trasformandoli in una vaga
. Con C, i nomi delle funzioni sono ancora disponibili nel file oggetto
che il linker può utilizzare. Per chiamare una funzione C, basta
il nome,
che forniamo come stringa a cwrap()
.
C++, invece, supporta il sovraccarico delle funzioni, il che significa che puoi implementare
la stessa funzione più volte purché la firma sia diversa (ad esempio,
parametri digitati in modo diverso). A livello di compilatore, un bel nome come add
otterrebbe mangled in qualcosa che codifica la firma nella funzione
del linker. Di conseguenza, non potremmo cercare la nostra funzione
con il suo nome.
Inserisci embind
embind fa parte della toolchain di Emscripten e fornisce una serie di macro C++ che ti consentono di annotare codice C++. Puoi dichiarare quali funzioni, enum classi o tipi di valore che prevedi di utilizzare da JavaScript. Iniziamo con alcune semplici funzioni:
#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
.
È invece disponibile una sezione EMSCRIPTEN_BINDINGS
in cui sono elencati i nomi
per cui vogliamo esporre le funzioni in JavaScript.
Per compilare questo file, possiamo usare la stessa configurazione (o, se vuoi, la stessa
Docker) come nella precedente
. Per utilizzare embind,
aggiungiamo il flag --bind
:
$ emcc --bind -O3 add.cpp
A questo punto, abbiamo creato un file HTML che carichi le nostre modulo wasm 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
all'interno della confezione. Ma soprattutto, non dobbiamo preoccuparci di copiare manualmente
blocchi di memoria per far funzionare le stringhe! embind ti offre senza costi, oltre
con i controlli dei tipi:
È un'ottima cosa, perché possiamo individuare alcuni errori in anticipo, invece di occuparci gli errori wasm a volte piuttosto ingombranti.
Oggetti
Molti costruttori e funzioni JavaScript utilizzano oggetti opzioni. È una bella in JavaScript, ma estremamente noioso da realizzare manualmente in wasm. Embind può essere di aiuto anche in questo caso.
Ad esempio, ho trovato questa funzione C++ incredibilmente utile che elabora i miei 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()
. Nella
Blocco EMSCRIPTEN_BINDINGS
, posso usare value_object
per far vedere a JavaScript
come oggetto. Potrei anche usare value_array
se preferisco
utilizza questo valore C++ come array. Collego anche la funzione processMessage()
e
il resto è magico. Ora posso chiamare la funzione processMessage()
da
JavaScript senza 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 crea molta sinergia con le classi ES6. Probabilmente puoi inizia a vedere un pattern ormai:
#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);
}
Sul lato JavaScript, sembra quasi una classe 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 la C?
embind è stato scritto per C++ e può essere utilizzato solo nei file C++,
significa che non è possibile creare link a file C! Per mescolare C e C++, basta
separa i file di input in due gruppi: uno per C e uno per i file C++.
aumenta i flag dell'interfaccia a riga di comando 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 grandi miglioramenti nell'esperienza degli sviluppatori durante il lavoro con wasm e C/C++. Questo articolo non tratta tutte le opzioni relative alle offerte. Se ti interessa, ti consiglio di continuare con embind's documentazione. Ricorda che l'uso di embind può rendere sia il modulo wasm sia il tuo Il codice glue code JavaScript è maggiore fino a 11 k con gzip, soprattutto nelle dimensioni moduli. Se hai solo una superficie wasm molto piccola, embind potrebbe costare più di in un ambiente di produzione. Ciononostante, devi sicuramente provarci.