WebAssembly的发展历程相对较短,但影响深远。WebAssembly 于 2015 年首次发布,先驱技术是来自Mozilla的asm.js和Google Native Client,最初的实现是基于asm.js的功能集。自2017年3月由WebAssembly创造的MVP的预览版发布以来,WebAssembly发展迅速,目前已经部署到了所有主流浏览器。到了2019年,WebAssembly 1.0成为了W3C推荐的标准,这打破了之前仅用JavaScript来进行Web开发的局面。那么什么是WebAssembly? 这篇文章让你从多方面了解WebAssembly这一技术。
WebAssembly(简称Wasm)是一种新型的、可以在现代Web浏览器中运行的代码。这是一种低级类汇编语言,其二进制格式紧凑,为诸如 C、C++和 Rust 等低级源语言提供一个高效的编译目标,以便它们可以在Web上运行。开发者通过自选语言编写代码,然后将其编译为WebAssembly字节码进行运行。字节码在客户端(通常是Web浏览器)上运行,在那里它被编译为可执行机器码并以接近原生的速度执行。
由于Wasm提供给浏览器的是以二进制格式编译的代码,节省了下载和解析JavaScript代码时间,使代码执行效率更高。此外,WASM提供了开发者手动管理内存的选项,以及可以直接访问内存的特性,从而提升了运行效率。WebAssembly不仅具有高效的解析和执行能力,还有着硬件和操作系统无关的低级虚拟机模型,提供了安全的运行环境。当前,WASM可以兼容主流的Web浏览器,例如Chrome、Edge、Firefox、Opera和Safari。
WASM被设计为与JavaScript协同工作,以此实现Web平台上的高性能应用。综合来看,WASM在Web平台上表现出近乎原生的开发速度,充分凸显了WebAssembly的性能与功能,以及JavaScript的表现力与灵活性,使得客户端应用可以轻松地在Web上运行。
在了解如何编译C语言为WASM的步骤之前,你需要先了解几个关键原理。
模块(Module): 表示一个已经被浏览器编译为可执行机器码的 WebAssembly 二进制代码。模块中包含一系列的函数和数据(例如,全局变量和初始化的内存)。模块是无状态的,能够像ES2015 的模块一样声明导入和导出。
内存(Memory): WebAssembly使用一种名为线性内存的数据结构来表示内存。线性内存是一个连续的字节块,其大小总是一页(64 KiB)的倍数。WebAssembly代码可以直接读取和写入这些字节。
表格(Table): 可调整大小的类似于数组的结构,包含引用(例如函数),出于安全和可移植性考虑,这些引用不能以原始字节形式存储在内存中。
实例(Instance): 一个已经与运行时使用的所有状态配对的模块,包括内存、表和导入值集。一个实例就像一个 ES 模块,它被加载到一个特定的整体中,并带有一组特定的导入值。
一个WebAssembly模块定义了一系列的函数、全局变量、内存和表格,它们通过与特定的导入和导出的值结合,可以被实例化为一个运行的应用。而内存和表格也可以被实例化为运行的应用,它们的值随着应用的执行而改变。
在示例如何用C语言编译为 Wasm之前,你需要满足一个前提条件 —— 获取 Emscripten SDK来配置安装环境。使用 Emscripten 编译主要适用于两个场景,下面分别来了解一下具体的操作步骤。
你可以用 C 语言编写一个程序,使用 Emscripten SDK 将该程序编译成 WebAssembly,然后创建一个 HTML 文件,在网络服务器上加载并运行 WebAssembly 代码。
hello.c
的文件中:#include
int main() {
printf("Hello World\n");
return 0;
}
emcc hello.c -o hello.html
-o hello.html
—— 这指定我们希望Emscripten生成一个HTML页面来运行我们的代码(以及要使用的文件名),以及Wasm模块和JavaScript粘合代码来编译和实例化Wasm,使其可以在Web环境中使用。
hello.wasm
)hello.js
)hello.html
)hello.html
来运行示例。Firefox 52、Chrome 57、Edge 57 和 Opera 44 默认启用了 WebAssembly。hello2.c
的文件中:#include
int main() {
printf("Hello World\n");
return 0;
}
shell_minimal.html
。将其复制到之前新目录中名为html_template
的子目录中。emcc -o hello2.html hello2.c -O3 --shell-file html_template/shell_minimal.html
-o hello2.html
,这意味着编译器仍将输出 JavaScript 粘合代码 和.html
。-O3
,用于优化代码。Emcc 与任何其他 C 编译器一样具有优化级别,包括:(-O0
无优化)、-O1
、-O2
、-Os
、-Oz
、-Og
和-O3
。-O3
是发布版本的良好设置。--shell-file html_template/shell_minimal.html
—— 这提供了您要用来创建将运行示例的 HTML 的 HTML 模板的路径。hello2.html
,其内容与模板大致相同,并添加了一些粘合代码来加载生成的 Wasm、运行它等。在浏览器中打开它,您将看到与上一个示例大致相同的输出。如果你在 C 代码中定义了一个函数,而又想根据需要从 JavaScript 中调用该函数,那么您可以使用Emscripten ccall()
函数和EMSCRIPTEN_KEEPALIVE
声明(该声明会将您的函数添加到导出函数列表中)来实现这一目的。
hello3.c.
默认情况下,Emscripten 生成的代码只会调用 main()
函数,其他的函数将被视为无用代码。在一个函数名之前添加 EMSCRIPTEN_KEEPALIVE
能够防止这样的事情发生。你需要导入 emscripten.h
库来使用 EMSCRIPTEN_KEEPALIVE
。#include
#include
int main() {
printf("Hello World\n");
return 0;
}
#ifdef __cplusplus
#define EXTERN extern "C"
#else
#define EXTERN
#endif
EXTERN EMSCRIPTEN_KEEPALIVE void myFunction(int argc, char ** argv) {
printf("MyFunction Called\n");
}
{{{ SCRIPT }}}
内容的 html_template/shell_minimal.html
也添加到这个新目录中(在实际开发环境中,显然应将其放在中心位置)。NO_EXIT_RUNTIME
进行编译:否则,当 main()
退出时,运行时将被关闭,并且调用编译后的代码将无效。这对于正确的 C 语言模拟来说非常必要:例如,确保调用 atexit()
函数。emcc -o hello3.html hello3.c --shell-file html_template/shell_minimal.html -s NO_EXIT_RUNTIME=1 -s "EXPORTED_RUNTIME_METHODS=['ccall']"
myFunction()
JavaScript 函数。首先,在文本编辑器中打开hello3.html
文档。
元素,如下所示,就在第一个