Emscripten's embind

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:

Errori DevTools quando richiami una funzione con il numero errato di argomenti
o gli argomenti presentano
tipo

È 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. Tieni presente che l'utilizzo di embind può rendere sia il modulo wasm 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.