Ele vincula o JS ao seu Wasm.
No meu último artigo Wasm, expliquei como compilar uma biblioteca C no Wasm para uso na Web. Uma coisa que se destacou para mim (e para muitos leitores) é a maneira bruta e um pouco estranha de declarar manualmente quais funções do módulo Wasm você está usando. Para relembrar, este é o snippet de código sobre o 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 quais são 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 maneira não oferece suporte a strings e
exige que você mova blocos de memória manualmente, o que torna o uso de muitas APIs
de biblioteca muito tedioso. Existe uma maneira melhor?
Mangling de nomes 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 importante: quando você compila código C
ou C++, cada arquivo é compilado separadamente. Em seguida, um vinculador cuida de agrupar todos esses chamados 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 o vinculador usar. Tudo o que você precisa para chamar uma função C é o nome,
que fornecemos como uma string para cwrap()
.
O C++, por outro lado, 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 bom nome como add
seria mutilado 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
com o nome dela.
Inserir embind
embind faz parte do conjunto de ferramentas Emscripten e oferece várias macros C++ que permitem anotar códigos C++. É possível declarar quais funções, tipos enumerados, classes ou tipos de valor você planeja usar no JavaScript. Vamos começar simplesmente 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);
}
Comparado ao artigo anterior, não estamos mais incluindo 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 ao JavaScript.
Para compilar esse arquivo, podemos usar a mesma configuração (ou, se você quiser, a mesma imagem Docker) do artigo anterior. Para usar o embind,
adicionamos a flag --bind
:
$ emcc --bind -O3 add.cpp
Agora, tudo o que resta é preparar um arquivo HTML que carrega 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 notar, não estamos mais usando cwrap()
. Isso funciona imediatamente. Mas, mais importante, não precisamos nos preocupar em copiar manualmente
blocos de memória para fazer as strings funcionarem. O embind oferece isso sem custos financeiros,
junto com verificações de tipo:
Isso é ótimo, porque podemos detectar alguns erros mais cedo, em vez de lidar com os erros Wasm que, às vezes, são bastante complicados.
Objetos
Muitos construtores e funções JavaScript usam objetos de opções. É um bom padrão em JavaScript, mas extremamente tedioso de perceber no Wasm manualmente. Ele também pode ajudar aqui.
Por exemplo, eu criei essa função C++ incrivelmente útil que processa minhas strings e quero usá-la com urgência na Web. Fiz isso da seguinte maneira:
#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 função processMessage()
. No bloco
EMSCRIPTEN_BINDINGS
, posso usar value_object
para fazer com que o JavaScript veja
esse valor C++ como um objeto. Também poderia usar value_array
se quisesse
usar esse valor em C++ como uma matriz. Também vinculo a função processMessage()
, e
o restante é vinculado a uma mágica. 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 fins de integridade, também vou mostrar como a vinculação permite expor classes inteiras, o que traz muita sinergia com as classes ES6. Você provavelmente já poderá notar 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);
}
No JavaScript, isso é quase como 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 o 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 a arquivos C. Para misturar C e C++, você só precisa
separar os arquivos de entrada em dois grupos: um para C e outro para arquivos C++ e
aumentar 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 o Wasm e o C/C++. Este artigo não abrange todas as opções que o embind oferece. Se você tiver interesse, recomendo continuar com a documentação da Embind (em inglês). Lembre-se de que usar o embind pode aumentar o módulo Wasm e o código agrupador JavaScript em até 11k quando o gzip é usado, principalmente em módulos pequenos. Se você tiver apenas uma superfície de Wasm muito pequena, ela poderá custar mais do que vale a pena em um ambiente de produção. No entanto, você deve definitivamente tentar.