文章

WebAssembly

WebAssembly

Wasm

  • 用于Web浏览器的低级虚拟机
  • 一种二进制指令格式
  • 用于在沙箱环境中执行类似汇编语言的代码

WebAssembly 旨在作为JavaScript的补充,而不是替代品,它可以让Web应用程序在浏览器中运行得更快。

主要优势在于其性能

  • 二进制格式使得代码加载速度更快
  • 低级指令集使得执行速度更高
  • 适合处理高性能计算任务:例如游戏、音频/视频处理、物理模拟和3D渲染等。

示例

0. 简单的C语言代码示例

1
2
3
4
5
6
7
8
9
10
11
#include <emscripten.h>

EMSCRIPTEN_KEEPALIVE
int add(int a, int b) {
  return a + b;
}

EMSCRIPTEN_KEEPALIVE
int subtract(int a, int b) {
  return a - b;
}

EMSCRIPTEN_KEEPALIVE宏用于告诉Emscripten编译器保留这个函数,以便在WebAssembly模块中导出。

1. 编译

使用Emscripten编译add.c文件。在命令行中,导航到包含add.c文件的目录,然后运行以下命令

  • 为导出的函数添加下划线 _ 是必要的
  • Emscripten将C/C++中的函数名映射到WebAssembly模块时,会在函数名前添加一个下划线。避免与JavaScript中的保留字和内置对象冲突。
1
emcc add.c -o add.wasm -s EXPORTED_FUNCTIONS="['_add', '_subtract']" -s EXTRA_EXPORTED_RUNTIME_METHODS="['cwrap']" -s WASM=1

2. 不用宿主环境中使用

2.1 Html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>WebAssembly Example</title>
  </head>
  <body>
    <script>
      fetch("add.wasm")
        .then((response) => response.arrayBuffer())
        .then((bytes) => WebAssembly.instantiate(bytes))
        .then((results) => {
          const { instance } = results;
          const add = instance.exports._add;
          const subtract = instance.exports._subtract;
          console.log("3 + 5 =", add(3, 5)); // 输出 "3 + 5 = 8"
          console.log("5 - 3 =", subtract(5, 3)); // 输出 "5 - 3 = 2"
        })
        .catch((error) => {
          console.error("Error loading WebAssembly module:", error);
        });
    </script>
  </body>
</html>

2.2 Cocos Creator

  1. 将编译好的.wasm文件(例如add.wasm)放入Cocos Creator项目的assets文件夹中。

  2. 在Cocos Creator中创建一个新的脚本组件(例如WasmLoader.js),并在start或需要的函数中加载和实例化WebAssembly模块:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    
    cc.Class({
        extends: cc.Component,
       
        start() {
            this.loadWasmModule();
        },
       
        async loadWasmModule() {
            try {
                // 获取.wasm文件的URL
                const wasmUrl = cc.url.raw('resources/add.wasm');
       
                // 加载和实例化WebAssembly模块
                const response = await fetch(wasmUrl);
                const bytes = await response.arrayBuffer();
                const result = await WebAssembly.instantiate(bytes);
                const instance = result.instance;
       
                // 调用导出的函数
                const add = instance.exports._add;
                console.log('3 + 5 =', add(3, 5)); // 输出 "3 + 5 = 8"
            } catch (error) {
                console.error('Error loading WebAssembly module:', error);
            }
        },
    });
    

如果将Cocos Creator项目导出为原生应用(例如iOS或Android应用),那么资源加载方式会有所不同。

在原生应用中,资源通常会被打包到应用内部,因此不需要通过HTTP操作来加载它们。

在Cocos Creator中,使用cc.assetManager.load方法来加载资源,这个方法会自动处理不同平台和环境下的资源加载。

使用cc.assetManager.load加载WebAssembly模块的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
cc.Class({
    extends: cc.Component,

    start() {
        this.loadWasmModule();
    },

    async loadWasmModule() {
        try {
            // 获取.wasm文件的URL
            const wasmUrl = cc.url.raw('resources/add.wasm');

            // 加载.wasm文件
            const result = await new Promise((resolve, reject) => {
                cc.assetManager.load({ url: wasmUrl, type: 'binary' }, (error, data) => {
                    if (error) {
                        reject(error);
                    } else {
                        resolve(data);
                    }
                });
            });

            // 实例化WebAssembly模块
            const bytes = new Uint8Array(result);
            const wasmModule = await WebAssembly.instantiate(bytes);
            const instance = wasmModule.instance;

            // 调用导出的函数
            const add = instance.exports._add;
            console.log('3 + 5 =', add(3, 5)); // 输出 "3 + 5 = 8"
        } catch (error) {
            console.error('Error loading WebAssembly module:', error);
        }
    },
});

Chrome Devtool 调试


WebGL 模式的 Unity 游戏跑在浏览器上,相当于游戏逻辑部分跑在 wasm,渲染部分跑在 WebGL 上。


定点数案例

TypeScripts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class FixedPointNumber {
    private static scaleFactor = 1000; // 放大因子,根据需要调整
    private value: number;

    constructor(n: number) {
        this.value = FixedPointNumber.fromNumber(n);
    }

    // 将实数转换为定点数
    private static fromNumber(n: number): number {
        return Math.round(n * this.scaleFactor);
    }

    // 将定点数转换为实数
    private static toNumber(n: number): number {
        return n / this.scaleFactor;
    }

    // 定点数加法
    add(b: FixedPointNumber): FixedPointNumber {
        return new FixedPointNumber(FixedPointNumber.toNumber(this.value + b.value));
    }

    // 定点数减法
    subtract(b: FixedPointNumber): FixedPointNumber {
        return new FixedPointNumber(FixedPointNumber.toNumber(this.value - b.value));
    }

    // 定点数乘法
    multiply(b: FixedPointNumber): FixedPointNumber {
        return new FixedPointNumber(
            FixedPointNumber.toNumber(Math.round((this.value * b.value) / FixedPointNumber.scaleFactor))
        );
    }

    // 定点数除法
    divide(b: FixedPointNumber): FixedPointNumber {
        return new FixedPointNumber(
            FixedPointNumber.toNumber(Math.round((this.value * FixedPointNumber.scaleFactor) / b.value))
        );
    }

    // 获取实数值
    toNumber(): number {
        return FixedPointNumber.toNumber(this.value);
    }
}

// 使用示例
let a = new FixedPointNumber(1.23);
let b = new FixedPointNumber(4.56);
let sum = a.add(b);
console.log(sum.toNumber());  // 输出:5.79

C/C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <emscripten.h>

const int SCALE_FACTOR = 1024;

EMSCRIPTEN_KEEPALIVE
int from_number(double n) {
  return (int)(n * SCALE_FACTOR + 0.5);
}

EMSCRIPTEN_KEEPALIVE
double to_number(int n) {
  return (double)n / SCALE_FACTOR;
}

EMSCRIPTEN_KEEPALIVE
int add(int a, int b) {
  return a + b;
}

EMSCRIPTEN_KEEPALIVE
int subtract(int a, int b) {
  return a - b;
}

EMSCRIPTEN_KEEPALIVE
int multiply(int a, int b) {
  return (a * b) >> 10; // 使用位移运算优化除法
}

EMSCRIPTEN_KEEPALIVE
int divide(int a, int b) {
  return (a << 10) / b; // 使用位移运算优化乘法
}

编译 fixed_point.c, 生成两个文件:fixed_point.wasm(WebAssembly 模块)和 fixed_point.js(加载 WebAssembly 模块的 JavaScript 文件)。

1
emcc fixed_point.c -s WASM=1 -O3 -o fixed_point.js -s EXPORTED_FUNCTIONS="['_from_number', '_to_number', '_add', '_subtract', '_multiply', '_divide']" -s EXTRA_EXPORTED_RUNTIME_METHODS="['ccall', 'cwrap']"

将生成的 fixed_point.wasmfixed_point.js 文件复制到 Cocos Creator 项目的 assets 目录下。然后,在 TypeScript 代码中加载和使用这个模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// FixedPoint.ts
const { ccall, cwrap } = require("fixed_point.js");

const from_number = cwrap("from_number", "number", ["number"]);
const to_number = cwrap("to_number", "number", ["number"]);
const add = cwrap("add", "number", ["number", "number"]);
const subtract = cwrap("subtract", "number", ["number", "number"]);
const multiply = cwrap("multiply", "number", ["number", "number"]);
const divide = cwrap("divide", "number", ["number", "number"]);

export class FixedPoint {
  static fromNumber(n: number): number {
    return from_number(n);
  }

  static toNumber(n: number): number {
    return to_number(n);
  }

  static add(a: number, b: number): number {
    return add(a, b);
  }

  static subtract(a: number, b: number): number {
    return subtract(a, b);
  }

  static multiply(a: number, b: number): number {
    return multiply(a, b);
  }

  static divide(a: number, b: number): number {
    return divide(a, b);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// TestFixedPoint.ts
import { FixedPoint } from "./FixedPoint";

const { ccclass, property } = cc._decorator;

@ccclass
export default class TestFixedPoint extends cc.Component {
  onLoad() {
    let a = FixedPoint.fromNumber(1.23);
    let b = FixedPoint.fromNumber(4.56);
    let sum = FixedPoint.add(a, b);
    console.log(FixedPoint.toNumber(sum)); // 输出:5.79
  }
}
本文由作者按照 CC BY 4.0 进行授权