O compilador faz parte do
Emscripten . Mas e se você remover todos os apitos e deixar apenas isso?
Emscripten é necessário para compilar C / C ++ no
WebAssembly . Mas isso é muito mais do que apenas um compilador. O objetivo do Emscripten é substituir completamente o compilador C / C ++ e executar o código na Web que
não foi originalmente
projetado para a Web. Para isso, o Emscripten emula todo o sistema operacional POSIX. Se o programa usar
fopen () , o Emscripten fornecerá emulação do sistema de arquivos. Se o OpenGL for usado, o Emscripten fornecerá um contexto GL compatível com C suportado pelo
WebGL . Isso é muito trabalho e muito código que precisará ser implementado no pacote final. Mas você pode apenas ... removê-lo?
O
compilador real no kit de ferramentas Emscripten é LLVM. É ele quem traduz o código C no código de bytes do WebAssembly. Esta é uma estrutura modular moderna para a análise, transformação e otimização de programas. O LLVM é modular no sentido de nunca ser compilado diretamente no código da máquina. Em vez disso, o
compilador front-end interno gera
uma representação intermediária (IR). Essa representação intermediária, de fato, é chamada LLVM, uma abreviação de Máquina Virtual de Baixo Nível, daí o nome do projeto.
O
compilador de back-end converte o IR no código da máquina host. A vantagem dessa separação estrita é que novas arquiteturas são suportadas pela adição "simples" de um novo compilador. Nesse sentido, o WebAssembly é apenas um dos muitos objetivos de compilação suportados pelo LLVM e, por algum tempo, foi ativado com um sinalizador especial. A partir do LLVM 8, o destino de compilação do WebAssembly está disponível por padrão.
No MacOS, você pode instalar o LLVM usando o
homebrew :
$ brew install llvm $ brew link --force llvm
Verifique o suporte do WebAssembly:
$ llc --version LLVM (http://llvm.org/): LLVM version 8.0.0 Optimized build. Default target: x86_64-apple-darwin18.5.0 Host CPU: skylake Registered Targets:
Parece que estamos prontos!
Compilando C da maneira mais difícil
Nota: aqui estão alguns formatos RAW WebAssembly de baixo nível. Se você acha difícil entender, isso é normal. O bom uso do WebAssembly não requer a compreensão de todo o texto deste artigo. Se você estiver procurando por código para copiar e colar, consulte a chamada para o compilador na seção Otimização . Mas se você estiver interessado, continue lendo! Eu escrevi anteriormente uma introdução ao Webassembly puro e ao WAT: estes são os princípios básicos necessários para entender este post.
Aviso: vou me afastar um pouco do padrão e tentar usar formatos legíveis por humanos a cada passo (na medida do possível). Nosso programa aqui será muito simples, a fim de evitar situações de fronteira e não se distrair:
Que façanha de engenharia magnífica! Especialmente porque o programa é chamado de
adição , mas na realidade ele não
adiciona nada (não adiciona). Mais importante: o programa não usa a biblioteca padrão e, dos tipos aqui, apenas 'int'.
Transformando C em uma visualização LLVM interna
O primeiro passo é transformar nosso programa C em LLVM IR. Esta é a tarefa do compilador de front-end do
clang
, que é instalado com o LLVM:
clang \ --target=wasm32 \
E, como resultado, obtemos
add.ll
com uma representação interna do LLVM IR.
Eu o mostro apenas por uma questão de perfeição . Ao trabalhar com WebAssembly ou até mesmo clang, você como desenvolvedor C nunca entrará em contato com o LLVM IR.
; ModuleID = 'add.c' source_filename = "add.c" target datalayout = "em:ep:32:32-i64:64-n32:64-S128" target triple = "wasm32" ; Function Attrs: norecurse nounwind readnone define hidden i32 @add(i32, i32) local_unnamed_addr #0 { %3 = mul nsw i32 %0, %0 %4 = add nsw i32 %3, %1 ret i32 %4 } attributes #0 = { norecurse nounwind readnone "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-frame-pointer-elim"="false" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="generic" "unsafe-fp-math"="false" "use-soft-float"="false" } !llvm.module.flags = !{!0} !llvm.ident = !{!1} !0 = !{i32 1, !"wchar_size", i32 4} !1 = !{!"clang version 8.0.0 (tags/RELEASE_800/final)"}
O LLVM IR está cheio de metadados e anotações adicionais, o que permite ao compilador tomar decisões mais informadas ao gerar código de máquina.Transforme LLVM IR em arquivos de objeto
A próxima etapa é chamar o compilador de back-end
llc
para criar um arquivo de objeto a partir da representação interna.
O arquivo de saída
add.o
já é um módulo WebAssembly válido que contém todo o código compilado do nosso arquivo C. Mas geralmente você não poderá executar arquivos de objetos porque não possuem partes essenciais.
Se omitimos
-filetype=obj
no comando, obteríamos o montador LLVM para WebAssembly, um formato legível por humanos que é um pouco semelhante ao WAT. No entanto, a ferramenta
llvm-mc
para trabalhar com esses arquivos ainda não suporta totalmente o formato e geralmente não pode processar arquivos. Portanto, desmontamos os arquivos de objeto após o fato. Uma ferramenta específica é necessária para verificar esses arquivos de objeto. No caso do WebAssembly, era
wasm-objdump
, parte do
WebAssembly Binary Toolkit, ou abreviação.
$ brew install wabt
A saída mostra que nossa função add () está neste módulo, mas também contém seções
personalizadas com metadados e, surpreendentemente, várias importações. No próximo estágio de
vinculação, as seções personalizadas serão analisadas e excluídas e o vinculador (vinculador) lidará com a importação.
Layout
Tradicionalmente, a tarefa do vinculador é reunir vários arquivos de objeto em um arquivo executável. O vinculador LLVM é chamado
lld
e é chamado com o link simbólico de destino. Para o WebAssembly, este
wasm-ld
.
wasm-ld \ --no-entry \
O resultado é um módulo WebAssembly de 262 bytes de tamanho.
Lançamento
Obviamente, o mais importante é ver que tudo
realmente funciona. Como no
último artigo , você pode usar algumas linhas de JavaScript incorporado para carregar e executar este módulo WebAssembly.
<!DOCTYPE html> <script type="module"> async function init() { const { instance } = await WebAssembly.instantiateStreaming( fetch("./add.wasm") ); console.log(instance.exports.add(4, 1)); } init(); </script>
Se estiver tudo bem, você verá o número 17. No console do DevTool.
Acabamos de compilar C com sucesso no WebAssembly sem tocar em Emscripten. Também é importante notar que não há middleware para configurar e carregar o módulo WebAssembly.
Compilar C é um pouco mais simples
Para compilar C no WebAssembly, demos vários passos. Como eu disse, para fins educacionais, examinamos em detalhes todas as etapas. Vamos pular os formatos intermediários legíveis por humanos e aplicar imediatamente o compilador C como um canivete suíço, como foi desenvolvido:
clang \ --target=wasm32 \ -nostdlib \
Aqui temos o mesmo arquivo
.wasm
, mas com um comando.
Otimização
Dê uma olhada no WAT do nosso módulo
wasm2wat
executando
wasm2wat
:
(module (type (;0;) (func)) (type (;1;) (func (param i32 i32) (result i32))) (func $__wasm_call_ctors (type 0)) (func $add (type 1) (param i32 i32) (result i32) (local i32 i32 i32 i32 i32 i32 i32 i32) global.get 0 local.set 2 i32.const 16 local.set 3 local.get 2 local.get 3 i32.sub local.set 4 local.get 4 local.get 0 i32.store offset=12 local.get 4 local.get 1 i32.store offset=8 local.get 4 i32.load offset=12 local.set 5 local.get 4 i32.load offset=12 local.set 6 local.get 5 local.get 6 i32.mul local.set 7 local.get 4 i32.load offset=8 local.set 8 local.get 7 local.get 8 i32.add local.set 9 local.get 9 return) (table (;0;) 1 1 anyfunc) (memory (;0;) 2) (global (;0;) (mut i32) (i32.const 66560)) (global (;1;) i32 (i32.const 66560)) (global (;2;) i32 (i32.const 1024)) (global (;3;) i32 (i32.const 1024)) (export "memory" (memory 0)) (export "__wasm_call_ctors" (func $__wasm_call_ctors)) (export "__heap_base" (global 1)) (export "__data_end" (global 2)) (export "__dso_handle" (global 3)) (export "add" (func $add)))
Uau, que ótimo código. Para minha surpresa, o módulo usa memória (como visto nas
i32.store
e
i32.store
), oito variáveis locais e diversas globais. Provavelmente, você pode escrever manualmente uma versão mais concisa. Este programa é muito grande porque não aplicamos nenhuma otimização. Vamos fazer isso:
clang \ --target=wasm32 \ + -O3 \
Nota: tecnicamente, a otimização de layout (LTO) não oferece benefícios, pois compomos apenas um arquivo. Em grandes projetos, o LTO ajudará a reduzir significativamente o tamanho do arquivo.
Após executar esses comandos, o arquivo
.wasm
diminuiu de 262 para 197 bytes, e o WAT também se tornou muito mais simples:
(module (type (;0;) (func)) (type (;1;) (func (param i32 i32) (result i32))) (func $__wasm_call_ctors (type 0)) (func $add (type 1) (param i32 i32) (result i32) local.get 0 local.get 0 i32.mul local.get 1 i32.add) (table (;0;) 1 1 anyfunc) (memory (;0;) 2) (global (;0;) (mut i32) (i32.const 66560)) (global (;1;) i32 (i32.const 66560)) (global (;2;) i32 (i32.const 1024)) (global (;3;) i32 (i32.const 1024)) (export "memory" (memory 0)) (export "__wasm_call_ctors" (func $__wasm_call_ctors)) (export "__heap_base" (global 1)) (export "__data_end" (global 2)) (export "__dso_handle" (global 3)) (export "add" (func $add)))
Ligue para a biblioteca padrão
Usar C sem a biblioteca libc padrão parece bastante rude. É lógico adicioná-lo, mas vou ser sincero:
não será fácil.
De fato, não chamamos diretamente nenhuma biblioteca libc no artigo . Existem vários adequados, especialmente
glibc ,
musl e
dietlibc . No entanto, a maioria dessas bibliotecas deve ser executada no sistema operacional POSIX, o que implementa um determinado conjunto de chamadas do sistema. Como não temos uma interface de kernel em JavaScript, teremos que implementar essas chamadas de sistema POSIX, provavelmente por JavaScript. Esta é uma tarefa difícil e não vou fazer aqui. A boa notícia é que é
isso que a Emscripten faz por você .
Obviamente, nem todas as funções libc dependem de chamadas do sistema. Funções como
strlen()
,
sin()
ou mesmo
memset()
são implementadas no C. simples. Isso significa que você pode usar essas funções ou apenas copiar / colar sua implementação de alguma biblioteca mencionada.
Memória dinâmica
Sem libc, interfaces fundamentais em C, como
malloc()
e
free()
não estão disponíveis para nós. No WAT não otimizado, vimos que o compilador usa memória, se necessário. Isso significa que não podemos simplesmente usar a memória como quisermos, sem correr o risco de danificá-la. Você precisa entender como é usado.
Modelos de memória LLVM
O método de segmentação de memória do WebAssembly surpreenderá um pouco os programadores experientes. Em primeiro lugar, no WebAssembly, um endereço nulo é tecnicamente permitido, mas muitas vezes ainda é tratado como um erro. Em segundo lugar, a pilha vem primeiro e cresce (para endereços mais baixos), e o heap aparece mais tarde e cresce. O motivo é que a memória do WebAssembly pode aumentar em tempo de execução. Isso significa que não há um final fixo para acomodar a pilha ou a pilha.
Aqui está o layout
wasm-ld
:
A pilha cresce e a pilha cresce. A pilha começa com __data_end
e o heap __heap_base
com __heap_base
. Como a pilha é colocada em primeiro lugar, é limitada pelo tamanho máximo definido durante a compilação, ou seja, __heap_base
menos __data_end
Se voltarmos e examinarmos a seção global em nosso WAT, encontraremos estes valores:
__heap_base
definido como 66560 e
__data_end
está definido como 1024. Isso significa que a pilha pode crescer até um máximo de 64 KiB, o que não é muito. Felizmente, o
wasm-ld
permite alterar este valor:
clang \ --target=wasm32 \ -O3 \ -flto \ -nostdlib \ -Wl,--no-entry \ -Wl,--export-all \ -Wl,--lto-O3 \ + -Wl,-z,stack-size=$[8 * 1024 * 1024] \
Montagem do alocador
A área de heap é conhecida por começar com
__heap_base
. Como a função
malloc()
está ausente, sabemos que a próxima área de memória pode ser usada com segurança. Podemos colocar os dados lá como quisermos, e não é preciso ter medo de corrupção de memória, pois a pilha cresce na outra direção. No entanto, um heap gratuito para todos pode ficar rapidamente entupido; portanto, geralmente é necessário algum tipo de gerenciamento dinâmico de memória. Uma opção é adotar uma implementação completa do malloc (), como
a implementação do malloc de Doug Lee , que é usada no Emscripten. Existem várias implementações mais pequenas com várias compensações.
Mas por que não escrever seu próprio
malloc()
? Estamos tão atolados que não faz diferença. Um dos mais simples é um alocador de resposta: é super rápido, extremamente pequeno e fácil de implementar. Mas há uma desvantagem: você não pode liberar memória. Embora à primeira vista esse alocador pareça incrivelmente inútil, mas ao desenvolver o
Squoosh, deparei- me com precedentes onde seria uma excelente escolha. O conceito de um alocador de resposta é que armazenamos o endereço inicial da memória não utilizada como global. Se o programa solicitar
n
bytes de memória, movemos o marcador para
n
retornamos o valor anterior:
extern unsigned char __heap_base; unsigned int bump_pointer = &__heap_base; void* malloc(int n) { unsigned int r = bump_pointer; bump_pointer += n; return (void *)r; } void free(void* p) {
As variáveis globais do WAT são realmente definidas por
wasm-ld
, para que possamos acessá-las a partir do nosso código C como variáveis comuns, se as declararmos
extern
. Então,
acabamos de escrever nosso próprio malloc()
... em cinco linhas de C.Nota: nosso alocador de resposta não é totalmente compatível com malloc()
de C. Por exemplo, não oferecemos garantias de alinhamento. Mas funciona bem o suficiente, então ...
Uso dinâmico de memória
Para testar, vamos criar uma função C, que pega uma matriz de números de tamanho arbitrário e calcula a soma. Não é muito interessante, mas isso nos força a usar memória dinâmica, pois não sabemos o tamanho da matriz durante a montagem:
int sum(int a[], int len) { int sum = 0; for(int i = 0; i < len; i++) { sum += a[i]; } return sum; }
A função sum (), espero, é bem direta. Uma pergunta mais interessante é como passar uma matriz do JavaScript para o WebAssembly - afinal, o WebAssembly entende apenas números. A idéia geral é usar
malloc()
do JavaScript para alocar um pedaço de memória, copiar os valores lá e passar o endereço (número!)
Onde a matriz está localizada:
<!DOCTYPE html> <script type="module"> async function init() { const { instance } = await WebAssembly.instantiateStreaming( fetch("./add.wasm") ); const jsArray = [1, 2, 3, 4, 5]; </script>
Após iniciar, você verá a resposta 15 no console do DevTools, que é realmente a soma de todos os números de 1 a 5.
Conclusão
Então, você lê até o fim. Parabéns! Novamente, se você se sentir um pouco sobrecarregado, tudo está em ordem.
Não é necessário ler todos os detalhes. Compreendê-los é totalmente opcional para um bom desenvolvedor web e nem é necessário para o excelente uso do WebAssembly . Mas eu queria compartilhar essas informações, porque elas permitem que você realmente aprecie todo o trabalho que um projeto como o
Emscripten faz para você. Ao mesmo tempo, isso mostra como os módulos puramente computacionais do WebAssembly podem ser pequenos. O módulo Wasm para somar a matriz tem apenas 230 bytes de tamanho,
incluindo um alocador de memória dinâmico . Compilar o mesmo código com o Emscripten produzirá 100 bytes de código do WebAssembly e 11K de código de link JavaScript. Você precisa tentar obter esse resultado, mas há situações em que vale a pena.