Às vezes, você quer usar uma biblioteca disponível apenas como código C ou C++. Tradicionalmente, é aqui que você desiste. Bem, não mais, porque agora temos Emscripten e WebAssembly (ou Wasm)!
O conjunto de ferramentas
Estabeleci meu objetivo de descobrir como compilar um código C existente para Wasm. Havia algum ruído ao redor do back-end Wasm do LLVM. Comecei a investigar isso. você pode compilar programas simples, dessa forma, no segundo em que você quer usar a biblioteca padrão de C ou até mesmo compilar vários arquivos, você provavelmente terá problemas. Isso me levou lição que aprendi:
Embora o Emscripten tenha usado ser um compilador C-to-asm.js, ele amadureceu para Wasm e é no processo de troca para o back-end oficial do LLVM internamente. O Emscripten também oferece Implementação compatível com Wasm da biblioteca padrão C. Use o Emscripten. Ela tem muito trabalho oculto, emula um sistema de arquivos, fornece gerenciamento de memória, une o OpenGL com o WebGL — uma coisas que você não precisa experimentar no desenvolvimento.
Embora isso possa parecer que você precisa se preocupar com a sobrecarga, eu certamente me preocupei — o compilador Emscripten remove tudo o que não é necessário. No meu nos experimentos, os módulos Wasm resultantes são dimensionados adequadamente de acordo com a lógica que eles contêm, e as equipes Emscripten e WebAssembly estão trabalhando para com um número ainda menor no futuro.
Para ter o Emscripten, siga as instruções na do site ou usando o Homebrew. Se você é fã de comandos dockerizados como eu e não quer instalar nada no sistema, para brincar com o WebAssembly, há um imagem Docker que pode ser usada como alternativa:
$ docker pull trzeci/emscripten
$ docker run --rm -v $(pwd):/src trzeci/emscripten emcc <emcc options here>
Compilação de algo simples
Vamos pegar o exemplo quase canônico de escrever uma função em C que calcula o no número de fibonacci:
#include <emscripten.h>
EMSCRIPTEN_KEEPALIVE
int fib(int n) {
if(n <= 0){
return 0;
}
int i, t, a = 0, b = 1;
for (i = 1; i < n; i++) {
t = a + b;
a = b;
b = t;
}
return b;
}
Se você conhece a linguagem C, a função em si não surpreenderá muito. Mesmo se você não conhece C, mas sabe JavaScript, espera-se que seja capaz de entender o que está acontecendo aqui.
emscripten.h
é um arquivo principal fornecido pela Emscripten. Nós só precisamos dele, então
têm acesso à macro EMSCRIPTEN_KEEPALIVE
, mas ela
oferece muito mais funcionalidades.
Essa macro diz ao compilador para não remover uma função, mesmo que ela apareça
o que não é usado. Se omitíssemos essa macro, o compilador otimizaria a função
Afinal, ninguém usa.
Vamos salvar tudo isso em um arquivo chamado fib.c
. Para transformá-lo em um arquivo .wasm
,
precisa usar o comando emcc
do compilador do Emscripten:
$ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' fib.c
Vamos analisar esse comando. emcc
é o compilador do Emscripten. fib.c
é nosso C
. Até aqui, tudo bem. O -s WASM=1
pede para o Emscripten fornecer um arquivo Wasm.
em vez de um arquivo asm.js.
-s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]'
instrui o compilador a sair do
A função cwrap()
está disponível no arquivo JavaScript. Saiba mais sobre ela
mais tarde. -O3
instrui o compilador a otimizar de forma agressiva. Você pode escolher
para reduzir o tempo de build, mas isso também tornará os pacotes resultantes
maior, já que o compilador pode não remover código não utilizado.
Depois de executar o comando, você terá um arquivo JavaScript chamado
a.out.js
e um arquivo WebAssembly chamado a.out.wasm
. O arquivo Wasm (ou
"module") contém nosso código C compilado e deve ser bem pequeno. A
JavaScript faz o carregamento e a inicialização do módulo Wasm e
fornecendo uma API melhor. Se necessário, ele também cuidará da configuração do
pilha, a pilha e outras funcionalidades que normalmente se espera que sejam fornecidas pela
no sistema operacional
ao escrever o código C. Dessa forma, o arquivo JavaScript
maior, pesando-se a 19 KB (~5 KB em gzip).
Executando algo simples
A maneira mais fácil de carregar e executar seu módulo é usar o JavaScript gerado
. Depois de carregar esse arquivo, você terá uma
Module
global
à sua disposição. Usar
cwrap
para criar uma função nativa de JavaScript que cuida da conversão de parâmetros
a algo compatível com C e invocar a função encapsulada. cwrap
pega
nome da função, tipo de retorno e tipos de argumento como argumentos, nesta ordem:
<script src="a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
const fib = Module.cwrap('fib', 'number', ['number']);
console.log(fib(12));
};
</script>
Se você executar o código, vai aparecer "144" no console, que é o 12o número de Fibonacci.
O Santo Graal: compilação de uma biblioteca C
Até agora, o código C que escrevemos foi escrito com o Wasm em mente. Um núcleo de uso do WebAssembly, no entanto, é pegar o ecossistema existente de e permitir que os desenvolvedores as usem na Web. Essas bibliotecas geralmente dependem da biblioteca padrão de C, um sistema operacional, um sistema de arquivos e outros coisas. A Emscripten fornece a maioria desses recursos, embora existam limitações.
Vamos voltar ao meu objetivo original: compilar um codificador para WebP para Wasm. A para o codec WebP é escrita em C e disponível em GitHub, assim como alguns exemplos Documentação da API. Esse é um ótimo ponto de partida.
$ git clone https://github.com/webmproject/libwebp
Para começar de forma simples, vamos tentar expor o WebPGetEncoderVersion()
da
encode.h
em JavaScript gravando um arquivo C chamado webp.c
:
#include "emscripten.h"
#include "src/webp/encode.h"
EMSCRIPTEN_KEEPALIVE
int version() {
return WebPGetEncoderVersion();
}
Esse é um bom programa simples para testar se conseguimos o código-fonte do libwebp para compilar, pois não precisamos de parâmetros ou estruturas de dados complexas para invoque essa função.
Para compilar esse programa, precisamos informar ao compilador onde ele pode encontrar
arquivos de cabeçalho do libwebp usando a sinalização -I
e também transmiti-la a todos os arquivos C do
libwebp necessário. Para ser honesta: eu só dei todo o Dó
dos arquivos que encontramos e dependia do compilador para remover tudo o que
desnecessária. Ele funcionou muito bem!
$ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' \
-I libwebp \
webp.c \
libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c
Agora só precisamos de HTML e JavaScript para carregar nosso novo módulo:
<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = async (_) => {
const api = {
version: Module.cwrap('version', 'number', []),
};
console.log(api.version());
};
</script>
E veremos o número da versão de correção na seção output:
Transferir uma imagem do JavaScript para o Wasm
Obter o número da versão do codificador é ótimo, mas codificar um seria mais impressionante, certo? Vamos fazer isso, então.
A primeira pergunta que precisamos responder é: como colocamos a imagem na terra do Wasm?
Analisando
API de codificação do libwebp, espera-se
uma matriz de bytes em RGB, RGBA, BGR ou BGRA. Felizmente, a API Canvas tem
getImageData()
,
que nos dá
Uint8ClampedArray
contendo os dados da imagem em RGBA:
async function loadImage(src) {
// Load image
const imgBlob = await fetch(src).then((resp) => resp.blob());
const img = await createImageBitmap(imgBlob);
// Make canvas same size as image
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
// Draw image onto canvas
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
return ctx.getImageData(0, 0, img.width, img.height);
}
Agora é "apenas" uma questão de copiar os dados do JavaScript para o Wasm terra. Para isso, precisamos expor mais duas funções. Um que aloca para a imagem dentro do Wasm e outra que a libere de novo:
EMSCRIPTEN_KEEPALIVE
uint8_t* create_buffer(int width, int height) {
return malloc(width * height * 4 * sizeof(uint8_t));
}
EMSCRIPTEN_KEEPALIVE
void destroy_buffer(uint8_t* p) {
free(p);
}
create_buffer
aloca um buffer para a imagem RGBA. Portanto, 4 bytes por pixel.
O ponteiro retornado por malloc()
é o endereço da primeira célula de memória do
desse buffer. Quando o ponteiro é retornado para JavaScript, ele é tratado como
apenas um número. Depois de expor a função ao JavaScript usando cwrap
, podemos
usar esse número para encontrar o início do nosso buffer e copiar os dados da imagem.
const api = {
version: Module.cwrap('version', 'number', []),
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};
const image = await loadImage('/image.jpg');
const p = api.create_buffer(image.width, image.height);
Module.HEAP8.set(image.data, p);
// ... call encoder ...
api.destroy_buffer(p);
Grand Finale: codificar a imagem
A imagem agora está disponível em Wasm. É hora de chamar o codificador WebP para
o que faz o trabalho dela. Analisando
Documentação do WebP, WebPEncodeRGBA
parece ser a opção perfeita. A função leva um ponteiro para a imagem de entrada e
suas dimensões e uma opção de qualidade entre 0 e 100. Ele também aloca
um buffer de saída, que precisamos liberar usando WebPFree()
quando
não tenha mais a imagem WebP.
O resultado da operação de codificação é um buffer de saída e seu comprimento. Devido ao funções em C não podem ter matrizes como tipos de retorno (a menos que aloquemos memória de forma dinâmica), optei por uma matriz global estática. Eu sei, não limpar C (na verdade, depende do fato de que os ponteiros Wasm têm 32 bits de largura, mas, para manter simples. Acho que esse é um atalho justo.
int result[2];
EMSCRIPTEN_KEEPALIVE
void encode(uint8_t* img_in, int width, int height, float quality) {
uint8_t* img_out;
size_t size;
size = WebPEncodeRGBA(img_in, width, height, width * 4, quality, &img_out);
result[0] = (int)img_out;
result[1] = size;
}
EMSCRIPTEN_KEEPALIVE
void free_result(uint8_t* result) {
WebPFree(result);
}
EMSCRIPTEN_KEEPALIVE
int get_result_pointer() {
return result[0];
}
EMSCRIPTEN_KEEPALIVE
int get_result_size() {
return result[1];
}
Agora que tudo isso está pronto, podemos chamar a função de codificação, e tamanho da imagem, colocá-lo em um buffer próprio de JavaScript libera todas as reservas Wasm-land que alocamos no processo.
api.encode(p, image.width, image.height, 100);
const resultPointer = api.get_result_pointer();
const resultSize = api.get_result_size();
const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);
const result = new Uint8Array(resultView);
api.free_result(resultPointer);
Dependendo do tamanho da imagem, você pode se deparar com um erro em que o Wasm não pode aumentar a memória o suficiente para acomodar a imagem de entrada e de saída:
Felizmente, a solução para esse problema está na mensagem de erro. Só precisamos
Adicione -s ALLOW_MEMORY_GROWTH=1
ao comando de compilação.
Pronto! Compilamos um codificador WebP e transcodificamos uma imagem JPEG
WebP Para provar que isso funcionou, podemos transformar nosso buffer de resultados em um blob e usar
em um elemento <img>
:
const blob = new Blob([result], { type: 'image/webp' });
const blobURL = URL.createObjectURL(blob);
const img = document.createElement('img');
img.src = blobURL;
document.body.appendChild(img);
Veja a glória de uma nova imagem WebP!
Conclusão
Não é um passeio para fazer uma biblioteca C funcionar no navegador, mas uma vez você entende o processo geral e como o fluxo de dados funciona, isso se torna mais fácil, e os resultados podem ser impressionantes.
O WebAssembly abre muitas novas possibilidades na Web para processamento, número a fragmentação e os jogos. Tenha em mente que o Wasm não é uma solução perfeita ser aplicado a tudo, mas quando você encontra um desses gargalos, o Wasm pode ser é uma ferramenta incrivelmente útil.
Conteúdo bônus: executar algo simples da maneira difícil
Para evitar o arquivo JavaScript gerado, Vamos voltar ao exemplo de Fibonacci. Para carregar e executar manualmente, podemos faça o seguinte:
<!DOCTYPE html>
<script>
(async function () {
const imports = {
env: {
memory: new WebAssembly.Memory({ initial: 1 }),
STACKTOP: 0,
},
};
const { instance } = await WebAssembly.instantiateStreaming(
fetch('/a.out.wasm'),
imports,
);
console.log(instance.exports._fib(12));
})();
</script>
Os módulos WebAssembly criados pela Emscripten não têm memória para funcionar
a menos que você forneça memória. A maneira como você fornece um módulo Wasm com
qualquer coisa é usar o objeto imports
, o segundo parâmetro da
função instantiateStreaming
. O módulo Wasm pode acessar tudo que está dentro dele
o objeto "imports", mas nada além dele. Por convenção, os módulos
compilado pela Emscripting esperam que algumas coisas do código JavaScript
de nuvem:
- Primeiro, há
env.memory
. O módulo Wasm não reconhece por assim dizer, então ele precisa de memória para trabalhar. Tecla EnterWebAssembly.Memory
Ele representa uma parte da memória linear (opcionalmente expansível). O dimensionamento parâmetros estão em "em unidades de páginas WebAssembly", ou seja, o código acima aloca uma página de memória, cada uma com tamanho de 64 KiB (em inglês). Sem fornecer ummaximum
a memória é teoricamente ilimitada em crescimento (o Chrome atualmente tem um limite absoluto de 2 GB). A maioria dos módulos WebAssembly não precisa configurar uma máximo. env.STACKTOP
define onde a pilha deve começar a crescer. A pilha é necessária para fazer chamadas de função e alocar memória para variáveis locais. Como não fazemos nenhum manobras de gerenciamento de memória dinâmica em nossa pequena programa Fibonacci, podemos usar toda a memória como uma pilha, ou seja,STACKTOP = 0
: