Em O que é o WebAssembly e de onde ele veio?, Expliquei como acabamos com a WebAssembly de hoje. Neste artigo, mostrarei minha abordagem de compilação de um programa em C atual, mkbitmap
, para o WebAssembly. Ele é mais complexo do que o exemplo do hello world, já que inclui trabalhar com arquivos, comunicar-se entre as páginas do WebAssembly e do JavaScript e desenhar em uma tela, mas ainda é gerenciável o suficiente para não sobrecarregar você.
O artigo foi escrito para desenvolvedores Web que querem aprender a usar o WebAssembly e mostra um passo a passo de como você pode compilar algo como mkbitmap
para o WebAssembly. Não é possível compilar um app ou uma biblioteca na primeira execução. É por isso que algumas das etapas descritas abaixo não funcionaram. Por isso, precisei recuar e tentar outra vez. O artigo não mostra o comando mágico de compilação final como se ele tivesse caído do céu, mas descreve meu progresso real, incluindo algumas frustrações.
Sobre mkbitmap
O programa C mkbitmap
lê uma imagem e aplica uma ou mais das seguintes operações a ela, nesta ordem: inversão, filtragem de passagem alta, escalonamento e limite. Cada operação pode ser controlada individualmente e ativada ou desativada. O principal uso do mkbitmap
é para converter imagens coloridas ou em escala de cinza em um formato adequado como entrada para outros programas, especialmente o programa de rastreamento potrace
que forma a base do SVGcode (link em inglês). Como uma ferramenta de pré-processamento, o mkbitmap
é particularmente útil para converter artes de linhas digitalizadas, como desenhos ou textos manuscritos, em imagens de dois níveis de alta resolução.
Para usar mkbitmap
, transmita uma série de opções e um ou vários nomes de arquivo. Para todos os detalhes, consulte a página do manual da ferramenta:
$ mkbitmap [options] [filename...]
Acessar o código
A primeira etapa é conseguir o código-fonte de mkbitmap
. Encontre o código no site do projeto. No momento em que este artigo foi escrito, potrace-1.16.tar.gz é a versão mais recente.
Compilar e instalar localmente
A próxima etapa é compilar e instalar a ferramenta localmente para ter uma ideia de como ela se comporta. O arquivo INSTALL
contém as seguintes instruções:
cd
, que é o diretório que contém o código-fonte e o tipo do pacote../configure
para configurar o pacote para o sistema.A execução de
configure
pode demorar um pouco. Durante a execução, ela imprime algumas mensagens informando quais recursos ele está verificando.Digite
make
para compilar o pacote.Se quiser, digite
make check
para executar os autotestes incluídos. geralmente usando os binários recém-criados e desinstalados.Digite
make install
para instalar os programas e todos os arquivos de dados e na documentação do Google Cloud. Ao instalar em um prefixo de propriedade do usuário raiz, é recomendado que o pacote seja configurado e criado usuário, e apenas a fasemake install
executada com acesso root para conceder privilégios de acesso.
Ao seguir essas etapas, você terá dois executáveis, potrace
e mkbitmap
. Este último é o foco deste artigo. Para verificar se ele funcionou corretamente, execute mkbitmap --version
. Esta é a saída das quatro etapas da minha máquina, bastante cortadas para simplificar:
Etapa 1, ./configure
:
$ ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... ./install-sh -c -d
checking for gawk... no
checking for mawk... no
checking for nawk... no
checking for awk... awk
checking whether make sets $(MAKE)... yes
[…]
config.status: executing libtool commands
Etapa 2, make
:
$ make
/Applications/Xcode.app/Contents/Developer/usr/bin/make all-recursive
Making all in src
clang -DHAVE_CONFIG_H -I. -I.. -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
mv -f .deps/main.Tpo .deps/main.Po
[…]
make[2]: Nothing to be done for `all-am'.
Etapa 3, make check
:
$ make check
Making check in src
make[1]: Nothing to be done for `check'.
Making check in doc
make[1]: Nothing to be done for `check'.
[…]
============================================================================
Testsuite summary for potrace 1.16
============================================================================
# TOTAL: 8
# PASS: 8
# SKIP: 0
# XFAIL: 0
# FAIL: 0
# XPASS: 0
# ERROR: 0
============================================================================
make[1]: Nothing to be done for `check-am'.
Etapa 4, sudo make install
:
$ sudo make install
Password:
Making install in src
.././install-sh -c -d '/usr/local/bin'
/bin/sh ../libtool --mode=install /usr/bin/install -c potrace mkbitmap '/usr/local/bin'
[…]
make[2]: Nothing to be done for `install-data-am'.
Para verificar se funcionou, execute mkbitmap --version
:
$ mkbitmap --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.
Se você recebeu os detalhes da versão, isso significa que o mkbitmap
foi compilado e instalado. Em seguida, faça o equivalente dessas etapas funcionar com o WebAssembly.
Compilar mkbitmap
para o WebAssembly
O Emscripten é uma ferramenta para compilar programas C/C++ para o WebAssembly. A documentação Como criar projetos da Emscripten diz o seguinte:
Criar grandes projetos com o Emscripten é muito fácil. O Emscripten fornece dois scripts simples que configuram seus makefiles para usar
emcc
como uma substituição simples paragcc
. Na maioria dos casos, o restante do sistema de compilação atual do seu projeto permanece inalterado.
A documentação segue em frente (um pouco editada para concisão):
Considere o caso em que você normalmente cria com os seguintes comandos:
./configure
:make
Para criar com o Emscripten, use os seguintes comandos:
emconfigure ./configure
emmake make
Basicamente, ./configure
se torna emconfigure ./configure
, e make
se torna emmake make
. Veja a seguir como fazer isso com mkbitmap
.
Etapa 0, make clean
:
$ make clean
Making clean in src
rm -f potrace mkbitmap
test -z "" || rm -f
rm -rf .libs _libs
[…]
rm -f *.lo
Etapa 1, emconfigure ./configure
:
$ emconfigure ./configure
configure: ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... ./install-sh -c -d
checking for gawk... no
checking for mawk... no
checking for nawk... no
checking for awk... awk
[…]
config.status: executing libtool commands
Etapa 2, emmake make
:
$ emmake make
make: make
/Applications/Xcode.app/Contents/Developer/usr/bin/make all-recursive
Making all in src
/opt/homebrew/Cellar/emscripten/3.1.36/libexec/emcc -DHAVE_CONFIG_H -I. -I.. -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
mv -f .deps/main.Tpo .deps/main.Po
[…]
make[2]: Nothing to be done for `all'.
Se tudo tiver dado certo, agora devem haver arquivos .wasm
em algum lugar do diretório. É possível encontrá-los executando find . -name "*.wasm"
:
$ find . -name "*.wasm"
./a.wasm
./src/mkbitmap.wasm
./src/potrace.wasm
Os dois últimos são promissores, então cd
no diretório src/
. Agora, há também dois novos arquivos correspondentes, mkbitmap
e potrace
. Para este artigo, apenas mkbitmap
é relevante. O fato de eles não terem a extensão .js
é um pouco confuso, mas eles são, na verdade, arquivos JavaScript, verificáveis com uma rápida chamada head
:
$ cd src/
$ head -n 20 mkbitmap
// include: shell.js
// The Module object: Our interface to the outside world. We import
// and export values on it. There are various ways Module can be used:
// 1. Not defined. We create it here
// 2. A function parameter, function(Module) { ..generated code.. }
// 3. pre-run appended it, var Module = {}; ..generated code..
// 4. External script tag defines var Module.
// We need to check if Module already exists (e.g. case 3 above).
// Substitution will be replaced with actual code on later stage of the build,
// this way Closure Compiler will not mangle it (e.g. case 4. above).
// Note that if you want to run closure, and also to use Module
// after the generated code, you will need to define var Module = {};
// before the code. Then that object will be used in the code, and you
// can continue to use Module afterwards as well.
var Module = typeof Module != 'undefined' ? Module : {};
// --pre-jses are emitted after the Module integration code, so that they can
// refer to Module (if they choose; they can also define Module)
Renomeie o arquivo JavaScript como mkbitmap.js
chamando mv mkbitmap mkbitmap.js
(e mv potrace potrace.js
, respectivamente, se quiser).
Agora, é hora do primeiro teste ver se funcionou executando o arquivo com Node.js na linha de comando usando node mkbitmap.js --version
:
$ node mkbitmap.js --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.
Você compilou o mkbitmap
para o WebAssembly. Agora, a próxima etapa é fazer com que ele funcione no navegador.
mkbitmap
com o WebAssembly no navegador
Copie os arquivos mkbitmap.js
e mkbitmap.wasm
para um novo diretório chamado mkbitmap
e crie um arquivo HTML boilerplate index.html
que carregue o arquivo JavaScript mkbitmap.js
.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>mkbitmap</title>
</head>
<body>
<script src="mkbitmap.js"></script>
</body>
</html>
Inicie um servidor local que disponibilize o diretório mkbitmap
e abra-o no navegador. Aparecerá um comando que solicita informações. Isso é o esperado, já que, de acordo com a página do manual da ferramenta, "[i]se nenhum argumento de nome de arquivo for fornecido, o mkbitmap atuará como um filtro, lendo a entrada padrão", que para Emscripten é, por padrão, um prompt()
.
Impedir a execução automática
Para interromper a execução de mkbitmap
imediatamente e fazer com que ele aguarde a entrada do usuário, você precisa entender o objeto Module
do Emscripten. Module
é um objeto JavaScript global com atributos que o código gerado pelo Emscripten chama em vários pontos durante a execução.
Você pode fornecer uma implementação de Module
para controlar a execução do código.
Quando um aplicativo Emscripten é iniciado, ele analisa os valores no objeto Module
e os aplica.
No caso de mkbitmap
, defina Module.noInitialRun
como true
para evitar a execução inicial que causou a exibição da solicitação. Crie um script com o nome script.js
, inclua-o antes de <script src="mkbitmap.js"></script>
em index.html
e adicione o seguinte código a script.js
. Quando você recarregar o app, a solicitação desaparecerá.
var Module = {
// Don't run main() at page load
noInitialRun: true,
};
Criar um build modular com mais algumas flags de build
Para fornecer entradas para o app, você pode usar o suporte ao sistema de arquivos do Emscripten no Module.FS
. A seção Incluindo o suporte a sistema de arquivos da documentação afirma:
O Emscripten decide se quer incluir o suporte ao sistema de arquivos automaticamente. Muitos programas não precisam de arquivos, e o suporte ao sistema de arquivos não é insignificante, então Emscripten evita incluí-los quando não há motivo para isso. Isso significa que, se o código C/C++ não acessar os arquivos, o objeto
FS
e outras APIs do sistema de arquivos não serão incluídos na saída. Por outro lado, se o código C/C++ usar arquivos, o suporte ao sistema de arquivos será incluído automaticamente.
Infelizmente, mkbitmap
é um dos casos em que o Emscripten não inclui automaticamente o suporte ao sistema de arquivos, portanto, é necessário instruí-lo a fazer isso. Isso significa que você precisa seguir as etapas emconfigure
e emmake
descritas anteriormente, com mais algumas flags definidas usando um argumento CFLAGS
. As sinalizações a seguir também podem ser úteis para outros projetos.
- Defina
-sFILESYSTEM=1
para que o suporte ao sistema de arquivos seja incluído. - Defina
-sEXPORTED_RUNTIME_METHODS=FS,callMain
para queModule.FS
eModule.callMain
sejam exportados. - Configure
-sMODULARIZE=1
e-sEXPORT_ES6
para gerar um módulo ES6 moderno. - Defina
-sINVOKE_RUN=0
para impedir a execução inicial que causou a exibição da solicitação.
Além disso, nesse caso específico, é necessário definir a flag --host
como wasm32
para informar ao script configure
que você está compilando para o WebAssembly.
O comando emconfigure
final ficará assim:
$ emconfigure ./configure --host=wasm32 CFLAGS='-sFILESYSTEM=1 -sEXPORTED_RUNTIME_METHODS=FS,callMain -sMODULARIZE=1 -sEXPORT_ES6 -sINVOKE_RUN=0'
Execute emmake make
novamente e copie os arquivos recém-criados para a pasta mkbitmap
.
Modifique index.html
para que ele carregue apenas o módulo ES script.js
, de onde você importa o módulo mkbitmap.js
.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>mkbitmap</title>
</head>
<body>
<!-- No longer load `mkbitmap.js` here -->
<script src="script.js" type="module"></script>
</body>
</html>
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
console.log(Module);
};
run();
Ao abrir o app no navegador, você verá o objeto Module
registrado no console do DevTools. O prompt desaparece, já que a função main()
de mkbitmap
não é mais chamada no início.
Executar manualmente a função principal
A próxima etapa é chamar manualmente a função main()
do mkbitmap
executando Module.callMain()
. A função callMain()
usa uma matriz de argumentos, que correspondem, um a um, ao que você transmitiria na linha de comando. Se você executar mkbitmap -v
na linha de comando, chame Module.callMain(['-v'])
no navegador. Isso registra o número da versão de mkbitmap
no console do DevTools.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
Module.callMain(['-v']);
};
run();
Redirecionar a saída padrão
Por padrão, a saída padrão (stdout
) é o console. No entanto, é possível redirecioná-lo para outra coisa, por exemplo, uma função que armazena a saída para uma variável. Isso significa que você pode adicionar a saída ao HTML definindo a propriedade Module.print
.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
let consoleOutput = 'Powered by ';
const Module = await loadWASM({
print: (text) => (consoleOutput += text),
});
Module.callMain(['-v']);
document.body.textContent = consoleOutput;
};
run();
Inserir o arquivo de entrada no sistema de arquivos de memória
Para colocar o arquivo de entrada no sistema de arquivos de memória, você precisa do equivalente de mkbitmap filename
na linha de comando. Para entender como abordo isso, primeiro vejamos como o mkbitmap
espera a entrada e cria a saída.
Os formatos de entrada com suporte de mkbitmap
são PNM (PBM, PGM, PPM) e BMP. Os formatos de saída são PBM para bitmaps e PGM para cinzas. Se um argumento filename
for fornecido, mkbitmap
vai criar, por padrão, um arquivo de saída cujo nome é obtido do nome do arquivo de entrada, mudando o sufixo para .pbm
. Por exemplo, para o nome do arquivo de entrada example.bmp
, o nome do arquivo de saída seria example.pbm
.
O Emscripten fornece um sistema de arquivos virtual que simula o sistema de arquivos local, para que o código nativo que usa APIs de arquivos síncronas possa ser compilado e executado com pouca ou nenhuma alteração.
Para que o mkbitmap
leia um arquivo de entrada como se ele tivesse sido transmitido como um argumento de linha de comando filename
, é necessário usar o objeto FS
fornecido pelo Emscripten.
O objeto FS
tem como base um sistema de arquivos na memória (geralmente chamado de MEMFS) e tem uma função writeFile()
que você usa para gravar arquivos no sistema de arquivos virtual. Use writeFile()
conforme mostrado no exemplo de código a seguir.
Para verificar se a operação de gravação de arquivos funcionou, execute a função readdir()
do objeto FS
com o parâmetro '/'
. Você verá example.bmp
e vários arquivos padrão que são sempre criados automaticamente.
A chamada anterior a Module.callMain(['-v'])
para mostrar o número da versão foi removida. Isso ocorre porque Module.callMain()
é uma função que geralmente precisa ser executada apenas uma vez.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
console.log(Module.FS.readdir('/'));
};
run();
Primeira execução real
Com tudo no lugar, execute mkbitmap
executando Module.callMain(['example.bmp'])
. Registre o conteúdo dos arquivos MEMFS '/'
. O arquivo de saída example.pbm
recém-criado será mostrado ao lado do arquivo de entrada example.bmp
.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
Module.callMain(['example.bmp']);
console.log(Module.FS.readdir('/'));
};
run();
Extrair o arquivo de saída do sistema de arquivos de memória
A função readFile()
do objeto FS
permite extrair o example.pbm
criado na última etapa do sistema de arquivos de memória. A função retorna um Uint8Array
que você converte em um objeto File
e salva em disco, já que os navegadores geralmente não são compatíveis com arquivos PBM para visualização direta no navegador.
Existem maneiras mais elegantes de salvar um arquivo, mas usar um <a download>
criado dinamicamente é a mais amplamente compatível. Depois que o arquivo for salvo, você poderá abri-lo no seu visualizador de imagens favorito.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
Module.callMain(['example.bmp']);
const output = Module.FS.readFile('example.pbm', { encoding: 'binary' });
const file = new File([output], 'example.pbm', {
type: 'image/x-portable-bitmap',
});
const a = document.createElement('a');
a.href = URL.createObjectURL(file);
a.download = file.name;
a.click();
};
run();
Adicionar uma interface interativa
Até aqui, o arquivo de entrada está fixado no código, e o mkbitmap
é executado com parâmetros padrão. A etapa final é permitir que o usuário selecione dinamicamente um arquivo de entrada, ajuste os parâmetros mkbitmap
e execute a ferramenta com as opções selecionadas.
// Corresponds to `mkbitmap -o output.pbm input.bmp -s 8 -3 -f 4 -t 0.45`.
Module.callMain(['-o', 'output.pbm', 'input.bmp', '-s', '8', '-3', '-f', '4', '-t', '0.45']);
O formato de imagem PBM não é muito difícil de analisar, portanto, com algum código JavaScript, é possível até mesmo mostrar uma visualização da imagem de saída. Consulte o código-fonte da demonstração incorporada abaixo para saber como fazer isso.
Conclusão
Parabéns, você compilou o mkbitmap
para o WebAssembly e o fez funcionar no navegador. Havia alguns becos sem saída, e era preciso compilar a ferramenta mais de uma vez até que funcionasse, mas, como escrevi acima, isso faz parte da experiência. Lembre-se também da tag webassembly
do StackOverflow se tiver problemas. Boa compilação!
Agradecimentos
Este artigo foi revisado por Sam Clegg e Rachel Andrew.