Emscripten

Ele vincula o JS ao Wasm.

No meu último artigo Wasm, falei sobre como compilar uma biblioteca C para o Wasm para que você possa usá-la na Web. Uma coisa que se destacou para mim (e para muitos leitores) foi a maneira grosseira e ligeiramente constrangedora você precisa declarar manualmente quais funções do módulo Wasm estão usando. Para refrescar a mente, este é o snippet de código do qual estou falando:

const api = {
    version: Module.cwrap('version', 'number', []),
    create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
    destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};

Aqui declaramos os nomes das funções marcadas com EMSCRIPTEN_KEEPALIVE, quais são os tipos de retorno e os tipos dos argumentos são. Em seguida, podemos usar os métodos no objeto api para invocar essas funções. No entanto, usar o Wasm dessa forma não oferece suporte a strings e exige a movimentação manual de blocos de memória, o que torna as APIs são muito cansativas de usar. Não existe uma maneira melhor? Por que sim. Caso contrário? qual é o assunto deste artigo?

Mangling de nome C++

Embora a experiência do desenvolvedor seja motivo suficiente para criar uma ferramenta que ajude com essas vinculações, há um motivo mais importante: ao compilar C ou C++, cada arquivo é compilado separadamente. Em seguida, um vinculador cuida reunindo todos os chamados arquivos de objeto e transformando-os em um Wasm . Com C, os nomes das funções ainda ficam disponíveis no arquivo de objeto que serão usadas pelo vinculador. Tudo que você precisa para chamar uma função C é o nome, que estamos fornecendo como uma string para cwrap().

Por outro lado, o C++ oferece suporte à sobrecarga de funções, o que significa que é possível implementar a mesma função várias vezes, desde que a assinatura seja diferente (por exemplo, parâmetros com tipos diferentes). No nível do compilador, um nome bom como add seria corrompido em algo que codificasse a assinatura na função nome do vinculador. Como resultado, não poderíamos pesquisar nossa função por seu nome.

Digite "embind"

embind (em inglês) faz parte do conjunto de ferramentas Emscripten e fornece várias macros C++ que permitem anotar códigos C++. É possível declarar quais funções, tipos enumerados classes ou tipos de valor que você planeja usar do JavaScript. Vamos começar com algumas funções 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);
}

Em comparação com meu artigo anterior, não estamos mais incluindo emscripten.h, pois não precisamos mais anotar nossas funções com EMSCRIPTEN_KEEPALIVE. Em vez disso, temos uma seção EMSCRIPTEN_BINDINGS em que listamos os nomes em e queremos expor nossas funções ao JavaScript.

Para compilar esse arquivo, podemos usar a mesma configuração (ou, se você quiser, a mesma do Docker) como na etapa anterior artigo. Para usar embind, adicionamos a sinalização --bind:

$ emcc --bind -O3 add.cpp

Agora só falta criar um arquivo HTML que carregue nossos arquivos criou o módulo 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 você pode ver, não estamos mais usando cwrap(). Isso funciona perfeitamente da caixa. Mas, o mais importante, não precisamos nos preocupar em copiar manualmente pedaços de memória para fazer as strings funcionarem! embind oferece isso, sem custo financeiro, junto com verificações de tipo:

Erros do DevTools quando você invoca uma função com o número errado de argumentos
ou os argumentos estiverem errados
tipo

Isso é ótimo, pois podemos detectar alguns erros mais cedo em vez de lidar com erros do Wasm que às vezes são bastante complicados.

Objetos

Muitos construtores e funções JavaScript usam objetos de opções. É uma boa em JavaScript, mas extremamente tedioso de perceber no Wasm manualmente. vincular também pode ajudar aqui.

Por exemplo, criei esta função C++ incrivelmente útil que processa meus strings e quero usá-lo urgentemente na Web. Eu fiz isso da seguinte forma:

#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);
}

Estou definindo um struct para as opções da minha função processMessage(). Na bloco EMSCRIPTEN_BINDINGS, posso usar value_object para fazer o JavaScript ver esse valor C++ como um objeto. Também posso usar value_array se preferir use esse valor em C++ como uma matriz. Também vinculo a função processMessage(). o restante é uma mágica integrada. Agora, posso chamar a função processMessage() do JavaScript sem código boilerplate:

console.log(Module.processMessage(
    "hello world",
    {
    reverse: false,
    exclaim: true,
    repeat: 3
    }
)); // Prints "hello world!hello world!hello world!"

Classes

Para garantir a completude, também preciso mostrar como o Embind permite expor classes inteiras, o que traz muita sinergia com as classes ES6. Você provavelmente pode começar a ver um padrão agora:

#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);
}

No lado do JavaScript, isso parece uma 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 C?

embind foi escrito para C++ e só pode ser usado em arquivos C++, mas isso não ou seja, não é possível criar links para arquivos C. Para combinar C e C++, você só precisa separe seus arquivos de entrada em dois grupos: um para arquivos C e outro para arquivos C++ e aumente as sinalizações da CLI para emcc da seguinte maneira:

$ emcc --bind -O3 --std=c++11 a_c_file.c another_c_file.c -x c++ your_cpp_file.cpp

Conclusão

O embind oferece ótimas melhorias na experiência do desenvolvedor ao trabalhar com Wasm e C/C++. Este artigo não abrange todas as opções de associação de ofertas. Se você tiver interesse, recomendo continuar com a configuração Documentação. Lembre-se de que usar embind pode tornar seu módulo Wasm e seu Agrupa código JavaScript com um tamanho de até 11 KB quando é compactado com gzip, principalmente em projetos pequenos módulos. Se você tiver apenas uma superfície de Wasm muito pequena, o embind poderá custar mais do que vale a pena em um ambiente de produção. No entanto, você deve definitivamente dar tente.