
O anúncio do WebAssembly ocorreu em 2015 - mas agora, depois de anos, ainda há poucos que podem se orgulhar dele na produção. Os materiais dessa experiência são ainda mais valiosos: as informações em primeira mão sobre como conviver com ela na prática ainda são escassas.
Na conferência do HolyJS, um relatório sobre a experiência do uso do WebAssembly recebeu notas altas da platéia e agora uma versão em texto deste relatório foi preparada especialmente para Habr (um vídeo também é anexado).
Meu nome é Andrey, vou falar sobre o WebAssembly. Podemos dizer que comecei a me envolver na web no século passado, mas sou modesto, então não digo isso. Durante esse período, consegui trabalhar tanto no backend quanto no frontend, e até desenhei um pouco de design. Hoje estou interessado em coisas como WebAssembly, C ++ e outras coisas nativas. Também gosto muito de tipografia e coleciono tecnologia antiga.
Primeiro, falarei sobre como a equipe e eu implementamos o WebAssembly em nosso projeto, depois discutiremos se você precisa de algo do WebAssembly e terminamos com algumas dicas, caso você queira implementá-lo por conta própria.
Como implementamos o WebAssembly
Trabalho na Inetra, estamos localizados em Novosibirsk e estamos realizando alguns de nossos próprios projetos. Um deles é o ByteFog. Essa é a tecnologia ponto a ponto para fornecer vídeo aos usuários. Nossos clientes são serviços que distribuem uma enorme quantidade de vídeo. Eles têm um problema: quando algum evento popular acontece, por exemplo, a conferência de imprensa de alguém ou algum evento esportivo, como não se preparar para isso, vários clientes vêm, apoiando-se no servidor e o servidor fica triste. Os clientes recebem uma qualidade de vídeo muito ruim no momento.
Mas todo mundo está assistindo o mesmo conteúdo. Vamos pedir aos dispositivos vizinhos dos usuários que compartilhem partes de vídeo e, em seguida, descarregaremos o servidor, economizaremos largura de banda e os usuários receberão vídeo com melhor qualidade. Essas nuvens são nossa tecnologia, nosso servidor proxy ByteFog.

Devemos estar instalados em todos os dispositivos que possam exibir vídeo, portanto, oferecemos suporte a uma ampla variedade de plataformas: Windows, Linux, Android, iOS, Web, Tizen. Qual idioma escolher para ter uma única base de código em todas essas plataformas? Escolhemos o C ++ porque ele apresenta as maiores vantagens :-D Mais seriamente, temos uma boa experiência em C ++, é realmente uma linguagem rápida e, em portabilidade, provavelmente perde apenas para C.
Temos uma aplicação bastante grande (900 classes), mas funciona bem. No Windows e Linux, compilamos em código nativo. Para Android e iOS, criamos uma biblioteca que conectamos ao aplicativo. Falaremos sobre Tizen outra vez, mas na Web costumávamos trabalhar como um plug-in de navegador.
Esta é a tecnologia da API do Netscape Plugin. Como o nome indica, é bastante antigo e também tem uma desvantagem: fornece acesso muito amplo ao sistema, portanto o código do usuário pode causar um problema de segurança. É provavelmente por isso que o Chrome desativou o suporte para essa tecnologia em 2015 e, em seguida, todos os navegadores se juntaram a esse flash mob. Por isso, ficamos sem uma versão da web por quase dois anos.
Em 2017, uma nova esperança veio. Como você pode imaginar, este é o WebAssembly. Como resultado, nos propusemos a tarefa de portar nosso aplicativo para um navegador. Desde que o suporte ao Firefox e Chrome já apareceu na primavera, e no outono de 2017, o Edge e o Safari se destacaram.
Era importante usarmos o código pronto, pois temos muita lógica de negócios que não queremos duplicar, para não duplicar o número de bugs. Pegue o compilador Emscripten. Ele faz o que precisamos - compila o aplicativo positivo no navegador e recria o ambiente familiar ao aplicativo nativo no navegador. Podemos dizer que o Emscripten é um código Browserify for C ++. Também permite encaminhar objetos de C ++ para JavaScript e vice-versa. Nosso primeiro pensamento foi: agora vamos usar o Emscripten, apenas compilar, e tudo funcionará. Claro que não. A partir daí começou nossa jornada ao longo do ancinho.
A primeira coisa que encontramos foi o vício. Havia várias bibliotecas em nossa base de código. Agora não faz sentido listá-las, mas para quem entende, temos o Boost. Essa é uma grande biblioteca que permite escrever código de plataforma cruzada, mas é muito difícil configurar a compilação com ele. Eu queria arrastar o mínimo de código possível para o navegador.
Arquitetura Bytefog
Como resultado, identificamos o núcleo: podemos dizer que este é um servidor proxy que contém a principal lógica de negócios. Este servidor proxy obtém dados de duas fontes. O primeiro e principal é o HTTP, ou seja, o canal para o servidor de distribuição de vídeo, o segundo é a nossa rede P2P, ou seja, um canal para outro mesmo proxy de outro usuário. Fornecemos os dados principalmente ao player, pois nossa tarefa é mostrar conteúdo de alta qualidade ao usuário. Se houver recursos, distribuímos o conteúdo para a rede P2P para que outros usuários possam baixá-lo. Dentro, há um cache inteligente que faz toda a mágica.

Depois de compilar tudo isso, somos confrontados com o fato de o WebAssembly ser executado na caixa de proteção do navegador. Isso significa que ele não pode fazer mais do que o JavaScript oferece. Enquanto os aplicativos nativos usam muitas coisas específicas da plataforma, como um sistema de arquivos, uma rede ou números aleatórios. Todos esses recursos terão que ser implementados em JavaScript usando o que o navegador nos fornece. Esta placa lista as substituições bastante óbvias listadas.

Para tornar isso possível, é necessário interromper a implementação de recursos nativos em um aplicativo nativo e inserir uma interface lá, ou seja, desenhar uma certa borda. Em seguida, você implementa isso em JavaScript e deixa a implementação nativa e, já durante a montagem, a necessária é selecionada. Então, examinamos nossa arquitetura e encontramos todos os lugares onde essa borda pode ser desenhada. Coincidentemente, este é um subsistema de transporte.

Para cada um desses locais, definimos uma especificação, ou seja, fixamos um contrato: quais métodos serão, quais parâmetros eles terão, que tipos de dados. Depois de fazer isso, você pode trabalhar em paralelo, cada desenvolvedor ao seu lado.
Qual é o resultado? Substituímos o principal canal de entrega de vídeo do provedor pelo AJAX usual. Emitimos dados para o player por meio da popular biblioteca HLS.js., mas há uma possibilidade fundamental de integração com outros players, se necessário. Substituímos toda a camada P2P pelo WebRTC.

Como resultado da compilação, vários arquivos são obtidos. O mais importante é o binário .wasm. Ele contém o código de código compilado que o navegador executará e que contém todo o seu legado em C ++. Mas, por si só, não funciona, o chamado "código de cola" é necessário, também é gerado pelo compilador. O código da cola está baixando um arquivo binário e você carrega esses dois arquivos para produção. Para fins de depuração, você pode gerar uma representação textual do assembler - um arquivo .wast e um mapa de origem. Você precisa entender que eles podem ser muito grandes. No nosso caso, eles atingiram 100 megabytes ou mais.
Coletando o Pacote Configurável
Vamos dar uma olhada no código da cola. Este é o bom e velho ES5 usual, reunido em um único arquivo. Quando o conectamos a uma página da web, temos uma variável global que contém todo o nosso módulo wasm instanciado, pronto para aceitar solicitações à sua API.
Mas incluir um arquivo separado é uma complicação bastante séria para a biblioteca que os usuários usarão. Gostaríamos de colocar tudo em um único pacote. Para isso, usamos o Webpack e uma opção de compilação especial MODULARIZE.
Ele agrupa o código adesivo no padrão “Módulo” e podemos buscá-lo: importar ou usar o require se escrevermos no ES5 - o Webpack entende calmamente essa dependência. Houve um problema com Babel - ele não gostou da grande quantidade de código, mas este é um código ES5, não precisa ser transposto, apenas o adicionamos para ignorar.
Em busca do número de arquivos, decidi usar a opção SINGLE_FILE. Ele converte todos os binários resultantes da compilação no formato Base64 e o envia para o código adesivo como uma sequência. Parece uma ótima idéia, mas depois disso o pacote ficou com 100 megabytes de tamanho. Nem o Webpack, nem o Babel, nem o navegador funcionam nesse volume. De qualquer forma, não forçaremos o usuário a carregar 100 megabytes ?!
Se você pensar bem, essa opção não é necessária. O código adesivo baixa arquivos binários por conta própria. Ele faz isso via HTTP, para obter o armazenamento em cache imediato, podemos definir os cabeçalhos que desejamos, por exemplo, ativar a compactação e os arquivos WebAssembly são perfeitamente compactados.
Mas a tecnologia mais legal é a compilação de streaming. Ou seja, o arquivo WebAssembly, durante o download do servidor, já pode ser compilado no navegador à medida que os dados chegam, e isso acelera bastante o carregamento do seu aplicativo. Em geral, toda a tecnologia WebAssembly se concentra no início rápido de uma grande base de códigos.
Thenable
Outro problema com o módulo é que ele é um objeto Thenable, ou seja, possui um método .then (). Esta função permite suspender um retorno de chamada no momento em que o módulo é iniciado, e é muito conveniente. Mas eu gostaria que a interface correspondesse ao Promise. Então não é Promessa, mas tudo bem, vamos encerrar. Vamos escrever um código tão simples:
return new Promise((resolve, reject) => { Module(config).then((module) => { resolve(module); }); });
Criamos Promise, iniciamos nosso módulo e, como retorno de chamada, chamamos a função de resolução e passamos o módulo que instalamos lá. Tudo parece óbvio, está tudo bem, estamos lançando - algo está errado, nosso navegador está congelado, nosso DevTools está travando e o processador está esquentando no computador. Não entendemos nada - algum tipo de recursão ou um loop infinito. A depuração é bastante difícil e, quando interrompemos o JavaScript, acabamos na função Then no módulo Emscripten.
Module['then'] = function(func) { if (Module['calledRun']) { func(Module); } else { Module['onRuntimeInitialized'] = function() { func(Module); }; }; return Module; };
Vamos dar uma olhada em mais detalhes. Traçar
Module['onRuntimeInitialized'] = function() { func(Module); };
responsável por suspender um retorno de chamada. Tudo está claro aqui: uma função assíncrona que chama nosso retorno de chamada. Tudo como queremos. Há outra parte desse recurso.
if (Module['calledRun']) { func(Module);
É chamado quando o módulo já foi iniciado. Em seguida, o retorno de chamada é chamado de forma síncrona imediatamente e o módulo é passado a ele no parâmetro Isso imita o comportamento da Promessa, e parece ser o que esperamos. Mas então o que está errado?
Se você ler atentamente a documentação, verifica-se que há um ponto muito sutil sobre o Promise. Quando resolvermos a Promessa usando um Thenable, o navegador desempacotará os valores desse Thenable e, para fazer isso, chamará o método .then (). Como resultado, resolvemos a promessa, passamos o módulo para ela. O navegador pergunta: Então isso é um objeto? Sim, este é um Thenable. Em seguida, a função .then () é chamada no módulo e a própria função de resolução é passada como retorno de chamada.
O módulo verifica se está em execução. Ele já está em execução, portanto, o retorno de chamada é chamado imediatamente e o mesmo módulo é passado a ele novamente. Como retorno de chamada, temos a função de resolução, e o navegador pergunta: este é um objeto Thenable? Sim, este é um Thenable. E tudo começa de novo. Como resultado, caímos em um ciclo interminável, do qual o navegador nunca retorna.

Não encontrei uma solução elegante para esse problema. Como resultado, simplesmente excluo o método .then () antes de resolver, e isso funciona.
Emscripten
Então, compilamos o módulo, montamos o JS, mas algo está faltando. Provavelmente precisamos fazer algum trabalho útil. Para fazer isso, transfira dados e conecte os dois mundos - JS e C ++. Como fazer isso? O Emscripten fornece três opções:
- A primeira são as funções ccall e cwrap. Na maioria das vezes, você os encontrará em alguns tutoriais no WebAssembly, mas eles não são adequados para o trabalho real, porque não suportam os recursos do C ++.
- O segundo é o WebIDL Binder. Ele já suporta funções C ++, você já pode trabalhar com ele. Essa é uma linguagem séria de descrição de interface usada, por exemplo, pelo W3C para sua documentação. Mas não queríamos carregá-lo em nosso projeto e usamos a terceira opção
- Incorporar. Podemos dizer que essa é uma maneira nativa de conectar objetos ao Emscripten, que é baseada em modelos C ++ e permite que você faça muitas coisas encaminhando diferentes entidades de C ++ para JS e vice-versa.
Incorporar permite que você:
- Chamar funções C ++ do código JavaScript
- Criar objetos JS a partir de uma classe C ++
- No código C ++, vá para a API do navegador (se, por algum motivo, você desejar, você pode, por exemplo, escrever toda a estrutura front-end em C ++).
- O principal para nós: implementar a interface JavaScript descrita em C ++.
Troca de dados
O último ponto é importante, pois essa é exatamente a ação que você executará constantemente ao portar o aplicativo. Portanto, gostaria de me debruçar sobre isso com mais detalhes. Agora haverá código C ++, mas não se assuste, é quase como o TypeScript :-D
O esquema é o seguinte:

No lado do C ++, existe um kernel ao qual queremos dar acesso, por exemplo, a uma rede externa - para carregar vídeo. Isso costumava ser feito com soquetes nativos, havia algum tipo de cliente HTTP que fazia isso, mas não há soquetes nativos no WebAssembly. Nós precisamos sair de alguma forma, então cortamos o cliente HTTP antigo, inserimos a interface nesse local e implementamos essa interface no JavaScript usando AJAX comum, de qualquer forma. Depois disso, passaremos o objeto resultante de volta ao C ++, onde o kernel o usará.
Vamos criar o cliente HTTP mais simples que só pode fazer solicitações de obtenção:
class HTTPClient { public: virtual std::string get(std::string url) = 0; };
Para a entrada, ele recebe uma string com o URL a ser baixado e a saída
string com o resultado da solicitação. No C ++, as strings podem ter dados binários, portanto, isso é adequado para vídeo. Emscripten nos faz escrever aqui
um invólucro tão assustador:

Nele, o principal é duas coisas - o nome da função no lado C ++ (marquei-os em verde) e os nomes correspondentes no lado JavaScript (marquei-os em azul). Como resultado, escrevemos uma declaração de comunicação:

Funciona como blocos de Lego, dos quais montamos. Nós temos uma classe, essa classe tem um método e queremos herdar dessa classe para implementar a interface. Isso é tudo. Vamos ao JavaScript e herdamos. Isso pode ser feito de duas maneiras. O primeiro é estender. Isso é muito semelhante ao bom e velho estendido do Backbone.

O módulo contém tudo o que o Emscripten compilou e possui uma propriedade com uma interface exportada. Chamamos o método extend e passamos um objeto para lá com a implementação desse método, ou seja, algum método será implementado na função get
Obtenha informações usando o AJAX.
Na saída, extend nos fornece um construtor JavaScript regular. Podemos chamá-lo quantas vezes for necessário e gerar objetos na quantidade necessária. Mas há uma situação em que temos um objeto e queremos apenas passá-lo para o lado do C ++.

Para fazer isso, de alguma forma, vincule esse objeto a um tipo que o C ++ entenderá. É isso que a função de implemento faz. Na saída, ele não fornece um construtor, mas um objeto pronto para uso, nosso cliente, que podemos devolver ao C ++. Você pode fazer isso, por exemplo, assim:
var app = Module.makeApp(client, …)
Suponha que tenhamos uma fábrica que crie nosso aplicativo e leve suas dependências para parâmetros, por exemplo, cliente e algo mais. Quando essa função funciona, obtemos o objeto de nosso aplicativo, que já contém a API necessária. Você pode fazer o oposto:
val client = val::global(″client″); client.call<std::string>(″get″, val(...) );
Diretamente do C ++, leve nosso cliente do escopo global do navegador. Além disso, no lugar do cliente, pode haver qualquer API do navegador, iniciando no console, terminando com a API DOM, WebRTC - o que você quiser. Em seguida, chamamos os métodos que esse objeto possui e agrupamos todos os valores na classe mágica val, que Emscripten nos fornece.
Erros de ligação
Em geral, é tudo, mas quando você inicia o desenvolvimento, erros de ligação esperam por você. Eles se parecem com isso:

Emscripten tenta nos ajudar e explicar o que está acontecendo de errado. Se tudo estiver resumido, é necessário garantir que coincidam (é fácil selar e obter um erro de ligação):
- Nomes
- Tipos
- Número de parâmetros
A sintaxe de incorporação é incomum não apenas para fornecedores de front-end, mas também para pessoas que lidam com C ++. Esse é um tipo de DSL no qual é fácil cometer um erro, você precisa seguir isso. Falando sobre interfaces, quando você implementa algum tipo de interface em JavaScript, é necessário que ele corresponda exatamente ao que você descreveu em seu contrato.
Tivemos um caso interessante. Meu colega Jura, envolvido no projeto no lado do C ++, usou o Extend para testar seus módulos. Eles funcionaram perfeitamente para ele, então ele os comprometeu e os passou para mim. Eu usei implementar para integrar esses módulos em um projeto JS. E eles pararam de trabalhar para mim. Quando descobrimos, descobrimos que, ao vincular os nomes das funções, recebemos um erro de digitação.
Como podemos ver pelo nome, o Extend é uma extensão da interface; portanto, se você cometer algum erro, o Extend não emitirá um erro, decidirá que você acabou de adicionar um novo método, e tudo bem.
Ou seja, oculta os erros de ligação até que o próprio método seja chamado. Eu sugiro usar o Implement em todos os casos em que for melhor para você, pois ele verifica imediatamente a exatidão da interface encaminhada. Mas se você precisar do Extend, deverá cobrir com testes a chamada de cada método para não estragar tudo.
Estender e ES6
Outro problema com o Extend é que ele não suporta as classes ES6. Quando você herda um objeto derivado de uma classe ES6, o Extend espera que todas as propriedades sejam enumeráveis, mas com o ES6 não é. Os métodos estão no protótipo e possuem enumeráveis: false. Eu uso uma muleta como esta, na qual reviso o protótipo e ligo o enumerável: true:
function enumerateProto(obj) { Object.getOwnPropertyNames(obj.prototype) .forEach(prop => Object.defineProperty(obj.prototype, prop, {enumerable: true}) ) }
Espero que um dia eu possa me livrar disso, pois há discussões na comunidade Emscripten sobre como melhorar o suporte ao ES6.
RAM
Falando sobre C ++, não se pode deixar de mencionar memória. Quando verificamos tudo em vídeo com qualidade SD, tudo estava bem conosco, funcionou perfeitamente! Assim que fizemos o teste FullHD, houve uma falta de erro de memória. Não importa, existe a opção TOTAL_MEMORY, que define o valor da memória inicial para o módulo. Fizemos meio gigabyte, está tudo bem, mas de alguma forma é desumano para os usuários, porque reservamos a memória para todos, mas nem todos têm uma assinatura do conteúdo FullHD.
Há outra opção - ALLOW_MEMORY_GROWTH. Permite aumentar a memória
gradualmente conforme necessário. Funciona assim: Emscripten, por padrão, fornece ao módulo 16 megabytes para operação. Quando você os usou, um novo pedaço de memória é alocado. Todos os dados antigos são copiados para lá e você ainda tem a mesma quantidade de espaço para novos. Isso acontece até você atingir 4 GB.
Suponha que você alocou 256 megabytes de memória, mas sabe com certeza que pensou que seu aplicativo tivesse 192. O suficiente. O restante da memória será usado ineficientemente. Você o destacou, tirou do usuário, mas não fez nada com ele. Eu gostaria de, de alguma forma, evitar isso. Há um pequeno truque: começamos a trabalhar com a memória aumentada uma vez e meia. Na terceira etapa, atingimos 192 megabytes, e é exatamente disso que precisamos. Reduzimos o consumo de memória nesse restante e salvamos uma alocação de memória desnecessária e, quanto mais, mais tempo eles demoram. Portanto, recomendo usar essas duas opções juntas.
Injeção de dependência
Parece que foi tudo, mas o rake foi um pouco mais. Há um problema com a injeção de dependência. Escrevemos a classe mais simples na qual é necessária uma dependência.
class App { constructor(httpClient) { this.httpClient = httpClient } }
Por exemplo, passamos nosso cliente HTTP para nosso aplicativo. Economizamos na propriedade de classe. Parece que tudo vai funcionar bem.
Module.App.extend( ″App″, new App(client) )
Herdamos da interface C ++, primeiro criamos nosso objeto, passamos a dependência a ele e depois herdamos. No momento da herança, Emscripten faz algo incrível com o objeto. É mais fácil pensar que ele mata um objeto antigo, cria um novo com base em seu modelo e arrasta todos os métodos públicos para lá. Mas, ao mesmo tempo, o estado do objeto é perdido e você obtém um objeto que não é formado e não funciona corretamente. Resolver esse problema é bastante simples. É necessário usar um construtor que funcione após o estágio de herança.
class App { _construct(httpClient) { this.httpClient = httpClient this._parent._construct.call(this) } }
Fazemos quase a mesma coisa: armazenamos a dependência no campo do objeto, mas esse é o objeto que resultou após a herança. Não devemos esquecer de encaminhar a chamada do construtor para o objeto pai, localizado no lado do C ++. A última linha é um análogo do método super () no ES6. É assim que a herança acontece neste caso:
const appConstr = Module.App.extend( ″App″, new App() ) const app = new appConstr(client)
Primeiro, herdamos e, em seguida, criamos um novo objeto no qual a dependência já foi passada, e isso funciona.
Truque do ponteiro
Outro problema é passar objetos por ponteiro de C ++ para JavaScript. Já fizemos um cliente HTTP. Por simplicidade, perdemos um detalhe importante.
std::string get(std::string url)
O método retorna o valor imediatamente, ou seja, acontece que a solicitação deve ser síncrona. Afinal, o AJAX solicita o AJAX e é assíncrono; portanto, na vida real, o método não retornará nada ou podemos retornar o ID do pedido. Mas, para que alguém retorne a resposta, passamos o ouvinte como o segundo parâmetro, no qual haverá retornos de chamada do C ++.
void get(std::string url, Listener listener)
Em JS, fica assim:
function get(url, listener) { fetch(url).then(result) => { listener.onResult(result) }) }
Temos uma função get que leva esse objeto de ouvinte. Iniciamos o download do arquivo e desligamos o retorno de chamada. Quando o arquivo é baixado, extraímos a função desejada do ouvinte e passamos o resultado para ele.
Parece que o plano é bom, mas quando a função get for concluída, todas as variáveis locais serão destruídas e, juntamente com eles, os parâmetros da função, ou seja, o ponteiro será destruído e o emscripten de tempo de execução destruirá o objeto no lado do C ++.
Como resultado, quando se trata de chamar a linha listener.onResult (result), o ouvinte não existe mais e, ao acessá-lo, ocorrerá um erro de acesso à memória que levará à falha do aplicativo.
Gostaria de evitar isso e existe uma solução, mas demorou várias semanas para encontrá-la.
function get(url, listener) { const listenerCopy = listener.clone() fetch(url).then((result) => { listenerCopy.onResult(result) listenerCopy.delete() }) }
Acontece que existe um método para clonar um ponteiro. Por alguma razão, ele não está documentado, mas funciona bem e permite aumentar a contagem de referências no ponteiro Emscripten. Isso nos permite suspendê-lo em um fechamento e, quando iniciarmos nosso retorno de chamada, nosso ouvinte estará acessível por esse ponteiro e podemos trabalhar conforme necessário.
O mais importante é não esquecer de excluir esse ponteiro, caso contrário, isso causará um erro de vazamento de memória, o que é muito ruim.
Gravação rápida na memória
Quando baixamos vídeos, essas são quantidades relativamente grandes de informações, e eu gostaria de reduzir a quantidade de dados de cópia para frente e para trás, a fim de economizar memória e tempo. Há um truque sobre como gravar uma grande quantidade de informações diretamente na memória do WebAssembly a partir do JavaScript.
var newData = new Uint8Array(…); var size = newData.byteLength; var ptr = Module._malloc(size); var memory = new Uint8Array( Module.buffer, ptr, size ); memory.set(newData);
newData são nossos dados como uma matriz digitada. Podemos tomar seu comprimento e solicitar a alocação de memória do tamanho que precisamos do módulo WebAssembly. A função malloc retornará um ponteiro para nós, que é apenas o índice da matriz que contém toda a memória no WebAssembly. Do lado do JavaScript, ele se parece com um ArrayBuffer.
Na próxima etapa, abriremos uma janela nesse ArrayBuffer do tamanho certo de um determinado local e copiaremos nossos dados lá. Apesar do fato de a operação set ter semântica de cópia, quando observei esta seção no criador de perfil, não vi um processo longo. Eu acho que o navegador otimiza essa operação com a ajuda da semântica de movimento, ou seja, transfere a propriedade da memória de um objeto para outro.
E em nosso aplicativo, também contamos com a semântica de movimentação para economizar cópias de memória.
Adblock
Um problema interessante, em vez disso, com o Adblock. Acontece que na Rússia todos os bloqueadores populares recebem uma assinatura do RU Adlist, e tem uma regra tão maravilhosa que proíbe o download do WebAssembly de sites de terceiros. Por exemplo, com uma CDN.

A saída é não usar a CDN, mas armazenar tudo no seu domínio (isso não nos convém). Ou renomeie o arquivo .wasm para que ele não se encaixe nessa regra. Você ainda pode ir ao fórum desses camaradas e tentar convencê-los a remover esta regra. Eu acho que eles se justificam lutando contra os mineiros dessa maneira, embora eu não saiba por que eles não conseguem adivinhar o nome do arquivo.
Produção
Como resultado, entramos em produção. Sim, não foi fácil, demorou 8 meses e quero me perguntar se valeu a pena. Na minha opinião - valeu a pena:
Não é necessário instalar
Concluímos que nosso código é entregue ao usuário sem instalar nenhum programa. Quando tínhamos um plug-in de navegador, o usuário precisava fazer o download e instalá-lo, e esse é um filtro enorme para a distribuição de tecnologia. Agora, o usuário apenas assiste o vídeo no site e nem entende que todo um maquinário funciona sob o capô e que tudo é complicado por lá. O navegador apenas baixa um arquivo adicional com o código, como uma imagem ou .css.
Base de código unificada e depuração em diferentes plataformas
Ao mesmo tempo, conseguimos manter nossa base de códigos única. Podemos distorcer o mesmo código em plataformas diferentes e aconteceu repetidamente que erros que eram invisíveis em uma das plataformas apareciam na outra. E assim, podemos detectar erros ocultos com diferentes ferramentas em diferentes plataformas.
Liberação rápida
Temos um lançamento rápido, pois podemos ser lançados como um aplicativo Web simples e atualizar o código C ++ a cada novo lançamento. Ele não se compara com o lançamento de novos plugins, aplicativos móveis ou aplicativos SmartTV. O lançamento depende apenas de nós: quando queremos, então ele será lançado.
Feedback rápido
E isso significa feedback rápido: se algo der errado, podemos descobrir durante o dia que há um problema e responder a ele.
Acredito que todos esses problemas valeram essas vantagens. Nem todo mundo tem um aplicativo C ++, mas se você tem um e deseja que ele esteja no navegador - o WebAssembly é um caso de uso 100% para você.
Onde aplicar
Nem todo mundo escreve em C ++. Mas não apenas o C ++ está disponível para o WebAssembly. Sim, essa é historicamente a primeira plataforma que ainda estava disponível no asm.js, uma tecnologia antiga do Mozilla. A propósito, portanto, ele tem boas ferramentas, como eles são mais antigos que a própria tecnologia.
Ferrugem
A nova linguagem Rust, que também está sendo desenvolvida pela Mozilla, agora está alcançando e ultrapassando o C ++ em termos de ferramentas. Tudo vai ao ponto de tornar o processo de desenvolvimento mais interessante para o WebAssembly.
Lua, Perl, Python, PHP, etc.
Quase todas as linguagens interpretadas também estão disponíveis no WebAssembly, uma vez que seus intérpretes são escritos em C ++, eles foram simplesmente compilados no WebAssembly e agora você pode alterar o PHP em um navegador.
Go
Na versão 1.11, eles fizeram uma versão beta da compilação no WebAssembly, na 2.0 eles prometem suporte à liberação. Seu suporte apareceu mais tarde, porque o WebAssembly não suporta coletor de lixo e o Go é um idioma de memória gerenciada. Então eles tiveram que arrastar seu coletor de lixo para o WebAssembly.
Kotlin / Nativo
Sobre a mesma história com Kotlin. O compilador deles tem suporte experimental, mas eles também terão que fazer algo com o coletor de lixo. Não sei qual é o status.
3D-
? , — 3D-. , , asm.js WebAssembly . , WebAssembly.

, : , , . , .

. , , , , . , , ; — .

, Google Chrome, , WebAssembly-. npm- , Wasm, JS. , ++ - — .
HunSpell — Wasm .
— « ». , - , — OpenSSL. WebAssembly. OpenSSL — , , .
use case wotinspector.com. World of Tanks. , , , , , .
— . , , . , , - ++, WebAssembly, ( , ).
. , , . . , , , , . . .
, , ++. , FFmpeg, . , ffmpeg. . , , , , .

— . OpenCV — , WebAssembly, . PDF. SQLite, SQL. SQLite WebAssembly Emscripten, .
Node.js

WebAssembly, Node.js. , Sass — css. Ruby, ++ ( libsass). , Webpack', Node.js.
node-sass , JS- .
, , . . :

, node-sass 100 . , ( ) . WebAssembly : , WebAssembly .
Node. , WebAssembly
libsass-asm . , . WebAssembly …
Figma — web-. - Sketch, , . ++ ( ), asm.js. , .

WebAssembly, , 3 . , .
Visual Studio Code, , Electron, , , Node-sass. , Node, . , , , WebAssembly.

— AutoCAD. 30 , ++, . , , - JavaScript, , . WebAssembly
AutoCAD - , 5 .
, , , , , , , , . FFMpeg — , — QEMU. , , KVM, .

2011
QEMU . , . ,
Linux , Linux-, , - .
, . bash, , Linux.
— GUI . . , , …

, , - .
Windows 2000 , , 18 , . , Chrome ( FireFox).
, WebAssembly , , , , .
, WebAssembly. , — , . — , .

, C++ web-. , , — . — , , , .
, . , C++, JavaScript, . , C++. , JS C++, .
— .

CI Pipeline
? JS- , Webpack. , , ( ), JS. webpack watch, , .

, . , , .
Chrome DevTools, Sources wasm-. ( - ), , , .

, , : «, , , , , !». , embedded-, , - .
: -g4 wast- , .

, 100 ( FAR). — , Chrome. E:/_work/bfg/bytefrog/… — . , ++ . , SourceMap!
SourceMap
, .
- Firefox.
- --sourcemap-base=http://localhost , SourceMap -, .
- HTTP.
- .
- Windows «:» . .
. CMake , URL -. : wast- , . , .
, :

++ . ! , , stack trace, . , wasm- stack trace, , , , , .

, — SourceMap . , , . , .

«var0».

, . , SourceMap, , .
. Chrome, Firefox. Firefox — «» , , .

Chrome ( , , Mangled ), , , , .

. , :
- . runtime, . ++ Rust Go.
- JS — Wasm. , JS Wasm. -, , . , .
- . , , , .
- Wasm . Wasm , JS. WebAssembly , .
- JS.
: .
- wasp_cpp_bench
- Chrome 65.0.3325.181 (64-bit)
- Core i5-4690
- 24gb ram
- 5 ; max min;
. JS — , .

++, , - . Grayscale. C++ , . ( ), , JS. , , , ++, .
Sentry, — wasm. , traceKit, Sentry — Raven, — , , wasm . , , , pull request, npm install JS-.

. production, , . debug-, , :

- WebAssembly , .
- — . 8 , C++, , .
- , , WebAssembly — .
- — JS. JS- , «» , , .
, :
- Emscripten Embind. .
- - Emscripten — . , , 3000 Emscripten.
- Sentry.
- Firefox.
Obrigado pela atenção! .

HolyJS, : 24-25 HolyJS . (, Node.js Ryan Dahl!), — 1 .