Emscripten

Ele vincula o JS ao seu WASM.

No meu último artigo sobre wasm, falei sobre como compilar uma biblioteca C para wasm para que você possa usá-la na Web. Uma coisa que me chamou a atenção (e a muitos leitores) é a maneira grosseira e um pouco estranha de declarar manualmente quais funções do módulo wasm você está usando. Para refrescar sua memória, este é o snippet de código:

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, os tipos de retorno e os tipos dos argumentos. Depois, podemos usar os métodos no objeto api para invocar essas funções. No entanto, o uso do WASM dessa forma não oferece suporte a strings e exige que você mova manualmente os pedaços de memória, o que torna muitas APIs de biblioteca muito cansativas de usar. Não existe uma maneira melhor? Sim, há. Caso contrário, sobre o que seria este artigo?

Mangling de nome em C++

Embora a experiência do desenvolvedor seja motivo suficiente para criar uma ferramenta que ajude com essas vinculações, há um motivo mais urgente: quando você compila código C ou C++, cada arquivo é compilado separadamente. Em seguida, um vinculador cuida de juntar todos esses arquivos de objeto e transformá-los em um arquivo wasm. Com C, os nomes das funções ainda estão disponíveis no arquivo de objeto para uso pelo vinculador. Tudo o que você precisa para chamar uma função C é o nome, que estamos fornecendo como uma string para cwrap().

O C++ oferece suporte à sobrecarga de funções, o que significa que você pode implementar a mesma função várias vezes, desde que a assinatura seja diferente (por exemplo, parâmetros de tipos diferentes). No nível do compilador, um nome legal como add seria modificado em algo que codifica a assinatura no nome da função para o vinculador. Como resultado, não seria mais possível procurar nossa função pelo nome.

Entrar em embind

embind faz parte da cadeia de ferramentas do Emscripten e oferece várias macros C++ que permitem anotar código C++. É possível declarar quais funções, tipos enumerados, classes ou tipos de valor você planeja usar no 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 incluímos mais emscripten.h, já que 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 que queremos expor nossas funções para JavaScript.

Para compilar esse arquivo, podemos usar a mesma configuração (ou, se preferir, a mesma imagem do Docker) do artigo anterior. Para usar o embind, adicione a flag --bind:

$ emcc --bind -O3 add.cpp

Agora, tudo o que resta é criar um arquivo HTML que carregue nosso módulo wasm recém-criado:

<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 usamos mais cwrap(). Isso funciona imediatamente. Mas o mais importante é que não precisamos nos preocupar em copiar manualmente partes da memória para fazer com que as strings funcionem. O embind oferece isso sem custo financeiro, com verificações de tipo:

Erros do DevTools ao invocar uma função com o número errado de argumentos
ou quando os argumentos têm o tipo
errado.

Isso é ótimo, porque podemos detectar alguns erros com antecedência em vez de lidar com os erros do Wasm, que às vezes são bastante difíceis de usar.

Objetos

Muitos construtores e funções do JavaScript usam objetos de opções. É um padrão legal em JavaScript, mas extremamente tedioso de realizar manualmente no Wasm. O embind também pode ajudar aqui.

Por exemplo, criei essa função C++ incrivelmente útil que processa minhas strings, e quero usá-la com urgência na Web. Veja como fiz isso:

#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(). No bloco EMSCRIPTEN_BINDINGS, posso usar value_object para fazer com que o JavaScript veja esse valor do C++ como um objeto. Também poderia usar value_array se preferisse usar esse valor C++ como uma matriz. Também vinculo a função processMessage(), e o restante é mágico. Agora posso chamar a função processMessage() do JavaScript sem nenhum código boilerplate:

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

Classes

Para completar, também preciso mostrar como o embind permite expor classes inteiras, o que traz muita sinergia com as classes ES6. Provavelmente, você já começou a perceber um padrão:

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

Do lado do JavaScript, isso quase 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 a C?

O embind foi escrito para C++ e só pode ser usado em arquivos C++, mas isso não significa que não é possível vincular arquivos C. Para misturar C e C++, basta separar os arquivos de entrada em dois grupos: um para C e outro para C++ e aumentar as flags 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 grandes melhorias na experiência do desenvolvedor ao trabalhar com wasm e C/C++. Este artigo não aborda todas as opções oferecidas pelo embind. Se você tiver interesse, recomendamos continuar com a documentação do embind. O uso de embind pode aumentar o módulo wasm e o código de união JavaScript em até 11k quando compactado com gzip, principalmente em módulos pequenos. Se você tiver apenas uma superfície de WASM muito pequena, o embind poderá custar mais do que o valor em um ambiente de produção. No entanto, vale a pena tentar.