Em O que é WebAssembly e de onde ele surgiu?, Expliquei como chegamos ao WebAssembly de hoje. Neste artigo, vou mostrar minha abordagem para compilar um programa C existente, mkbitmap
, para WebAssembly. Ele é mais complexo do que o exemplo hello world, já que inclui trabalhar com arquivos, comunicar entre o WebAssembly e o JavaScript e desenhar em uma tela, mas ainda é gerenciável o suficiente para não sobrecarregar você.
O artigo é destinado a desenvolvedores da Web que querem aprender WebAssembly e mostra como proceder se você quiser compilar algo como mkbitmap
para WebAssembly. Como um aviso justo, não conseguir compilar um app ou uma biblioteca na primeira execução é completamente normal. Por isso, algumas das etapas descritas abaixo não funcionaram, então precisei voltar atrás e tentar de novo de outra forma. 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 em C mkbitmap
lê uma imagem e aplica uma ou mais das seguintes operações a ela, nesta ordem: inversão, filtragem de alta frequência, dimensionamento e limiarização. Cada operação pode ser controlada e ativada ou desativada individualmente. O uso principal do mkbitmap
é converter imagens coloridas ou em escala de cinza em um formato adequado como entrada para outros programas, principalmente o programa de rastreamento potrace
, que forma a base do SVGcode. Como ferramenta de pré-processamento, o mkbitmap
é especialmente útil para converter arte linear digitalizada, como desenhos animados ou texto manuscrito, em imagens bicolores de alta resolução.
Para usar mkbitmap
, transmita várias opções e um ou mais nomes de arquivo. Para todos os detalhes, consulte a página do manual da ferramenta:
$ mkbitmap [options] [filename...]


mkbitmap -f 2 -s 2 -t 0.48
(Fonte).Acessar o código
A primeira etapa é conseguir o código-fonte de mkbitmap
. Ele está disponível no site do projeto. No momento da redação deste artigo, 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 entender como ela se comporta. O arquivo INSTALL
contém as seguintes instruções:
cd
para o diretório que contém o código-fonte do pacote e digite./configure
para configurar o pacote para seu sistema.A execução de
configure
pode levar algum tempo. Durante a execução, ele imprime algumas mensagens informando quais recursos estão sendo verificados.Digite
make
para compilar o pacote.Se quiser, digite
make check
para executar os autotestes que vêm com o pacote, geralmente usando os binários não instalados recém-criados.Digite
make install
para instalar os programas e todos os arquivos de dados e documentação. Ao instalar em um prefixo pertencente ao root, é recomendável que o pacote seja configurado e criado como um usuário comum, e apenas a fasemake install
seja executada com privilégios de root.
Ao seguir essas etapas, você vai ter dois executáveis, potrace
e mkbitmap
. Este artigo se concentra no segundo. Para verificar se ele funcionou corretamente, execute mkbitmap --version
. Esta é a saída de todas as quatro etapas da minha máquina, bastante reduzida para ser breve:
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ê receber os detalhes da versão, significa que compilou e instalou o mkbitmap
. Em seguida, faça o equivalente dessas etapas funcionar com o WebAssembly.
Compilar mkbitmap
para WebAssembly
O Emscripten é uma ferramenta para compilar programas C/C++ em WebAssembly. A documentação Building Projects do Emscripten afirma o seguinte:
É muito fácil criar projetos grandes com o Emscripten. O Emscripten oferece dois scripts simples que configuram seus makefiles para usar
emcc
como substituto direto degcc
. Na maioria dos casos, o restante do sistema de build atual do projeto permanece inalterado.
A documentação continua (um pouco editada para fins de brevidade):
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
Assim, ./configure
se torna emconfigure ./configure
e make
se torna emmake make
. Confira abaixo 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 der certo, agora haverá arquivos .wasm
em algum lugar do diretório. Para encontrá-los, execute find . -name "*.wasm"
:
$ find . -name "*.wasm"
./a.wasm
./src/mkbitmap.wasm
./src/potrace.wasm
Os dois últimos parecem promissores. Portanto, cd
no diretório src/
. Agora também há dois novos arquivos correspondentes, mkbitmap
e potrace
. Para este artigo, apenas mkbitmap
é relevante. O fato de não terem a extensão .js
é um pouco confuso, mas eles são arquivos JavaScript, o que pode ser verificado com uma chamada rápida de 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 para mkbitmap.js
chamando mv mkbitmap mkbitmap.js
(e mv potrace potrace.js
, respectivamente, se quiser).
Agora é hora do primeiro teste para conferir se funcionou. Execute 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 mkbitmap
para WebAssembly. Agora, a próxima etapa é fazer com que ele funcione no navegador.
mkbitmap
com WebAssembly no navegador
Copie os arquivos mkbitmap.js
e mkbitmap.wasm
para um novo diretório chamado mkbitmap
e crie um arquivo de modelo HTML 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 atenda ao diretório mkbitmap
e abra-o no navegador. Você vai encontrar uma solicitação para inserir informações. Isso é esperado, já que, de acordo com a página de manual da ferramenta, "se nenhum argumento de nome de arquivo for fornecido, o mkbitmap vai agir como um filtro, lendo da entrada padrão", que para o Emscripten é um prompt()
por padrão.
Impedir a execução automática
Para impedir que mkbitmap
seja executado imediatamente e, em vez disso, aguarde a entrada do usuário, é necessário 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 da 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 fez o aviso aparecer. Crie um script chamado script.js
, inclua-o antes do <script src="mkbitmap.js"></script>
em index.html
e adicione o seguinte código a script.js
. Ao recarregar o app, o aviso não vai mais aparecer.
var Module = {
// Don't run main() at page load
noInitialRun: true,
};
Criar um build modular com mais flags de build
Para fornecer entrada ao app, use a compatibilidade do sistema de arquivos do Emscripten em Module.FS
. A seção Incluindo suporte ao sistema de arquivos da documentação afirma:
O Emscripten decide se vai incluir suporte ao sistema de arquivos automaticamente. Muitos programas não precisam de arquivos, e o suporte ao sistema de arquivos não é insignificante em tamanho. Por isso, o Emscripten evita incluí-lo quando não há um motivo para isso. Isso significa que, se o código C/C++ não acessar 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, a compatibilidade com o sistema de arquivos será incluída automaticamente.
Infelizmente, mkbitmap
é um dos casos em que o Emscripten não inclui automaticamente o suporte ao sistema de arquivos. Portanto, você precisa informar explicitamente que quer fazer isso. Isso significa que você precisa seguir as etapas emconfigure
e emmake
descritas anteriormente, com mais algumas flags definidas por um argumento CFLAGS
. As flags a seguir também podem ser úteis para outros projetos.
- Defina
-sFILESYSTEM=1
para incluir a compatibilidade com o sistema de arquivos. - Defina
-sEXPORTED_RUNTIME_METHODS=FS,callMain
para queModule.FS
eModule.callMain
sejam exportados. - Defina
-sMODULARIZE=1
e-sEXPORT_ES6
para gerar um módulo ES6 moderno. - Defina
-sINVOKE_RUN=0
para evitar a execução inicial que fez o aviso aparecer.
Além disso, nesse caso específico, você precisa definir a flag --host
como wasm32
para informar ao script configure
que você está compilando para WebAssembly.
O comando emconfigure
final fica assim:
$ emconfigure ./configure --host=wasm32 CFLAGS='-sFILESYSTEM=1 -sEXPORTED_RUNTIME_METHODS=FS,callMain -sMODULARIZE=1 -sEXPORT_ES6 -sINVOKE_RUN=0'
Não se esqueça de executar emmake make
novamente e copiar 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ê vai ver o objeto Module
registrado no console do DevTools, e o aviso não vai mais aparecer, 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ê executasse mkbitmap -v
na linha de comando, chamaria Module.callMain(['-v'])
no navegador. Isso registra o número da versão 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
A saída padrão (stdout
) é o console. No entanto, é possível redirecionar para outra coisa, por exemplo, uma função que armazena a saída em 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();
Colocar o arquivo de entrada no sistema de arquivos da memória
Para colocar o arquivo de entrada no sistema de arquivos da memória, você precisa do equivalente a mkbitmap filename
na linha de comando. Para entender minha abordagem, primeiro vamos falar sobre como o mkbitmap
espera a entrada e cria a saída.
Os formatos de entrada compatíveis de mkbitmap
são PNM (PBM, PGM, PPM) e BMP. Os formatos de saída são PBM para bitmaps e PGM para mapas de tons de cinza. 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 de arquivo de entrada example.bmp
, o nome de arquivo de saída seria example.pbm
.
O Emscripten oferece um sistema de arquivos virtual que simula o sistema de arquivos local. Assim, o código nativo que usa APIs de arquivos síncronas pode ser compilado e executado com pouca ou nenhuma mudança.
Para que o mkbitmap
leia um arquivo de entrada como se ele fosse transmitido como um argumento de linha de comando filename
, use o objeto FS
fornecido pelo Emscripten.
O objeto FS
é apoiado por 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 na amostra de código a seguir.
Para verificar se a operação de gravação de arquivo funcionou, execute a função readdir()
do objeto FS
com o parâmetro '/'
. Você vai encontrar example.bmp
e vários arquivos padrão que são sempre criados automaticamente.
A chamada anterior para Module.callMain(['-v'])
para imprimir o número da versão foi removida. Isso ocorre porque Module.callMain()
é uma função que geralmente espera 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 da pasta '/'
do MEMFS. O arquivo de saída example.pbm
recém-criado vai aparecer 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 na memória
A função readFile()
do objeto FS
permite extrair o example.pbm
criado na última etapa do sistema de arquivos na memória. A função retorna um Uint8Array
que você converte em um objeto File
e salva no disco, já que os navegadores geralmente não são compatíveis com arquivos PBM para visualização direta no navegador.
Há maneiras mais elegantes de salvar um arquivo, mas usar um <a download>
criado dinamicamente é a mais aceita. Depois que o arquivo for salvo, abra-o 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é agora, o arquivo de entrada foi codificado 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 é particularmente difícil de analisar. Portanto, com algum código JavaScript, você pode até mostrar uma prévia 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 mkbitmap
para WebAssembly e fez com que ele funcionasse no navegador. Houve alguns becos sem saída, e você teve que compilar a ferramenta mais de uma vez até que ela funcionasse, mas, como escrevi acima, isso faz parte da experiência. Lembre-se também da tag webassembly
do StackOverflow se tiver dificuldades. Boa compilação!
Agradecimentos
Este artigo foi revisado por Sam Clegg e Rachel Andrew.