WebVR Parte 3: Desbloqueando o potencial do WebAssembly e do AssemblyScript
Publicados: 2022-03-11O WebAssembly definitivamente não é um substituto para o JavaScript como a língua franca da web e do mundo.
WebAssembly (abreviado como Wasm) é um formato de instrução binária para uma máquina virtual baseada em pilha. Wasm foi projetado como um alvo portátil para compilação de linguagens de alto nível como C/C++/Rust, permitindo a implantação na web para aplicativos cliente e servidor.” –WebAssembly.org
É importante distinguir que WebAssembly não é uma linguagem. WebAssembly é como um '.exe' - ou melhor ainda - um arquivo Java '.class'. Ele é compilado pelo desenvolvedor web a partir de outro idioma, depois baixado e executado no seu navegador.
O WebAssembly está fornecendo ao JavaScript todos os recursos que ocasionalmente queríamos emprestar, mas nunca realmente desejamos possuir. Assim como alugar um barco ou um cavalo, o WebAssembly nos permite viajar para outros idiomas sem ter que fazer escolhas extravagantes de “estilo de vida linguístico”. Isso permitiu que a web se concentrasse em coisas importantes, como fornecer recursos e melhorar a experiência do usuário.
Mais de 20 linguagens compilam para WebAssembly: Rust, C/C++, C#/.Net, Java, Python, Elixir, Go e, claro, JavaScript.
Se você se lembra do diagrama de arquitetura da nossa simulação, delegamos toda a simulação ao nBodySimulator
, para que ele gerencie o web worker.
Se você se lembra do post de introdução, nBodySimulator
tem uma função step()
chamada a cada 33ms. A função step()
faz essas coisas - numeradas no diagrama acima:
- CalculaForces
calculateForces()
do nBodySimulator chamathis.worker.postMessage()
para iniciar o cálculo. - workerWasm.js
this.onmessage()
recebe a mensagem. - workerWasm.js executa de forma síncrona a função
nBodyForces()
de nBodyForces.wasm. - workerWasm.js responde usando
this.postMessage()
ao thread principal com as novas forces. -
this.worker.onMessage()
do thread principal empacota os dados e chamadas retornados. - applyForces
applyForces()
do nBodySimulator para atualizar as posições dos corpos. - Finalmente, o visualizador redesenha.
No post anterior, construímos o web worker que está envolvendo nossos cálculos WASM. Hoje, estamos construindo a pequena caixa rotulada “WASM” e movendo os dados para dentro e para fora.
Para simplificar, escolhi AssemblyScript como a linguagem de código-fonte para escrever nossos cálculos. AssemblyScript é um subconjunto do TypeScript - que é um JavaScript tipado - então você já o conhece.
Por exemplo, esta função AssemblyScript calcula a gravidade entre dois corpos: O :f64
em someVar:f64
marca a variável someVar como um float para o compilador. Lembre-se de que este código é compilado e executado em um tempo de execução completamente diferente do JavaScript.
// AssemblyScript - a TypeScript-like language that compiles to WebAssembly // src/assembly/nBodyForces.ts /** * Given two bodies, calculate the Force of Gravity, * then return as a 3-force vector (x, y, z) * * Sometimes, the force of gravity is: * * Fg = G * mA * mB / r^2 * * Given: * - Fg = Force of gravity * - r = sqrt ( dx + dy + dz) = straight line distance between 3d objects * - G = gravitational constant * - mA, mB = mass of objects * * Today, we're using better-gravity because better-gravity can calculate * force vectors without polar math (sin, cos, tan) * * Fbg = G * mA * mB * dr / r^3 // using dr as a 3-distance vector lets * // us project Fbg as a 3-force vector * * Given: * - Fbg = Force of better gravity * - dr = (dx, dy, dz) // a 3-distance vector * - dx = bodyB.x - bodyA.x * * Force of Better-Gravity: * * - Fbg = (Fx, Fy, Fz) = the change in force applied by gravity each * body's (x,y,z) over this time period * - Fbg = G * mA * mB * dr / r^3 * - dr = (dx, dy, dz) * - Fx = Gmm * dx / r3 * - Fy = Gmm * dy / r3 * - Fz = Gmm * dz / r3 * * From the parameters, return an array [fx, fy, fz] */ function twoBodyForces(xA: f64, yA: f64, zA: f64, mA: f64, xB: f64, yB: f64, zB: f64, mB: f64): f64[] { // Values used in each x,y,z calculation const Gmm: f64 = G * mA * mB const dx: f64 = xB - xA const dy: f64 = yB - yA const dz: f64 = zB - zA const r: f64 = Math.sqrt(dx * dx + dy * dy + dz * dz) const r3: f64 = r * r * r // Return calculated force vector - initialized to zero const ret: f64[] = new Array<f64>(3) // The best not-a-number number is zero. Two bodies in the same x,y,z if (isNaN(r) || r === 0) return ret // Calculate each part of the vector ret[0] = Gmm * dx / r3 ret[1] = Gmm * dy / r3 ret[2] = Gmm * dz / r3 return ret }
Essa função AssemblyScript usa o (x, y, z, mass) para dois corpos e retorna uma matriz de três floats descrevendo o vetor de força (x, y, z) que os corpos aplicam um ao outro. Não podemos chamar essa função do JavaScript porque o JavaScript não tem ideia de onde encontrá-la. Temos que “exportá-lo” para JavaScript. Isso nos leva ao nosso primeiro desafio técnico.
Importações e Exportações do WebAssembly
No ES6, pensamos em importações e exportações em código JavaScript e usamos ferramentas como Rollup ou Webpack para criar código que roda em navegadores legados para lidar com import
e require()
. Isso cria uma árvore de dependência de cima para baixo e permite tecnologia legal, como “agitação de árvore” e divisão de código.
No WebAssembly, as importações e exportações realizam tarefas diferentes de uma importação ES6. Importações/exportações do WebAssembly:
- Forneça um ambiente de tempo de execução para o módulo WebAssembly (por exemplo, funções
trace()
eabort()
). - Importe e exporte funções e constantes entre os tempos de execução.
No código abaixo, env.abort
e env.trace
fazem parte do ambiente que devemos fornecer ao módulo WebAssembly. As funções nBodyForces.logI
e amigos fornecem mensagens de depuração para o console. Observe que a passagem de strings para dentro/fora do WebAssembly não é trivial, pois os únicos tipos do WebAssembly são números i32, i64, f32, f64, com referências i32 a uma memória linear abstrata.
Nota: Esses exemplos de código estão alternando entre o código JavaScript (o web worker) e o AssemblyScript (o código WASM).
// Web Worker JavaScript in workerWasm.js /** * When we instantiate the Wasm module, give it a context to work in: * nBodyForces: {} is a table of functions we can import into AssemblyScript. See top of nBodyForces.ts * env: {} describes the environment sent to the Wasm module as it's instantiated */ const importObj = { nBodyForces: { logI(data) { console.log("Log() - " + data); }, logF(data) { console.log("Log() - " + data); }, }, env: { abort(msg, file, line, column) { // wasm.__getString() is added by assemblyscript's loader: // https://github.com/AssemblyScript/assemblyscript/tree/master/lib/loader console.error("abort: (" + wasm.__getString(msg) + ") at " + wasm.__getString(file) + ":" + line + ":" + column); }, trace(msg, n) { console.log("trace: " + wasm.__getString(msg) + (n ? " " : "") + Array.prototype.slice.call(arguments, 2, 2 + n).join(", ")); } } }
Em nosso código AssemblyScript, podemos concluir a importação dessas funções da seguinte forma:
// nBodyForces.ts declare function logI(data: i32): void declare function logF(data: f64): void
Nota : Abortar e rastrear são importados automaticamente .
Do AssemblyScript, podemos exportar nossa interface. Aqui estão algumas constantes exportadas:
// src/assembly/nBodyForces.ts // Gravitational constant. Any G could be used in a game. // This value is best for a scientific simulation. export const G: f64 = 6.674e-11; // for sizing and indexing arrays export const bodySize: i32 = 4 export const forceSize: i32 = 3
E aqui está a exportação de nBodyForces()
que chamaremos do JavaScript. Exportamos o tipo Float64Array
na parte superior do arquivo para que possamos usar o carregador JavaScript do AssemblyScript em nosso web worker para obter os dados (veja abaixo):

// src/assembly/nBodyForces.ts export const FLOAT64ARRAY_ID = idof<Float64Array>(); ... /** * Given N bodies with mass, in a 3d space, calculate the forces of gravity to be applied to each body. * * This function is exported to JavaScript, so only takes/returns numbers and arrays. * For N bodies, pass and array of 4N values (x,y,z,mass) and expect a 3N array of forces (x,y,z) * Those forces can be applied to the bodies mass to update its position in the simulation. * Calculate the 3-vector each unique pair of bodies applies to each other. * * 0 1 2 3 4 5 * 0 xxxxx * 1 xxxx * 2 xxx * 3 xx * 4 x * 5 * * Sum those forces together into an array of 3-vector x,y,z forces * * Return 0 on success */ export function nBodyForces(arrBodies: Float64Array): Float64Array { // Check inputs const numBodies: i32 = arrBodies.length / bodySize if (arrBodies.length % bodySize !== 0) trace("INVALID nBodyForces parameter. Chaos ensues...") // Create result array. This should be garbage collected later. let arrForces: Float64Array = new Float64Array(numBodies * forceSize) // For all bodies: for (let i: i32 = 0; i < numBodies; i++) { // Given body i: pair with every body[j] where j > i for (let j: i32 = i + 1; j < numBodies; j++) { // Calculate the force the bodies apply to one another const bI: i32 = i * bodySize const bJ: i32 = j * bodySize const f: f64[] = twoBodyForces( arrBodies[bI], arrBodies[bI + 1], arrBodies[bI + 2], arrBodies[bI + 3], // x,y,z,m arrBodies[bJ], arrBodies[bJ + 1], arrBodies[bJ + 2], arrBodies[bJ + 3], // x,y,z,m ) // Add this pair's force on one another to their total forces applied x,y,z const fI: i32 = i * forceSize const fJ: i32 = j * forceSize // body0 arrForces[fI] = arrForces[fI] + f[0] arrForces[fI + 1] = arrForces[fI + 1] + f[1] arrForces[fI + 2] = arrForces[fI + 2] + f[2] // body1 arrForces[fJ] = arrForces[fJ] - f[0] // apply forces in opposite direction arrForces[fJ + 1] = arrForces[fJ + 1] - f[1] arrForces[fJ + 2] = arrForces[fJ + 2] - f[2] } } // For each body, return the sum of forces all other bodies applied to it. // If you would like to debug wasm, you can use trace or the log functions // described in workerWasm when we initialized // Eg trace("nBodyForces returns (b0x, b0y, b0z, b1z): ", 4, arrForces[0], arrForces[1], arrForces[2], arrForces[3]) // x,y,z return arrForces // success }
Artefatos do WebAssembly: .wasm e .wat
Quando nosso AssemblyScript nBodyForces.ts
é compilado em um binário WebAssembly nBodyForces.wasm
, há uma opção para também criar uma versão “texto” descrevendo as instruções no binário.
Dentro do arquivo nBodyForces.wat
, podemos ver essas importações e exportações:
;; This is a comment in nBodyForces.wat (module ;; compiler defined types (type $FUNCSIG$iii (func (param i32 i32) (result i32))) … ;; Expected imports from JavaScript (import "env" "abort" (func $~lib/builtins/abort (param i32 i32 i32 i32))) (import "env" "trace" (func $~lib/builtins/trace (param i32 i32 f64 f64 f64 f64 f64))) ;; Memory section defining data constants like strings (memory $0 1) (data (i32.const 8) "\1e\00\00\00\01\00\00\00\01\00\00\00\1e\00\00\00~\00l\00i\00b\00/\00r\00t\00/\00t\00l\00s\00f\00.\00t\00s\00") ... ;; Our global constants (not yet exported) (global $nBodyForces/FLOAT64ARRAY_ID i32 (i32.const 3)) (global $nBodyForces/G f64 (f64.const 6.674e-11)) (global $nBodyForces/bodySize i32 (i32.const 4)) (global $nBodyForces/forceSize i32 (i32.const 3)) ... ;; Memory management functions we'll use in a minute (export "memory" (memory $0)) (export "__alloc" (func $~lib/rt/tlsf/__alloc)) (export "__retain" (func $~lib/rt/pure/__retain)) (export "__release" (func $~lib/rt/pure/__release)) (export "__collect" (func $~lib/rt/pure/__collect)) (export "__rtti_base" (global $~lib/rt/__rtti_base)) ;; Finally our exported constants and function (export "FLOAT64ARRAY_ID" (global $nBodyForces/FLOAT64ARRAY_ID)) (export "G" (global $nBodyForces/G)) (export "bodySize" (global $nBodyForces/bodySize)) (export "forceSize" (global $nBodyForces/forceSize)) (export "nBodyForces" (func $nBodyForces/nBodyForces)) ;; Implementation details ...
Agora temos nosso binário nBodyForces.wasm
e um web worker para executá-lo. Prepare-se para decolar! E algum gerenciamento de memória!
Para completar a integração, temos que passar um array variável de floats para o WebAssembly e retornar um array variável de floats para JavaScript.
Com o JavaScript ingênuo burguês, eu me propus a passar esses arrays de tamanho variável espalhafatosos dentro e fora de um tempo de execução de alto desempenho multiplataforma. Passar dados de/para WebAssembly foi, de longe, a dificuldade mais inesperada neste projeto.
No entanto, com muito obrigado pelo trabalho pesado feito pela equipe do AssemblyScript, podemos usar seu “loader” para ajudar:
// workerWasm.js - our web worker /** * AssemblyScript loader adds helpers for moving data to/from AssemblyScript. * Highly recommended */ const loader = require("assemblyscript/lib/loader")
O require()
significa que precisamos usar um empacotador de módulos como Rollup ou Webpack. Para este projeto, escolhi o Rollup por sua simplicidade e flexibilidade e nunca olhei para trás.
Lembre-se de que nosso web worker é executado em um thread separado e é essencialmente uma função onmessage()
com uma instrução switch()
.
loader
cria nosso módulo wasm com algumas funções extras de gerenciamento de memória. __retain()
e __release()
gerenciam referências de coleta de lixo no tempo de execução do trabalhador __allocArray()
copia nosso array de parâmetros na memória do módulo wasm __getFloat64Array()
copia o array de resultados do módulo wasm no tempo de execução do trabalhador
Agora podemos empacotar arrays float dentro e fora de nBodyForces()
e completar nossa simulação:
// workerWasm.js /** * Web workers listen for messages from the main thread. */ this.onmessage = function (evt) { // message from UI thread var msg = evt.data switch (msg.purpose) { // Message: Load new wasm module case 'wasmModule': // Instantiate the compiled module we were passed. wasm = loader.instantiate(msg.wasmModule, importObj) // Throws // Tell nBodySimulation.js we are ready this.postMessage({ purpose: 'wasmReady' }) return // Message: Given array of floats describing a system of bodies (x,y,x,mass), // calculate the Grav forces to be applied to each body case 'nBodyForces': if (!wasm) throw new Error('wasm not initialized') // Copy msg.arrBodies array into the wasm instance, increase GC count const dataRef = wasm.__retain(wasm.__allocArray(wasm.FLOAT64ARRAY_ID, msg.arrBodies)); // Do the calculations in this thread synchronously const resultRef = wasm.nBodyForces(dataRef); // Copy result array from the wasm instance to our javascript runtime const arrForces = wasm.__getFloat64Array(resultRef); // Decrease the GC count on dataRef from __retain() here, // and GC count from new Float64Array in wasm module wasm.__release(dataRef); wasm.__release(resultRef); // Message results back to main thread. // see nBodySimulation.js this.worker.onmessage return this.postMessage({ purpose: 'nBodyForces', arrForces }) } }
Com tudo o que aprendemos, vamos revisar nossa jornada de trabalho da Web e WebAssembly. Bem-vindo ao novo back-end do navegador da web. Estes são os links para o código no GitHub:
- GET Index.html
- main.js
- nBodySimulator.js - passa uma mensagem para seu web worker
- workerWasm.js - chama a função WebAssembly
- nBodyForces.ts - calcula e retorna uma matriz de forças
- workerWasm.js - passa os resultados de volta para o encadeamento principal
- nBodySimulator.js - resolve a promessa de forças
- nBodySimulator.js - então aplica as forças aos corpos e diz aos visualizadores para pintar
A partir daqui, vamos começar o show criando nBodyVisualizer.js
! Nossa próxima postagem cria um visualizador usando a API do Canvas, e a postagem final termina com WebVR e Aframe.