Como você integra o WebAssembly a essa configuração? Neste artigo, vamos resolver isso usando C/C++ e Emscripten como exemplo.
WebAssembly (Wasm) é frequentemente como um primitivo de desempenho ou uma forma de executar seu código base de código na Web. Com o squoosh.app, queríamos mostrar que há pelo menos uma terceira perspectiva para o Wasm: usar a enorme ecossistemas de outras linguagens de programação. Com Emscripten, use código C/C++, O Rust tem suporte para Wasm integrado, e o recurso Go está trabalhando nisso também. estou que muitos outros idiomas virão.
Nesses cenários, o Wasm não é o elemento central do app, mas sim um quebra-cabeça. pedaço: mais um módulo. Seu aplicativo já tem JavaScript, CSS, recursos de imagem, sistema de build centrado na Web e talvez até mesmo um framework como o React. Como você integrar o WebAssembly nessa configuração? Neste artigo, vamos abordar isso usando C/C++ e Emscripten como exemplo.
Docker
Achei o Docker inestimável ao trabalhar com a Emscripten. C/C++ As bibliotecas costumam ser criadas para funcionar com o sistema operacional em que são criadas. É incrivelmente útil ter um ambiente consistente. Com o Docker, você tem uma virtualizado do Linux, que já está configurado para funcionar com a Emscripten e tem todas as ferramentas e dependências instaladas. Se faltar algo, basta instalá-lo sem se preocupar com como ele afeta sua própria máquina outros projetos. Se algo der errado, jogue o contêiner e comece de novo. Se ele funcionar uma vez, pode ter certeza de que continuará funcionando e a produzir resultados idênticos.
O Docker Registry tem um arquivo Emscripten imagem de trzeci que tenho usado bastante.
Integração com npm
Na maioria dos casos, o ponto de entrada para um projeto da Web é o
package.json
: Por convenção, a maioria dos projetos pode ser criada com npm install &&
npm run build
.
Em geral, os artefatos de build produzidos pela Emscripten (um .js
e um .wasm
)
) deve ser tratado apenas como outro módulo JavaScript e como
ativo. O arquivo JavaScript pode ser processado por um bundler como webpack ou rollup,
e o arquivo wasm deve ser tratado como qualquer outro ativo binário maior, como
de imagens de contêiner.
Dessa forma, os artefatos de compilação Emscripten precisam ser construídos antes da O processo de build é iniciado:
{
"name": "my-worldchanging-project",
"scripts": {
"build:emscripten": "docker run --rm -v $(pwd):/src trzeci/emscripten
./build.sh",
"build:app": "<the old build command>",
"build": "npm run build:emscripten && npm run build:app",
// ...
},
// ...
}
A nova tarefa build:emscripten
pode invocar o Emscripten diretamente, mas
mencionei antes, recomendo usar o Docker para garantir que o ambiente de build seja
consistentes.
docker run ... trzeci/emscripten ./build.sh
diz ao Docker para ativar um novo
contêiner usando a imagem trzeci/emscripten
e execute o comando ./build.sh
.
build.sh
é um script de shell que você vai criar a seguir. --rm
diz
Docker para excluir o contêiner após a conclusão da execução. Dessa forma, você não cria
uma coleção de imagens de máquina desatualizadas ao longo do tempo. -v $(pwd):/src
significa que
você quer que o Docker seja "espelhado" diretório atual ($(pwd)
) para /src
dentro
o contêiner. Todas as mudanças feitas nos arquivos do diretório /src
dentro da
contêiner será espelhado no projeto real. Esses diretórios espelhados
são chamadas de "montagens de vinculação".
Vamos dar uma olhada em build.sh
:
#!/bin/bash
set -e
export OPTIMIZE="-Os"
export LDFLAGS="${OPTIMIZE}"
export CFLAGS="${OPTIMIZE}"
export CXXFLAGS="${OPTIMIZE}"
echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
(
# Compile C/C++ code
emcc \
${OPTIMIZE} \
--bind \
-s STRICT=1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s MALLOC=emmalloc \
-s MODULARIZE=1 \
-s EXPORT_ES6=1 \
-o ./my-module.js \
src/my-module.cpp
# Create output folder
mkdir -p dist
# Move artifacts
mv my-module.{js,wasm} dist
)
echo "============================================="
echo "Compiling wasm bindings done"
echo "============================================="
Há muito o que discutir aqui!
set -e
coloca o shell no modo "fail fast" modo Se algum comando no script
retornar um erro, todo o script será abortado imediatamente. Isso pode ser
incrivelmente útil, pois o último resultado do script será sempre um sucesso
ou o erro que causou a falha do build.
Com as instruções export
, você define os valores de alguns parâmetros
variáveis. Elas permitem que você passe parâmetros adicionais de linha de comando para o comando
compilador (CFLAGS
), o compilador C++ (CXXFLAGS
) e o vinculador (LDFLAGS
).
Todos recebem as configurações do otimizador via OPTIMIZE
para garantir que
tudo é otimizado da mesma maneira. Há alguns valores possíveis
para a variável OPTIMIZE
:
-O0
: não faz otimização. Nenhum código morto é eliminado, e o Emscripten também não reduz o código JavaScript que emite. Bom para depuração.-O3
: otimize de forma agressiva para melhorar o desempenho.-Os
: otimize de forma intensa para desempenho e tamanho como secundário. critério.-Oz
: otimize de forma agressiva em relação ao tamanho, sacrificando o desempenho, se necessário.
Para a Web, recomendamos principalmente o -Os
.
O comando emcc
tem uma infinidade de opções próprias. Observe que emcc é
deve ser uma "substituição simples" para compiladores como GCC ou clang. Então, tudo
que você precisa saber no GCC provavelmente serão implementadas por emcc como
muito bem. A flag -s
é especial porque permite configurar o Emscripten
especificamente. Todas as opções disponíveis podem ser encontradas na
settings.js
,
mas esse arquivo pode sobrecarregar tudo. Aqui está uma lista das bandeiras Emscripten
que considero mais importantes para os desenvolvedores Web:
- O
--bind
ativa embind. -s STRICT=1
descarta o suporte a todas as opções de build descontinuadas. Isso garante que seu código crie de maneira compatível com versões futuras.-s ALLOW_MEMORY_GROWTH=1
permite que a memória aumente automaticamente se necessários. No momento em que este artigo foi escrito, o Emscripten vai alocar 16 MB de memória inicialmente. À medida que seu código aloca blocos de memória, essa opção decide se essas operações farão com que todo o módulo Wasm falhe quando a memória for exausto, ou se o código agrupador tiver permissão para expandir a memória total para para acomodar a alocação.-s MALLOC=...
escolhe qual implementação demalloc()
usar.emmalloc
é uma implementação pequena e rápida demalloc()
especificamente para o Emscripten. A A alternativa é adlmalloc
, uma implementação completa demalloc()
. Você só precisa Mude paradlmalloc
se estiver alocando muitos objetos pequenos. com frequência ou se quiser usar linhas de execução.- O
-s EXPORT_ES6=1
transforma o código JavaScript em um módulo ES6 com uma uma exportação padrão que funcione com qualquer bundler. Também requer-s MODULARIZE=1
para ser definido.
As sinalizações a seguir nem sempre são necessárias ou úteis apenas para depuração. finalidades:
-s FILESYSTEM=0
é uma flag relacionada ao Emscripten e à capacidade de emular um sistema de arquivos para você quando seu código C/C++ usar operações de sistema de arquivos. Ele analisa o código que compila para decidir se deve incluir os emulação de sistema de arquivos no código cola ou não. Às vezes, no entanto, a análise pode errar, e você paga uns 70 KB grandes em cola adicional. para uma emulação de sistema de arquivos que talvez você não precise. Com-s FILESYSTEM=0
, é possível forçar o Emscripten a não incluir esse código.- O
-g4
vai fazer com que o Emscripten inclua informações de depuração nos.wasm
e também emite um arquivo de mapas de origem para o módulo Wasm. Você pode ler mais em de depuração com o Emscripten na seção de depuração .
Pronto! Para testar essa configuração, vamos criar um pequeno my-module.cpp
:
#include <emscripten/bind.h>
using namespace emscripten;
int say_hello() {
printf("Hello from your wasm module\n");
return 0;
}
EMSCRIPTEN_BINDINGS(my_module) {
function("sayHello", &say_hello);
}
E uma index.html
:
<!doctype html>
<title>Emscripten + npm example</title>
Open the console to see the output from the wasm module.
<script type="module">
import wasmModule from "./my-module.js";
const instance = wasmModule({
onRuntimeInitialized() {
instance.sayHello();
}
});
</script>
(Aqui está um gist (em inglês) que contém todos os arquivos.
Para criar tudo, execute
$ npm install
$ npm run build
$ npm run serve
Ao navegar para localhost:8080, você verá a seguinte saída no Console do DevTools:
Como adicionar código C/C++ como uma dependência
Se você quiser criar uma biblioteca C/C++ para seu app da Web, será necessário que o código seja
parte do seu projeto. É possível adicionar o código ao repositório do projeto manualmente
ou você também pode usar o npm para gerenciar esse tipo de dependências. Digamos que eu
quero usar libvpx no meu webapp. libvpx
é uma biblioteca C++ para codificar imagens com VP8, o codec usado em arquivos .webm
.
No entanto, a libvpx não está no npm e não tem um package.json
, então não posso
e instalá-lo usando npm diretamente.
Para sair desse enigma, há
napa. O napa permite que você instale
URL do repositório como uma dependência na sua pasta node_modules
.
Instale o napa como uma dependência:
$ npm install --save napa
e execute napa
como um script de instalação:
{
// ...
"scripts": {
"install": "napa",
// ...
},
"napa": {
"libvpx": "git+https://github.com/webmproject/libvpx"
}
// ...
}
Quando você executa npm install
, o napa cuida da clonagem do libvpx do GitHub
repositório em node_modules
com o nome libvpx
.
Agora você pode estender seu script de compilação para criar o libvpx. O libvpx usa configure
e make
sejam criados. A Emscripten pode ajudar a garantir que configure
e
make
usam o compilador do Emscripten. Para isso, há o wrapper
comandos emconfigure
e emmake
:
# ... above is unchanged ...
echo "============================================="
echo "Compiling libvpx"
echo "============================================="
(
rm -rf build-vpx || true
mkdir build-vpx
cd build-vpx
emconfigure ../node_modules/libvpx/configure \
--target=generic-gnu
emmake make
)
echo "============================================="
echo "Compiling libvpx done"
echo "============================================="
echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
# ... below is unchanged ...
Uma biblioteca C/C++ é dividida em duas partes: os cabeçalhos (normalmente .h
ou
.hpp
) que definem as estruturas de dados, classes, constantes etc. que um
expõe a biblioteca e a biblioteca real (tradicionalmente, arquivos .so
ou .a
). Para
usar a constante VPX_CODEC_ABI_VERSION
da biblioteca no código, você terá
para incluir os arquivos principais da biblioteca usando uma instrução #include
:
#include "vpxenc.h"
#include <emscripten/bind.h>
int say_hello() {
printf("Hello from your wasm module with libvpx %d\n", VPX_CODEC_ABI_VERSION);
return 0;
}
O problema é que o compilador não sabe onde procurar por vpxenc.h
.
É para isso que serve a flag -I
. Ele informa ao compilador quais diretórios
verifique se há arquivos de cabeçalho. Além disso, também é necessário fornecer ao compilador a
arquivo de biblioteca real:
# ... above is unchanged ...
echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
(
# Compile C/C++ code
emcc \
${OPTIMIZE} \
--bind \
-s STRICT=1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s ASSERTIONS=0 \
-s MALLOC=emmalloc \
-s MODULARIZE=1 \
-s EXPORT_ES6=1 \
-o ./my-module.js \
-I ./node_modules/libvpx \
src/my-module.cpp \
build-vpx/libvpx.a
# ... below is unchanged ...
Se você executar npm run build
agora, vai notar que o processo cria um novo .js
.
e um novo arquivo .wasm
e que a página de demonstração vai gerar a constante:
O processo de build leva muito tempo. O motivo
os tempos de build longos podem variar. No caso do libvpx, isso leva muito tempo
ele compila um codificador e um decodificador para o VP8 e o VP9 sempre que você executa
ao comando de compilação, mesmo que os arquivos de origem não tenham sido alterados. Mesmo um pequeno
no my-module.cpp
vai levar muito tempo para ser criada. Seria muito
é útil manter os artefatos de compilação do libvpx por perto quando forem
criado pela primeira vez.
Uma maneira de fazer isso é usando variáveis de ambiente.
# ... above is unchanged ...
eval $@
echo "============================================="
echo "Compiling libvpx"
echo "============================================="
test -n "$SKIP_LIBVPX" || (
rm -rf build-vpx || true
mkdir build-vpx
cd build-vpx
emconfigure ../node_modules/libvpx/configure \
--target=generic-gnu
emmake make
)
echo "============================================="
echo "Compiling libvpx done"
echo "============================================="
# ... below is unchanged ...
(Confira um gist que contém todos os arquivos.
Com o comando eval
, é possível definir variáveis de ambiente transmitindo parâmetros.
ao script de compilação. O comando test
ignora a criação do libvpx se
$SKIP_LIBVPX
é definido (com qualquer valor).
Agora você pode compilar o módulo, mas sem recriar o libvpx:
$ npm run build:emscripten -- SKIP_LIBVPX=1
Como personalizar o ambiente de build
Às vezes, as bibliotecas dependem de outras ferramentas para a criação. Se essas dependências
estiverem ausentes no ambiente de build fornecido pela imagem Docker, você precisará
e adicioná-las você mesmo. Como exemplo, digamos que você também queira criar o
documentação do libvpx usando doxygen. O doxigênio não é
disponível no contêiner do Docker, mas é possível instalá-lo usando apt
.
Se você fizer isso no build.sh
, faça o download e a instalação outra vez.
doxygen toda vez que quiser criar sua biblioteca. Não apenas isso seria
desperdício, mas também impediria você de trabalhar em seu projeto enquanto estiver off-line.
Aqui faz sentido criar sua própria imagem Docker. As imagens Docker são criadas
criando um Dockerfile
que descreve as etapas de build. Os Dockerfiles são
poderosas e têm muitos
comandos, mas a maioria dos
é possível usar apenas FROM
, RUN
e ADD
. Nesse caso:
FROM trzeci/emscripten
RUN apt-get update && \
apt-get install -qqy doxygen
Com FROM
, é possível declarar qual imagem do Docker você quer usar como ponto de partida
ponto Escolhi trzeci/emscripten
como base: a imagem que você está usando
o tempo todo. Com RUN
, você instrui o Docker a executar comandos do shell dentro do
contêiner do Docker. As mudanças que esses comandos fazem no contêiner agora fazem parte do
a imagem Docker. Para verificar se a imagem Docker foi criada e está
disponível antes de executar o build.sh
, você precisa ajustar os package.json
pouco:
{
// ...
"scripts": {
"build:dockerimage": "docker image inspect -f '.' mydockerimage || docker build -t mydockerimage .",
"build:emscripten": "docker run --rm -v $(pwd):/src mydockerimage ./build.sh",
"build": "npm run build:dockerimage && npm run build:emscripten && npm run build:app",
// ...
},
// ...
}
(Confira um gist que contém todos os arquivos.
Isso criará sua imagem Docker, mas somente se ela ainda não tiver sido criada. Depois,
tudo será executado como antes, mas agora o ambiente de build tem a propriedade doxygen
disponível, o que fará com que a documentação de libvpx seja criada como
muito bem.
Conclusão
Não surpreende que o código C/C++ e o npm não sejam uma opção natural, mas você pode fazê-lo funcionar confortavelmente com algumas ferramentas adicionais e o isolamento fornecidos pelo Docker. Essa configuração não vai funcionar em todos os projetos, mas é uma que você possa ajustar de acordo com suas necessidades. Se você tiver melhorias, compartilhe.
Apêndice: como usar camadas de imagem do Docker
Uma solução alternativa é encapsular mais desses problemas com o Docker e a abordagem inteligente do Docker para armazenamento em cache. O Docker executa os Dockerfiles passo a passo e atribui ao resultado de cada etapa uma imagem própria. Essas imagens intermediárias são muitas vezes chamadas de "camadas". Se um comando em um Dockerfile não tiver sido alterado, o Docker essa etapa não será executada novamente quando você recompilar o Dockerfile. Em vez ela vai reutilizar a camada da última vez que a imagem foi criada.
Anteriormente, era necessário tentar não recriar o libvpx todas as vezes
durante a criação do seu app. Em vez disso, você pode mover as instruções de criação para libvpx
do build.sh
para o Dockerfile
para usar o armazenamento em cache do Docker
mecanismo de atenção:
FROM trzeci/emscripten
RUN apt-get update && \
apt-get install -qqy doxygen git && \
mkdir -p /opt/libvpx/build && \
git clone https://github.com/webmproject/libvpx /opt/libvpx/src
RUN cd /opt/libvpx/build && \
emconfigure ../src/configure --target=generic-gnu && \
emmake make
(Confira um gist que contém todos os arquivos.
Você precisa instalar o git e clonar o libvpx manualmente, porque não
vincular montagens ao executar docker build
. Como efeito colateral, não é necessário
mais um napa.