WebAssembly 简介

229 阅读11分钟

什么是 WebAssembly

WebAssembly 是一种新型的低级字节码,是一种可移植、体积小、加载快并且安全的二进制格式,可以在现代 Web 浏览器中运行。WebAssembly 旨在成为一种通用的编译目标,可以将任何语言编译成 WebAssembly 字节码,以便在 Web 上运行。

WebAssembly 的优势

  • 体积小:WebAssembly 二进制文件通常比等效的 JavaScript 代码小 20-30 倍。
  • 加载快:WebAssembly 二进制文件可以更快地加载和解析,因为它们是一种紧凑的二进制格式,而不是文本格式,可以直接被浏览器解释执行,避免了解释性语言的性能瓶颈。。
  • 安全:WebAssembly 代码是在沙箱中运行的,这意味着它不能访问主机系统的资源,例如文件系统或网络。
  • 跨平台性:WebAssembly 代码可以在任何支持 WebAssembly 的平台上运行,包括 Web 浏览器、桌面应用程序和移动应用程序。
  • 可扩展性:WebAssembly可以与其他Web平台技术(如JavaScript、WebGL等)配合使用,实现更丰富的Web应用程序。

WebAssembly 的应用场景

  • 游戏:WebAssembly 可以用于构建高性能的 Web 游戏,因为它可以在浏览器中运行原生代码。

  • 图形处理:WebAssembly可以用于图形处理,如图像压缩、渲染等。

  • 音视频处理:WebAssembly可以用于音视频处理,如音频解码、视频编辑等。

  • 应用程序:WebAssembly 可以用于构建高性能的 Web 应用程序。

  • 人工智能:WebAssembly 可以用于在浏览器中运行人工智能模型。

    • TensorFlow.js 就是使用 WebAssembly 运行模型的
  • 库:WebAssembly 可以用于构建可移植的库,这些库可以在任何支持 WebAssembly 的平台上使用。

  • 工具:WebAssembly 可以用于构建各种工具,例如编译器、解释器和调试器。

WebAssembly 的工作原理

WebAssembly 代码是由编译器生成的,可以使用任何编程语言编写。WebAssembly 代码可以在浏览器中运行,因为浏览器有一个内置的 WebAssembly 解释器。当浏览器加载 WebAssembly 模块时,它会将模块的二进制代码加载到内存中,并将其编译成机器代码。然后,浏览器可以直接执行这些机器代码,从而实现高性能的计算。WebAssembly 还提供了一组标准的 API,可以让 JavaScript 代码与 WebAssembly 代码进行交互。这些 API 包括将 JavaScript 值传递给 WebAssembly 模块、从 WebAssembly 模块返回值以及在 JavaScript 和 WebAssembly 之间共享内存等功能。

javascript的执行过程

  • 传统的浏览器(chrome出现之前)

image.jpeg

  • 现代浏览器

image.jpeg

WebAssembly的执行过程

webassembly编译成.wasm文件,wasm是一种二进制格式文件,可以直接被浏览器理解为机器码执行。

这与JavaScript的解析和编译(Just-In-Time Compilation)方式不同,JavaScript需要在运行时编译代码,然后再执行。

如何使用 WebAssembly

要使用 WebAssembly,您需要完成以下步骤:

  1. 编写 WebAssembly 模块:您可以使用任何编程语言编写 WebAssembly 模块,例如 C、C++、Rust 或者 AssemblyScript。

  2. 编译 WebAssembly 模块:您需要使用 WebAssembly 编译器将 WebAssembly 模块编译成 WebAssembly 字节码。常用的 WebAssembly 编译器有:

    1. C/C++:Emscripten,Binaryen,LLVM
    2. Rust:Rustc,Wasm-pack
    3. Go:TinyGo,Wasm-Go
    4. Python:Pyodide
    5. Ruby:WebAssembly.rb
    6. Java:WebAssembly Studio,TeaVM
    7. Typescript: AssemblyScript
  3. 在 JavaScript 中加载 WebAssembly 模块:您可以使用 JavaScript 的 WebAssembly API 将 WebAssembly 模块加载到浏览器中。

  4. 在 JavaScript 中调用 WebAssembly 模块:您可以使用 JavaScript 的 WebAssembly API 调用 WebAssembly 模块中的函数,并将参数传递给这些函数。

示例

使用Emscripten

  1. 安装 Emscripten

    1. emscripten.org/docs/gettin…
  2. 使用 Emscripten 编译 C/C++ 代码

    1. #include <stdio.h>
      
      
      int main() {
        printf("hello, world!\n");
        return 0;
      }
      
    2. 运行命令:emcc hello.c -o hello.js
  3. 胶水代码

    1. 包含两部分:

      1. JavaScript代码:这部分代码是由EMCC生成的,用于加载和解析由C/C++代码编译成的字节码文件(.wasm或.js)以及提供一些必要的JavaScript函数和数据结构。这些函数和数据结构可用于在JavaScript中与C/C++代码进行交互,并且在运行时可供调用。这部分代码可以被视为一个胶水,将C/C++代码与JavaScript代码粘合在一起。
      2. 编译生成的C/C++代码:这部分代码是原始C/C++代码编译成的,包含编译器生成的机器代码和标准库函数。这部分代码可以使用JavaScript函数调用,从JavaScript代码中调用并在浏览器中执行。
    2. 因此,生成的JavaScript胶水代码的主要作用是提供与C/C++代码交互所需的函数和数据结构,以及将JavaScript代码和编译后的C/C++代码粘合在一起,从而使其能够在Web浏览器中运行。

  4. 运行WebAssembly模块

    1. 执行node hello.js,输出hello, world!

      1. // 老方法
        fetch('hello.wasm')
          .then(response => response.arrayBuffer())
          .then(bytes => WebAssembly.instantiate(bytes, {}))
          .then(results => console.log(results.instance.exports.hello()) )
        
        // 新方法,流式实例化
        WebAssembly.instantiateStreaming(fetch('hello.wasm'), {}).then(res => {
          console.log(res.instance.exports.hello())
        })
        
    2. 默认执行main方法

js中调用c文件中的函数

可通过以下两种方式:

  • 使用emcc EXPORTED_FUNCTIONS参数

    #include <stdio.h>
    
    int addd(int a, int b){
        return a + b;
    }
    
    int main() {
        printf("%d\n", addd(10, 20));
        return 0;
    }
    // 输出命令emcc add.c -s EXPORTED_FUNCTIONS='["_addd, _main"]' -o add.js
    // -s指定编译选项,WASM=0表示不使用WebAssembly,-s EXPORTED_FUNCTIONS='["_add"]'指定将.add函数导出到JavaScript中供外部调用,-o add.js指定输出文件名。
    // 使用 emcc 转换 C 代码时,C 中的函数名如果不做特殊处理的话,会变成带有前缀的名称,如:_add
    

    运行命令:emcc add.c -s EXPORTED_FUNCTIONS='["_addd, _main"]' -o add.js

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    
    <body>
         <script>
            Module = {};
    				// Module.onRuntimeInitialized是一个回调函数,当Emscripten模块加载完成并且运行时准备就绪时,会调用该函数。
            // 它的作用是允许您在模块完全初始化之前注册回调函数,这些回调函数可以在模块初始化后执行。
            // 这允许您在模块加载和运行时进行一些初始化操作,例如注册各种功能和类型,以便您可以在模块中访问它们。
            Module.onRuntimeInitialized = function () {
                console.log(Module._addd(1, 22))
            }
        </script>
        <script src="add.js"></script>
    </body>
    
    </html>
    

    然后可以使用_addd函数。

  • c文件中使用EMSCRIPTEN_KEEPALIVE

    #include <stdio.h>
    #include <emscripten.h>
    
    // EMSCRIPTEN_KEEPALIVE可以确保在编译成asm.js或WebAssembly格式的代码中,能够保留C/C++函数的名称,防止被优化器删除,以便在JavaScript中使用这些函数。这个宏定义通常用于在C/C++代码中声明需要导出到JavaScript的函数。
    EMSCRIPTEN_KEEPALIVE
    int addd(int a, int b){
        return a + b;
    }
    
    EMSCRIPTEN_KEEPALIVE
    int fib(int n){
        int a = 0, b = 1, i;
    
        if(n == 0)
            return a;
    
        for(i = 2; i <= n; i++){
            int next = a + b;
            a = b;
            b = next;
        }
    
        return b;
    }
    
    int main() {
        printf("%d\n", addd(10, 20));
        return 0;
    }
    

    运行命令:emcc add.c -o add.js

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    
    <body>
        <script src="add.js"></script>
    </body>
    
    </html>
    

    然后可以使用_addd, _fib等方法。

动态调用js

需要加一些胶水代码把emcc生成的代码包起来


// We are modularizing this manually because the current modularize setting in Emscripten has some issues:
// https://github.com/kripken/emscripten/issues/5820
// In addition, When you use emcc's modularization, it still expects to export a global object called `Module`,
// which is able to be used/called before the WASM is loaded.
// That way, this module can't be used before the WASM is finished loading.

var initJsPromise = undefined;

var initJs = function (moduleConfig) {

    if (initJsPromise){
      return initJsPromise;
    }
    // If we're here, we've never called this function before
    initJsPromise = new Promise(function (resolveModule, reject) {

        // We are modularizing this manually because the current modularize setting in Emscripten has some issues:
        // https://github.com/kripken/emscripten/issues/5820

        // The way to affect the loading of emcc compiled modules is to create a variable called `Module` and add
        // properties to it, like `preRun`, `postRun`, etc
        // We are using that to get notified when the WASM has finished loading.
        // Only then will we return our promise

        // If they passed in a moduleConfig object, use that
        // Otherwise, initialize Module to the empty object
        var Module = typeof moduleConfig !== 'undefined' ? moduleConfig : {};

        // EMCC only allows for a single onAbort function (not an array of functions)
        // So if the user defined their own onAbort function, we remember it and call it
        var originalOnAbortFunction = Module['onAbort'];
        Module['onAbort'] = function (errorThatCausedAbort) {
            reject(new Error(errorThatCausedAbort));
            if (originalOnAbortFunction){
              originalOnAbortFunction(errorThatCausedAbort);
            }
        };

        Module['postRun'] = Module['postRun'] || [];
        Module['postRun'].push(function () {
            // When Emscripted calls postRun, this promise resolves with the built Module
            resolveModule(Module);
        });

        // There is a section of code in the emcc-generated code below that looks like this:
        // (Note that this is lowercase `module`)
        // if (typeof module !== 'undefined') {
        //     module['exports'] = Module;
        // }
        // When that runs, it's going to overwrite our own modularization export efforts in shell-post.js!
        // The only way to tell emcc not to emit it is to pass the MODULARIZE=1 or MODULARIZE_INSTANCE=1 flags,
        // but that carries with it additional unnecessary baggage/bugs we don't want either.
        // So, we have three options:
        // 1) We undefine `module`
        // 2) We remember what `module['exports']` was at the beginning of this function and we restore it later
        // 3) We write a script to remove those lines of code as part of the Make process.
        //
        // Since those are the only lines of code that care about module, we will undefine it. It's the most straightforward
        // of the options, and has the side effect of reducing emcc's efforts to modify the module if its output were to change in the future.
        // That's a nice side effect since we're handling the modularization efforts ourselves
        var module = undefined;

        // The emcc-generated code and shell-post.js code goes below,
        // meaning that all of it runs inside of this promise. If anything throws an exception, our promise will abort


        // The shell-pre.js and emcc-generated code goes above
        return Module;
    }); // The end of the promise being returned

  return initJsPromise;
} // The end of our initJs function

// This bit below is copied almost exactly from what you get when you use the MODULARIZE=1 flag with emcc
// However, we don't want to use the emcc modularization. See shell-pre.js
if (typeof exports === 'object' && typeof module === 'object'){
    module.exports = initJs;
    // This will allow the module to be used in ES6 or CommonJS
    module.exports.default = initJs;
}
else if (typeof define === 'function' && define['amd']) {
    define([], function() { return initJs; });
}
else if (typeof exports === 'object'){
    exports["Module"] = initJs;
}

export default initJs;

在wasm中执行js代码

  • 在C代码中执行js,使用emscripten_run_script命令,类似于JS中的eval()方法

    #include <stdio.h>
    #include <emscripten.h>
    
    int main() {
        // emscripten_run_script()系列函数可以接收动态输入的字符串,该系列辅助函数类比与JS中的eval()方法。
        emscripten_run_script("console.log('hello, word!');");
        return 0;
    }
    
  • 在wasm初始化时WebAssembly.instantiate,引入

    (module
        (import "js" "import1" (func $i1))
        (import "js" "import2" (func $i2))
        (func $main (call $i1))
        (start $main)
        (func (export "f") (call $i2))
    )
    
    var importObj = {js: {
        import1: () => console.log("hello,"),
        import2: () => console.log("world!")
    }};
    fetch('demo.wasm').then(response =>
        response.arrayBuffer()
    ).then(buffer =>
        WebAssembly.instantiate(buffer, importObj)
    ).then(({module, instance}) =>
        instance.exports.f()
    );
    

WebAssembly 语法

WebAssembly 语法类型

  1. 模块(Modules):

WebAssembly程序的最高层级结构是模块,每个模块包含多个段,这些段可以是函数、数据、导入、导出等等。

  1. 函数(Functions):

Wasm中的函数实际上是一些必须按照特定方式定义的指令序列,它从输入栈中获取参数,并将结果压入输出栈。Wasm程序可以通过导入、本地定义或者导出的方式来使用具体的函数。

  1. 类型(Types):

Wasm中的函数类型是一个参数类型列表和一个结果类型。参数类型和结果类型可以是i32、i64、f32、f64等等预定义的类型。函数类型也可以通过本地定义的方式来添加。

  1. 全局变量(Global):

表示一个全局变量,可以在程序的任何地方访问。全局变量可以是不可变的(const)或可变的(var)

  1. 表(Tables):

Wasm中的表是一个数字索引的数组,元素类型可以是任何预定义类型的函数,用于存储函数的指针。表被用来提供动态函数调用功能。

  1. 内存(Memory):

Wasm中的内存是一个字节数组,用于存储动态分配的数据。内存可以通过导入、本地定义或者导出的方式来使用。

  1. 导入(Imports):

Wasm中的导入是用于将函数、表或内存从外部环境导入到模块中的声明。导入的功能类似于在JS中使用的require()函数。

  1. 导出(Exports):

Wasm中的导出是用于将模块内的函数、表、内存等数据导出到外部环境的声明。导出的功能类似于在JS中使用的module.exports变量。

S表达式

“S-表达式”,又被称为 “S-Expression”,或者简写为 “sexpr”,它是一种用于表达树形结构化数据的记号方式。最初,S-表达式被用于 Lisp 语言,表达其源代码以及所使用到的字面量数据。比如,在 Common Lisp 这个 Lisp 方言中,我们可以有如下形式的一段代码。

(print (* 2 (+ 3 4)) )

在 “S-表达式” 中,我们使用一对小括号 “()” 来定义每一个表达式的结构。而表达式之间的相互嵌套关系则表达了一定的语义规则。对一个 “S-表达式” 的求值会从最内层的括号表达式开始。

(module
  (func $addTwo (param $x i32) (result i32)
    get_local $x
    i32.const 2
    i32.add)
  (export "addTwo" (func $addTwo)))

这个程序定义了一个名为“addTwo”的函数,接收一个i32类型的参数,并返回一个i32类型的结果。函数的实现是将参数加2,然后返回结果。最后,这个函数被导出到外部环境中,以便于其他程序可以调用它。

wasm es module

相信 “ES Module” 对 Web 前端开发的同学来说,可谓是再熟悉不过了。作为一种官方的 JavaScript 模块化方案,“ES Module” 使得我们能够通过 “export” 与 “import” 两个关键字,来定义一个 JavaScript 模块所需要导入,以及可以公开导出给外部使用的资源。

那么试想一下,我们是否也可以为 Wasm 提供类似的能力呢?借助于该提案,我们可以简化一个 Wasm 模块的加载、解析与实例化过程。并且可以通过与 JavaScript 一致的方式,来使用从 Wasm 模块中导出的资源。

import { add } from "./add.wasm"; console.log(add(1, 2)); // 3;

可以看到在上面的代码中,相较于我们之前介绍的通过 JavaScript API 来加载和实例化 Wasm 模块的方式,使用 import 的方式会相对更加简洁。

不仅如此,在该提案下,我们也可以通过 

现阶段,该提案还没有被任何浏览器实现。

WebAssembly 的未来

Wasm 诞生于 Web,但却不止于 Web。WebAssembly目前主要应用于Web前端的领域,未来还将逐渐向其他领域扩展。

使用Rust和WebAssembly构建Web服务器。

WebAssembly能够快速编译成CPU本地指令,因此运行速度非常快,适用于需要快速响应的应用场景,例如游戏、音视频处理等。

WebAssembly不依赖于任何特定的编程语言,因此未来它还有可能成为多种编程语言之间的通用语言。