WebAssembly com C++

WebAssembly: um formato de instrução binário para a Web

editar

O WebAssembly é um formato de instrução binário para máquinas virtuais baseadas em pilha. Um dos principais objetivos desse formato é funcionar como um alvo de compilação leve, eficiente e portável, ideal para o ambiente Web. Como um alvo de compilação, programas nesse formato podem ser gerados a partir de diferentes linguagens já existentes. Isso possibilita a conversão de software já existente para o ambiente Web.

Um programa WebAssembly é definido a partir de um módulo, que contém estruturas como funções e tabelas. As funções definidas no módulo podem ser exportadas, possibilitando sua execução no sistema host do módulo. De maneira similar, o módulo pode importar funções do sistema host.

(module
  (func $soma (param $a i32) (param $b i32) (result i32)
    local.get $a
    local.get $b
    i32.add)
  (export "soma" (func $soma))
)

Cheerp: ecossistema para a compilação de C++ para WebAssembly

editar

O Cheerp é um sistema baseado no compilador Clang do LVVM que, além de compilar C++ para um módulo WASM, fornece a infraestrutura JavaScript necessária para executar esse módulo em um navegador. Além disso, o Cheerp fornece funcionalidades para a comunicação de alguns tipos complexos como arrays e strings entre o JavaScript e o C++. O Cheerp permite ainda transpilar funções em C++ para JavaScript, facilitando ainda mais a integração entre essas duas linguagens. O Cheerp pode ser instalado seguindo os passos descritos no tutorial de instalação.

Compilação

editar

Para compilar um programa em C++ a partir do Cheerp, basta utilizar o compilador Clang fornecido com a instalação. O Cheerp fornece como alvos para a compilação o JavaScript puro e o WebAssembly com sistemas auxiliares em JavaScript. Para definir qual desses alvos deve ser usado na compilação, é possível definir a flag -target como cheerp ou cheerp-wasm, respectivamente. Podem ser definidas também algumas outras flags para configurar aspectos como os arquivos de saída da compilação e o nível de otimização do código. A lista completa das flags pode ser encontrada na documentação do Cheerp.

Execução do compilador

/opt/cheerp/bin/clang++ -target cheerp-wasm tutorial.cpp -o tutorial.js -O3

Execução

editar

Para que módulo WebAssembly compilado possa ser executado em um navegador, é necessário criar um arquivo HTML que inclua o arquivo JavaScript gerado na compilação do módulo. Esse arquivo inclui os mecanismos necessários para carregar e instanciar o módulo WebAssembly de forma correta. Ele pode conter também JavaScript correspondente a funções C++ convertidas. Para que o módulo seja carregado corretamente, é necessário também servir esses arquivos por meio de um servidor HTTP. Em um cenário de desenvolvimento, isso pode ser feito de forma simples por meio do módulo http.server do Python.

HTML

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Tutorial</title>
    <script src="tutorial.js"></script>
  </head>
  <body></body>
</html>

Execução do servidor http.server

python3 -m http.server 3000

Inicialização do programa

editar

Logo após instanciar o módulo WASM, o Cheerp invoca a função webMain, se ela estiver definida. Isso pode ser utilizado para a inicialização de outras partes do programa. Além disso, ao contrário do que acontece em um programa C++ tradicional, caso sejam criadas estruturas que não estejam diretamente ligadas ao escopo da função webMain, como classes estáticas, instâncias no heap e variáveis globais, essas estruturas persistirão na memória, mesmo após terminada a execução da função. Dessa forma, o término da execução da função webMain não implica na terminação do módulo WASM.

C++

int contador = 0;

// a função poderá ser invocada para incrementar o contador
// mesmo após o término de webMain
[[cheerp::jsexport]]
void incrementar() {
	contador++;
}

void webMain() {
	// inicialização do programa aqui
}

Invocando funções do C++ a partir do JavaScript

editar

Com o atributo [[cheerp::jsexport]], é possivel definir funções do C++ que devem ser exportadas para o JavaScript. Para isso, os argumentos da função a ser exportada, assim como o seu valor de retorno, devem ser de tipos simples.

Para utilizar tipos como strings e arrays como argumentos e retorno de funções, é necessário que a função seja compilada para JavaScript, o que pode ser feito com o atributo [[cheerp::genericjs]]. Além disso, para os parâmetros e para o retorno, devem ser utilizados apenas tipos simples ou tipos definidos no namespace client, que representam recursos do JavaScript.

C++

[[cheerp::jsexport]]
int somar(int a, int b) {
	return a+b;
}

[[cheerp::jsexport]] [[cheerp::genericjs]]
client::String* cumprimentar(const client::String* nome) {
	return "Olá, " + nome.
}

JavaScript

console.log(somar(a+b));
console.log(cumprimentar("Paulo"));

Invocando funções em JavaScript a partir do C++

editar

O Cheerp possibilita ainda invocar funções definidas em JavaScript a partir do C++. Para isso, é necessário declarar dentro do namespace client a assinatura das funções a serem invocadas. É necessário ainda que essas funções estejam disponíveis no escopo global do JavaScript durante a execução do módulo WebAssembly. Na declaração das funções, é possível definir argumentos e valor de retorno como tipos simples ou como tipos definidos no namespace client. Nesse contexto, caso seja usado algum tipo do namespace client, a função poderá ser invocada apenas por uma função que tenha sido marcada com o decorador [[cheerp::genericjs]], ou seja, que tenha sido transpilada para JavaScript.

JavaScript

function cumprimentar(nome) {
	console.log("Olá, " + nome);
}

function alertar() {
	console.log("Você foi alertado!");
}

C++

namespace client {
	void alertar();
	void cumprimentar(const client::String* nome);
}

[[cheerp::genericjs]]
void cumprimentar_e_alertar() {
	client::cumprimentar("Paulo");
	client::alertar();
}

void apenas_alertar() {
	client::alertar();
}

Conversão de funções e estruturas do C++ para o JavaScript

editar

O Cheerp possibilita a conversão de funções e estruturas em C++ para JavaScript. Para isso, é possível marcar o código a ser convertido com o atributo [[cheerp::genericjs]]. As estruturas convertidas devem possuir apenas membros de tipos simples. Além disso, os argumentos e valor de retorno das funções convertidas devem ser de tipos simples ou de tipos definidos no namespace client. Uma característica interessante das funções convertidas com esse método é que elas dispõem de acesso à memória linear, o que permite o acesso dessas funções a estruturas definidas no C++, mesmo que as funções estejam sendo executadas em JavaScript.

C++

[[cheerp::genericjs]]
pair<char,char> extremos(int id_string) {

	const client::String* str = client::recuperarString(id_string);

	if(str->get_length() >= 2) {
		char primeiro = str->charCodeAt(0);
		char ultimo = str->charCodeAt(str->get_length()-1);
		return {primeiro, ultimo};
	}
	
	return {'\0','\0'};
}

Referências opacas: manipulando objetos do JavaScript a partir do C++

editar

Apesar de já existir alguns mecanismos no WebAssembly para gerenciar referências a objetos do JavaScript, esses recursos ainda não foram totalmente integrados ao Cheerp. Dessa forma uma alternativa para acessar objetos do JavaScript é construir um sistema de referências opacas. Uma referência opaca é uma forma de referenciar um objeto sem ter acesso à representação interna desse objeto. Ou seja, a partir de uma referência opaca, é possível ler e até mesmo alterar propriedades do objeto referenciado, mas todas essas operações devem passar pelo sistema que gerencia essas referências, de forma que não é possível acessar as propriedades do objeto diretamente. Apesar dessa limitação, o uso de referências opacas não é um grande obstáculo, principalmente em cenários em que essas referências não são utilizadas com uma grande frequência.

Para a criação de referências opacas a objetos JavaScript, uma alternativa simples é aproveitar do fato de a linguagem ser fracamente tipificada e criar um array para armazenar os objetos a serem referenciados. A partir disso, os índices no array podem funcionar como referências aos objetos correspondentes. Para garantir um uso mais eficiente do espaço, é possível ainda criar uma pilha para armazenar os índices livres. Dessa forma, quando um objeto deixa de ser necessário, ele é removido do array e o índice correspondente é inserido na pilha. Analogamente, quando deve ser armazenado um novo objeto, o último índice da pilha é utilizado, e, se a pilha estiver vazia, uma nova posição é criada no array. Para ter melhores garantias da integridade dessas estruturas é possível criar funções específicas para o registro, recuperação e remoção de objetos no array.

A partir disso, para chamar, a partir do C++ uma função do JavaScript que receba um objeto como argumento, basta passar o índice correspondente ao objeto, e, durante a execução da função, recuperar o objeto correspondente ao índice.

Essa mesma estratégia de referências opacas pode ser utilizada para referenciar objetos do C++ a partir do JavaScript. Nesse caso, como os objetos do C++ são armazenados em uma memória linear, não é necessária a criação de estruturas auxiliares para armazenar os objetos, basta utilizar como referência os apontadores, que são representados como valores numéricos, e, consequentemente, podem ser passados normalmente na chamada de uma função do JavaScript.

Function: invocando expressões lambda do C++ a partir do JavaScript

editar

No C++, o tipo function pode ser utilizado para representar expressões lambda, sendo que um ponteiro para uma instância function que representa uma lambda pode ser passada para o JavaScript e pode ser ainda armazenada em alguma variável ou estrutura. É possível também criar um método em C++ que receba um ponteiro para function e invoque a expressão correspondente. Dessa forma, para invocar a expressão lambda representada, basta passar a referência armazenada no JavaScript para o método C++ criado.

Referências

editar