作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Michael Cole's profile image

Michael Cole

Michael is an expert full-stack web engineer, speaker, 拥有20多年经验和计算机科学学位的顾问.

Previously At

Ernst & Young
Share

WebAssembly is definitely not JavaScript作为网络和世界通用语言的替代品.

WebAssembly(简称Wasm)是一种用于基于堆栈的虚拟机的二进制指令格式. Wasm被设计为可移植的目标,用于编译C/ c++ /Rust等高级语言, 启用客户端和服务器应用程序的web部署.”WebAssembly.org

重要的是要区分WebAssembly不是一种语言. WebAssembly is like an ‘.exe’ - or even better - a Java ‘.class’ file. 它由web开发人员从另一种语言编译,然后下载并运行在您的浏览器上.

WebAssembly为JavaScript提供了所有我们偶尔想借用但从未借用过的特性 really wanted to own. Much like renting a boat or a horse, WebAssembly让我们不必选择奢侈的“语言生活方式”就可以使用其他语言. 这使得web可以专注于重要的事情,比如交付功能和改善用户体验.

超过20种语言可以编译成WebAssembly: Rust、C/ c++、c# /.Net, Java, Python, Elixir, Go,当然还有JavaScript.

如果您还记得我们的模拟架构图,我们将整个模拟委托给 nBodySimulator, so it manages the web worker.

Simulation’s architecture diagram

If you remember from the intro post, nBodySimulator has a step() function called every 33ms. The step() 函数做这些事情——在上面的图表中编号:

  1. nBodySimulator’s calculateForces() calls this.worker.postMessage() to start the calculation.
  2. workerWasm.js this.onmessage() gets the message.
  3. workerWasm.js synchronously runs nBodyForces.wasm’s nBodyForces() function.
  4. workerWasm.js replies using this.postMessage() to the main thread with the new forces.
  5. The main thread’s this.worker.onMessage() marshals the returned data and calls.
  6. nBodySimulator’s applyForces() to updates the positions of the bodies.
  7. Finally, the visualizer repaints.

UI thread, web worker thread

In the previous post,我们构建了包裹WASM计算的web worker. 今天,我们正在构建一个名为“WASM”的小盒子,并将数据移入和移出.

For simplicity, I chose AssemblyScript 作为源代码语言来编写我们的计算. AssemblyScript是TypeScript的一个子集——TypeScript是一种类型化的JavaScript——所以你已经知道它了.

例如,这个AssemblyScript函数计算两个物体之间的重力 :f64 in someVar:f64 将someVar变量标记为编译器的浮点数. 请记住,这些代码是在与JavaScript完全不同的运行时中编译和运行的.

// AssemblyScript——一种类似typescript的语言,可以编译成WebAssembly
// src/assembly/nBodyForces.ts

/**
 *给定两个物体,计算重力, 
 * 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) =三维物体之间的直线距离    
 * - G  = gravitational constant
 * - mA, mB = mass of objects
 * 
 *今天,我们使用更好的重力,因为更好的重力可以计算 
 *没有极坐标的力向量(sin, cos, tan)
 * 
 * Fbg = G * mA * mB * dr / r^3 //使用dr作为3距离矢量让 
 * //我们将Fbg投影为3-力矢量
 * 
 * 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) =重力作用下的力的变化 
 *这段时间内的身体(x,y,z 
 * - Fbg = G * mA * mB * dr / r^3         
 * - dr = (dx, dy, dz)
 * - Fx = Gmm * dx / r3 
 * - Fy = Gmm * dy / r3 
 * - Fz = Gmm * dz / r3 
 * 
 *从参数中返回一个数组[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

  //返回计算的力向量-初始化为零
  const ret: f64[] = new Array(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
}

This AssemblyScript function takes the (x, y, z, 质量),并返回一个由三个浮点数组成的数组,这些浮点数描述(x, y, z) force-vector the bodies apply to each other. 我们不能从JavaScript中调用这个函数,因为JavaScript不知道在哪里找到它. We have to “export” it to JavaScript. This brings us to our first technical challenge.

WebAssembly Imports and Exports

In ES6, 我们考虑JavaScript代码中的导入和导出,并使用Rollup或Webpack等工具来创建在旧浏览器中运行的代码来处理 import and require(). 这创建了一个自顶向下的依赖树,并启用了像“tree-shaking” and code-splitting.

在WebAssembly中,导入和导出完成的任务与ES6导入不同. WebAssembly imports/exports:

  • 为WebAssembly模块提供一个运行时环境.g., trace() and abort() functions).
  • 在运行时之间导入和导出函数和常量.

In the code below, env.abort and env.trace 是我们必须提供给WebAssembly模块的环境的一部分吗. The nBodyForces.logI 好友函数向控制台提供调试消息. 请注意,在WebAssembly中传入/传出字符串是非常重要的,因为WebAssembly的唯一类型是i32, i64, f32, f64 numbers, with i32 references to an abstract linear memory.

Note: 这些代码示例在JavaScript代码(web worker)和AssemblyScript (WASM代码)之间来回切换。.

// Web Worker JavaScript in workerWasm.js

/**
 当我们实例化Wasm模块时,给它一个工作环境:
 * nBodyForces:{}是一个可以导入到AssemblyScript中的函数表. See top of nBodyForces.ts
 * env:{}描述在实例化Wasm模块时发送给它的环境
 */
const importObj = {
  nBodyForces: {
    logI(data) { console.log("Log() - " + data); },
    logF(data) { console.log("Log() - " + data); },
  },
  env: {
    abort(msg, file, line, column) {
      // wasm.__getString()是由汇编脚本的加载器添加的: 
      // http://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(", "));
    }
  }
}

在我们的AssemblyScript代码中,我们可以像这样完成这些函数的导入:

// nBodyForces.ts
declare function logI(data: i32): void
declare function logF(data: f64): void

Note: Abort and trace are imported automatically.

From AssemblyScript, we can export our interface. Here are some exported constants:

// 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

And here is the export of nBodyForces() which we will call from JavaScript. We export the type Float64Array 在文件的顶部,这样我们就可以在我们的web worker中使用AssemblyScript的JavaScript加载器来获取数据(见下文):

// src/assembly/nBodyForces.ts

export const FLOAT64ARRAY_ID = idof();

...

/**
 * Given N bodies with mass, in a 3d space, 计算每个物体所受的重力.
 * 
 *这个函数被导出到JavaScript,所以只接受/返回数字和数组.
 *对于N个物体,传递一个4N值的数组(x,y,z,质量),并期望一个3N的力数组(x,y,z)
 *这些力可以应用于物体质量以更新其在模拟中的位置.
 *计算3-向量每对独特的身体相互适用.
 * 
 *   0 1 2 3 4 5
 * 0   x x x x x
 * 1     x x x x
 * 2       x x x
 * 3         x x
 * 4           x
 * 5
 * 
 *将这些力总和成一个3向量x,y,z力数组
 * 
 * Return 0 on success
 */
导出函数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++) {
      //计算物体相互作用的力
      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
      )

      //将这一对相互作用的力加到它们的总作用力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    
      arforces [fJ] = arforces [fJ] - f[0] //向相反方向施力
      arrForces[fJ + 1] = arrForces[fJ + 1] - f[1]
      arrForces[fJ + 2] = arrForces[fJ + 2] - f[2]
    }
  }
  //对于每个物体,返回所有其他物体施加到它的力的总和.
  //如果你想调试wasm,你可以使用trace或log函数 
  // described in workerWasm when we initialized
  // E.g. trace("nBodyForces returns (b0x, b0y, b0z, b1z): ", 4, arrForces[0], arrForces[1], arrForces[2], arrForces[3]) // x,y,z
  return arrForces  // success
}

WebAssembly Artifacts: .wasm and .wat

When our AssemblyScript nBodyForces.ts is compiled into a WebAssembly nBodyForces.wasm binary,还有一个选项可以创建一个“文本”版本来描述二进制文件中的指令.

WebAssembly Artifacts

Inside the nBodyForces.wat file, we can see these imports and exports:

;; 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))))
 (import "env" "trace" (func $~lib/ builts /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))
 (出口“__rtti_base”(全球~ lib / rt / __rtti_base美元))

 ;; Finally our exported constants and function
 (导出“FLOAT64ARRAY_ID”(全局$nBodyForces/FLOAT64ARRAY_ID))
 (export "G" (global $nBodyForces/G))
 (导出“bodySize”(全局$nBodyForces/bodySize))
 (export“forceSize”(global $nBodyForces/forceSize))
 (export“nBodyForces”(func $nBodyForces/nBodyForces))

 ;; Implementation details
 ...

We now have our nBodyForces.wasm binary and a web worker to run it. Get ready for blastoff! And some memory management!

To complete the integration, 我们必须向WebAssembly传递一个浮动变量数组,并向JavaScript返回一个浮动变量数组.

With naive JavaScript bourgeois, 我开始在跨平台高性能运行时中随意传递这些华丽的可变大小数组. 到目前为止,在WebAssembly之间传递数据是这个项目中最意想不到的困难.

However, with many thanks for the heavy lifting done by the AssemblyScript team, we can use their “loader” to help:

// workerWasm.js - our web worker
/**
 * AssemblyScript加载器增加了移动数据到/从AssemblyScript的助手.
 * Highly recommended
 */
Const loader = require("assemblyscript/lib/loader")

The require() 意味着我们需要使用Rollup或Webpack这样的模块打包器. 在这个项目中,我选择Rollup是因为它的简单性和灵活性,并且从未回头.

记住,我们的web worker运行在一个单独的线程中,本质上是一个 onmessage() function with a switch() statement.

loader 用一些额外方便的内存管理函数创建wasm模块. __retain() and __release() 管理工作运行时中的垃圾收集引用 __allocArray() 将参数数组复制到wasm模块的内存中 __getFloat64Array() 将结果数组从wasm模块复制到工作运行时

We can now marshal float arrays in and out of nBodyForces() and complete our simulation:

// workerWasm.js
/**
 Web worker监听来自主线程的消息.
 */
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 


    //消息:给定一个描述物体(x,y,x,mass)系统的浮点数数组, 
    //计算每个物体的重力

    case 'nBodyForces':
      if (!wasm) throw new Error('wasm not initialized')

      // Copy msg.将arrBodies数组放入wasm实例中,增加GC计数
      const dataRef = wasm.__retain(wasm.__allocArray(wasm.FLOAT64ARRAY_ID, msg.arrBodies));
      //同步执行该线程中的计算
      const resultRef = wasm.nBodyForces(dataRef);
      //从wasm实例复制结果数组到javascript运行时
      const arrForces = wasm.__getFloat64Array(resultRef);

      //从__retain()中减少dataRef的GC计数, 
      //从wasm模块中的新Float64Array获取GC计数
      wasm.__release(dataRef);
      wasm.__release(resultRef);
      
      // Message results back to main thread.
      // see nBodySimulation.js this.worker.onmessage
      return this.postMessage({
        purpose: 'nBodyForces', 
        arrForces
      })
  }
}

有了我们所学到的,让我们回顾一下我们的web worker和WebAssembly之旅. Welcome to the new browser-backend of the web. These are links to the code on GitHub:

  1. GET Index.html
  2. main.js
  3. nBodySimulator.js - passes a message to its web worker
  4. workerWasm.js - calls the WebAssembly function
  5. nBodyForces.ts - calculates and returns an array of forces
  6. workerWasm.js - passes the results back to the main thread
  7. nBodySimulator.js - resolves the promise for forces
  8. nBodySimulator.js -然后将力应用到身体上,并告诉可视化人员去绘画

From here, let’s start the show by creating nBodyVisualizer.js! 下一篇文章将使用Canvas API创建一个可视化工具, and the final post wraps up with WebVR and Aframe.

Understanding the basics

  • Can WebAssembly replace JavaScript?

    WebAssembly不是一种语言,所以它不能取代JavaScript. 此外,在WebAssembly中开发功能和用户体验的效率较低.

  • Why is WebAssembly faster?

    WebAssembly之所以更快,是因为它做得更少,而且是为性能而不是开发人员的可用性而设计的.

  • Can JavaScript be compiled to WebAssembly?

    是的,AssemblyScript编译成WebAssembly,感觉就像Typescript.

Consult the author or an expert on this topic.
Schedule a call
Michael Cole's profile image
Michael Cole

Located in Dallas, United States

Member since September 10, 2014

About the author

Michael is an expert full-stack web engineer, speaker, 拥有20多年经验和计算机科学学位的顾问.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Previously At

Ernst & Young

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Toptal Developers

Join the Toptal® community.