什么是 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出现之前)
- 现代浏览器
WebAssembly的执行过程
webassembly编译成.wasm文件,wasm是一种二进制格式文件,可以直接被浏览器理解为机器码执行。
这与JavaScript的解析和编译(Just-In-Time Compilation)方式不同,JavaScript需要在运行时编译代码,然后再执行。
如何使用 WebAssembly
要使用 WebAssembly,您需要完成以下步骤:
-
编写 WebAssembly 模块:您可以使用任何编程语言编写 WebAssembly 模块,例如 C、C++、Rust 或者 AssemblyScript。
-
编译 WebAssembly 模块:您需要使用 WebAssembly 编译器将 WebAssembly 模块编译成 WebAssembly 字节码。常用的 WebAssembly 编译器有:
- C/C++:Emscripten,Binaryen,LLVM
- Rust:Rustc,Wasm-pack
- Go:TinyGo,Wasm-Go
- Python:Pyodide
- Ruby:WebAssembly.rb
- Java:WebAssembly Studio,TeaVM
- Typescript: AssemblyScript
-
在 JavaScript 中加载 WebAssembly 模块:您可以使用 JavaScript 的 WebAssembly API 将 WebAssembly 模块加载到浏览器中。
-
在 JavaScript 中调用 WebAssembly 模块:您可以使用 JavaScript 的 WebAssembly API 调用 WebAssembly 模块中的函数,并将参数传递给这些函数。
示例
使用Emscripten
-
安装 Emscripten
-
使用 Emscripten 编译 C/C++ 代码
-
#include <stdio.h> int main() { printf("hello, world!\n"); return 0; } - 运行命令:emcc hello.c -o hello.js
-
-
胶水代码
-
包含两部分:
- JavaScript代码:这部分代码是由EMCC生成的,用于加载和解析由C/C++代码编译成的字节码文件(.wasm或.js)以及提供一些必要的JavaScript函数和数据结构。这些函数和数据结构可用于在JavaScript中与C/C++代码进行交互,并且在运行时可供调用。这部分代码可以被视为一个胶水,将C/C++代码与JavaScript代码粘合在一起。
- 编译生成的C/C++代码:这部分代码是原始C/C++代码编译成的,包含编译器生成的机器代码和标准库函数。这部分代码可以使用JavaScript函数调用,从JavaScript代码中调用并在浏览器中执行。
-
因此,生成的JavaScript胶水代码的主要作用是提供与C/C++代码交互所需的函数和数据结构,以及将JavaScript代码和编译后的C/C++代码粘合在一起,从而使其能够在Web浏览器中运行。
-
-
运行WebAssembly模块
-
执行node hello.js,输出hello, world!
-
// 老方法 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()) })
-
-
默认执行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 语法类型
- 模块(Modules):
WebAssembly程序的最高层级结构是模块,每个模块包含多个段,这些段可以是函数、数据、导入、导出等等。
- 函数(Functions):
Wasm中的函数实际上是一些必须按照特定方式定义的指令序列,它从输入栈中获取参数,并将结果压入输出栈。Wasm程序可以通过导入、本地定义或者导出的方式来使用具体的函数。
- 类型(Types):
Wasm中的函数类型是一个参数类型列表和一个结果类型。参数类型和结果类型可以是i32、i64、f32、f64等等预定义的类型。函数类型也可以通过本地定义的方式来添加。
- 全局变量(Global):
表示一个全局变量,可以在程序的任何地方访问。全局变量可以是不可变的(const)或可变的(var)
- 表(Tables):
Wasm中的表是一个数字索引的数组,元素类型可以是任何预定义类型的函数,用于存储函数的指针。表被用来提供动态函数调用功能。
- 内存(Memory):
Wasm中的内存是一个字节数组,用于存储动态分配的数据。内存可以通过导入、本地定义或者导出的方式来使用。
- 导入(Imports):
Wasm中的导入是用于将函数、表或内存从外部环境导入到模块中的声明。导入的功能类似于在JS中使用的require()函数。
- 导出(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不依赖于任何特定的编程语言,因此未来它还有可能成为多种编程语言之间的通用语言。