Processamento de dados no lado do cliente com WebAssembly





WebAssembly (abreviado WASM) é uma tecnologia para executar código binário pré-compilado em um navegador no lado do cliente. Foi introduzido pela primeira vez em 2015 e atualmente é compatível com a maioria dos navegadores modernos.



Um caso de uso comum é o pré-processamento de dados do lado do cliente antes de enviar os arquivos para o servidor. Neste artigo, vamos entender como isso é feito.



Antes do começo



A arquitetura WebAssembly e as etapas gerais são descritas com mais detalhes aqui e aqui . Examinaremos apenas os fatos básicos.



Trabalhar com WebAssembly começa com a pré-montagem dos artefatos necessários para executar o código compilado no lado do cliente. Existem dois deles: o próprio arquivo binário WASM e uma camada JavaScript por meio da qual você pode chamar os métodos exportados para ele.



Um exemplo do código C ++ mais simples para compilação



#include <algorithm>

extern "C" {
int calculate_gcd(int a, int b) {
  while (a != 0 && b != 0) {
    a %= b;
    std::swap(a, b);
  }
  return a + b;
}
}


Para a montagem, é usado o Emscripten , que, além da interface principal de tais compiladores, contém sinalizadores adicionais através dos quais a configuração da máquina virtual e os métodos exportados são definidos. O lançamento mais simples se parece com isto:



em++ main.cpp --std=c++17 -o gcd.html \
    -s EXPORTED_FUNCTIONS='["_calculate_gcd"]' \
    -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]'


Especificando um arquivo * .html como um objeto , ele informa ao compilador para criar uma marcação html simples com um console js também. Agora, se iniciarmos o servidor nos arquivos recebidos, veremos este console com a capacidade de iniciar _calculate_gcd:







Processamento de dados



Vamos analisá-lo usando um exemplo simples de compressão lz4 usando uma biblioteca escrita em C ++. Observe que os muitos idiomas suportados não param por aí.



Apesar da simplicidade e alguma natureza sintética do exemplo, esta é uma ilustração bastante útil de como trabalhar com dados. Da mesma forma, você pode executar qualquer ação neles para a qual o poder do cliente é suficiente: pré-processamento de imagens antes de enviar para o servidor, compressão de áudio, contagem de várias estatísticas e muito mais.



O código completo pode ser encontrado aqui.



Parte C ++



Usamos uma implementação pronta de lz4 . Então, o arquivo principal parecerá muito lacônico:



#include "lz4.h"

extern "C" {

uint32_t compress_data(uint32_t* data, uint32_t data_size, uint32_t* result) {
  uint32_t result_size = LZ4_compress(
        (const char *)(data), (char*)(result), data_size);
  return result_size;
}

uint32_t decompress_data(uint32_t* data, uint32_t data_size, uint32_t* result, uint32_t max_output_size) {
  uint32_t result_size = LZ4_uncompress_unknownOutputSize(
        (const char *)(data), (char*)(result), data_size, max_output_size);
  return result_size;
}

}


Como você pode ver, ele simplesmente declara funções externas (usando a palavra-chave extern ) que chamam internamente os métodos correspondentes da biblioteca com lz4.



De um modo geral, no nosso caso, o arquivo é inútil: você pode usar imediatamente a interface nativa do lz4.h . No entanto, em projetos mais complexos (por exemplo, combinar a funcionalidade de diferentes bibliotecas), é conveniente ter um ponto de entrada comum listando todas as funções usadas.



A seguir, compilamos o código usando o compilador Emscripten já mencionado :



em++ main.cpp lz4.c -o wasm_compressor.js \
    -s EXPORTED_FUNCTIONS='["_compress_data","_decompress_data"]' \
    -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' \
    -s WASM=1 -s ALLOW_MEMORY_GROWTH=1


O tamanho dos artefatos recebidos é alarmante:



$ du -hs wasm_compressor.*
112K    wasm_compressor.js
108K    wasm_compressor.wasm


Se você abrir o arquivo da camada JS, poderá ver algo assim:







Ele contém muitas coisas desnecessárias: de comentários a funções de serviço, muitos dos quais não são usados. A situação pode ser corrigida adicionando o sinalizador -O2, no compilador Emscripten, também inclui a otimização do código js.



Depois disso, o código js parece melhor:







Código do cliente



Você precisa chamar o manipulador do lado do cliente de alguma forma. Primeiramente, carregue o arquivo fornecido pelo usuário através do FileReader, vamos armazenar os dados brutos em uma primitiva Uint8Array:



var rawData = new Uint8Array(fileReader.result);


Em seguida, você precisa transferir os dados baixados para a máquina virtual. Para fazer isso, primeiro alocamos o número necessário de bytes usando o método _malloc e, em seguida, copiamos o array JS usando o método set. Por conveniência, vamos separar essa lógica na função arrayToWasmPtr (array):




function arrayToWasmPtr(array) {
  var ptr = Module._malloc(array.length);
  Module.HEAP8.set(array, ptr);
  return ptr;
}


Depois de carregar os dados na memória da máquina virtual, você precisa de alguma forma chamar a função do processamento. Mas como encontrar essa função? O método cwrap nos ajudará - o primeiro argumento nele especifica o nome da função necessária, o segundo - o tipo de retorno e o terceiro - uma lista com argumentos de entrada.




compressDataFunction = Module.cwrap('compress_data', 'number', ['number', 'number', 'number']);


Finalmente, você precisa retornar os bytes finalizados da máquina virtual. Para fazer isso, escrevemos outra função que os copia em uma matriz JS usando o métodosubarray



function wasmPtrToArray(ptr, length) {
  var array = new Int8Array(length);
  array.set(Module.HEAP8.subarray(ptr, ptr + length));
  return array;
}


O script completo para processamento de arquivos recebidos está aqui . Marcação HTML contendo formulário de upload de arquivo e upload de artefatos wasm aqui .



Resultado



Você pode brincar com o protótipo aqui .



O resultado é um backup de trabalho usando WASM. Das desvantagens - a implementação atual da tecnologia não permite liberar memória alocada na máquina virtual. Isso cria um vazamento implícito quando um grande número de arquivos é carregado em uma sessão, mas pode ser corrigido reutilizando a memória existente em vez de alocar uma nova.










All Articles