了解WebAssembly的原理WebAssembly是一个可以在浏览器上运行的二进制可执行格式文件。这将是浏览器进化史上的又一次革命。自从浏览器问世以来,javascript就成为了在浏览器上执行程序的唯一标准。越来越多的应用程序通过javascript开发并运行在浏览器上;浏览器提出了更多挑战。最重要的问题之一是性能问题。JavaScript是一种弱类型的解释型脚本语言。天生慢,成为很多h5应用的软肋。虽然googleV8在2008年引入了即时编译等技术,大大提高了js的运行速度,但一些大型应用,如游戏、视频编辑、压缩、算法等,仍然不适合在浏览器上运行。WebAssembly的到来解决了这个问题,为开发基于浏览器的应用程序提供了另一种编程语言选择。2017年,三大浏览器同时加入了对WebAssembly的支持,标志着WebAssembly已经达到了生产实用化的标准。为什么WebAssembly比javascript更快回答这个问题需要深入了解浏览器执行javascript代码的各个方面。浏览器加载和执行javascript大致可以分为以下几个步骤:下载、解析、执行和优化、垃圾回收。下载javascript是明文下载的。相比之下,webassembly是以二进制格式存储的,其结构更加精简,体积更小。js引擎解析完javascript下载后,需要经过tokenize和parse两个阶段将其转化为AST(抽象语法树),再转化为浏览器需要的中间字节码。由于js是一门比较高级的语言,解析js也需要做更多的事情。webassembly的格式类似于汇编语言。它本来就是一个中间字节码,更接近于需要运行的机器码。需要简单的转换工作,将其转换成CPU可以直接执行的机器码。下图是一个真实运行的webassembly(是文本形式,只是为了调试方便),可以看出它和assembly很像,转换成机器码更容易。执行和优化在执行阶段,js一般采用解释型执行策略,也就是说javascript指令的每一次执行都必须通过js引擎转交给cpu。现代js引擎也采用了即时编译策略。这就需要同时运行一个profiler,关注各个函数的调用。当profiler发现一个函数被调用的频率更高时,它会将这个函数丢给编译器,为它生成一个更快的编译版本。在某些情况下,参数类型会发生变化。这时候就需要删除之前的编译版本,针对新的参数类型编译新的版本。但是由于类汇编的结构,webassembly通过简单的编译就可以转化为直接在CPU上运行的机器码,执行速度更快。垃圾收集javascript需要同时间歇性的运行一个垃圾收集器来扫描堆上的垃圾并释放内存。垃圾回收器的运行和js引擎的执行是相互排斥的,导致js的执行会被垃圾回收器间歇性的打断。Webassembly不负责垃圾回收,只能由编程语言自己解决。所以不同的编程语言是不一样的。C/C++手动管理内存(malloc/free,new/delete),而rust使用基于生命周期的自动内存管理。所有这些内存管理方法都不需要间歇性的全局暂停。因此性能更好。从以上几个方面来看,WebAssembly确实比javascript性能更高。事实上,在现阶段,WebAssembly的执行时间大致相当于原生程序X1.2的执行时间。加载和执行WebAssemblywasm是WebAssembly格式的浏览器可执行文件。它是二进制的,但不像桌面win32程序那样,可以随意使用系统资源,调用操作系统API。实际上,所有与外界相关的操作,都必须通过javascript传入。比如:要申请一段内存,必须要javascript申请,传给他。在浏览器上,javascript做不到的,它也做不到;javascript可以做什么,它可以做得更快。这就是它的价值。目前js必须启动WebAssembly的加载和实例化(后面可能会有单独的加载机制)。以下函数使用fetchAPI加载wasm文件并实例化wasm模块。函数fetchAndInstantiate(url,importObject){returnfetch(url).then(response=>response.arrayBuffer()).then(bytes=>WebAssembly.instantiate(bytes,importObject)).then(results=>results.instance);}fetchAndInstantiate('module.wasm',importObject).then(function(instance){...})importObject是浏览器需要注入webassembly的交互API。如下,一个真正运行的importObject,包含了很多js函数。注意global.memory是webassembly程序执行使用的内存,是js请求的一个很大的ArrayBuffer。学习WebAssembly开发讲了这么多WebAssembly的优点,接下来就讲讲WebAssembly的开发。为WebAssembly开发并不意味着手动编写WebAssembly汇编程序。一个开源项目emscripten提供了可以编译C/C++并输出WebAssembly的wasm文件的sdk。目前,rust还支持编译为wasm。未来所有支持编译为LLVM字节码的编程语言理论上都可以输出wasm。安装好emscripten,下载emscriptensdk后,是一个压缩文件,其实就是sdk包管理器。您需要执行以下命令来完成SDK安装。./emsdkupdate./emsdkinstalllatest./emsdkactivatelatestsource./emsdk_env.sh现在有可用的emcc编译器,输入:emcc--version查看编译器版本。emsdk安装完成后,emscripten文件中包含了根据版本号安装的sdk内容。里面有很多C/C++的用例,可以自己研究。简单的demo这个简单的C程序可以直接编译成wasm。#includeintmain(){printf("你好,世界!\n");return0;}./emcchello_world.cnodea.out.jsemcc默认只输出一个js(asmjs)。asmjs是webassembly的早期原型,它提供了webassembly在旧浏览器上的兼容性。使用以下命令输出webassembly二进制wasm。./emcchello_world.c-sWASM=1-oindex.html本次编译输出三个文件:index.html、index.js、index.wasm。通过静态服务器打开index.html,可以在控制台看到输出。这个index.html是一个调试页面。生产中加载webassembly,一般需要自己编写index.html,只保留js和wasm文件即可。在上面的示例中,printf的标准输出被定向到浏览器的控制台。系统API调用被js实现替代。实际上,libc中的很多功能都是通过emscripten实现的,作为浏览器上的兼容方案,从而更好的与浏览器集成。Environment所有的编程语言都必须和它的运行环境打交道,否则除了让CPU满负荷运转之外没有任何实用价值。运行在浏览器上的webassembly通过js相互调用来发挥作用。Emscriptensdk提供了许多API来与js运行时环境/浏览器进行交互。它定义在其中两个头文件中:emscripten.h:里面定义了一些基本功能相关的API,包括调用js、文件读写、网络请求等,这些API在node.js中也可以使用。html5.h中定义了浏览器中与DOM相关的各种操作,包括DOM、事件、设备相关等。接下来提取一些关键的API来说明webassembly是如何与浏览器协同工作的。调用jsEM_ASM宏,让webassembly可以直接调用js。EM_ASM(alert('hai');alert('bai'));如果需要从js获取执行结果,可以使用EM_ASM_INT和EM_ASM_DOUBLE分别获取int和double值。intx=EM_ASM_INT({返回$0+42;},100);如果需要给js传递一个字符串,可以给js传递一个指向字符串开头的指针。由于js可以访问整个wasm程序的内存区域,所以js可以使用这个指针从内存中读取字符串。Module对象上的函数UTF8ToString(ptr)、UTF16ToString(ptr)、UTF32ToString(ptr)、Pointer_stringify(ptr,length)可以获取指针处的字符串。char*sample="这是一个字符串";EM_ASM_({console.log("js得到字符串:",Module.UTF8ToString($0));},示例);标准输入输出标准输出我们之前已经看到了,printf最后是转给Module.print的,默认是通过console.log来实现的。标准错误输出最终会被传输到Module.printErr,默认由console.error实现。从标准输入读取成为浏览器上的提示框。体验不好,尽量不要看。显示Emscripten支持两种GUI显示方法。DOM:wasm可以调用js,js可以操作DOM。因此wasm可以通过js操作DOM来创建程序的GUI。WebglCanvas:除了DOM,emscripten还可以提供opengles的浏览器实现。通过操作一个WebglCanvas,在Canvas上绘制显示的内容。事件循环C++GUI程序一般都有一个事件循环,它实际上是一个无限循环,在GUI层面反复获取和处理各种事件。这样程序运行完main函数就不会直接退出了。webassembly程序运行在浏览器上,而浏览器是事件驱动的,已经有了事件循环。如果不改直接进入浏览器,会导致浏览器的GUI进程卡死。所以webassembly程序需要浏览器来控制事件循环。emscripten_set_main_loop(em_callback_funcfunc,intfps,intsimulate_infinite_loop)函数接受一个函数指针后,浏览器会根据fps准时调用传入的函数。#include#includeintframe=0;voidmain_loop(void){printf("frame:%d\n",frame);frame++;}intmain(void){emscripten_set_main_loop(main_loop,0,1);return0;}存储浏览器隔离了程序直接操作存储的权限,所以webapp是安全的,但是很多C代码都有同步文件操作的API,比如打开,写入,关闭。为了兼容性,emscripten实现了一个内存文件系统,可以通过全局对象FS访问。下图是FS对象下的函数。另外,emcc还提供了--preload-file参数。在webassembly程序加载过程中,将预加载的文件放在虚拟文件系统中。wasm中的文件虽然存储在内存中,但是通过indexDB支持持久化。js如下,挂载一个indexdb文件夹到/data目录下,然后FS.syncfs将indexdb中的文件同步到内存中。FS.mkdir('/数据');FS.mount(IDBFS,{},'/data');FS.syncfs(true,function(err){});接下来,所有在/data目录下的读Write,都在内存中同步读写。当程序关闭时,需要调用FS.syncfs(false,function(err){})将内存中的文件反向同步回indexdb。库emsdk提供了一些常用C++库的WebAssembly兼容版本。使用emcc--show-ports命令显示。如果要使用SDL2,需要在emcc中加入选项-sUSE_SDL=2来链接SDL2库。目前,emcc内置了对这些库的支持。$emcc--show-ports可用端口:zlib(USE_ZLIB=1;zlib许可证)libpng(USE_LIBPNG=1;zlib许可证)SDL2(USE_SDL=2;zlib许可证)SDL2_image(USE_SDL_IMAGE=2;zlib许可证)ogg(USE_OGG=1;zlib许可证)vorbis(USE_VORBIS=1;zlib许可证)bullet(USE_BULLET=1;zlib许可证)freetype(USE_FREETYPE=1;freetype许可证)SDL2_ttf(USE_SDL_TTF=2;zlib许可证)SDL2_net(zlib许可证)Binaryen(Apache2.0)cocos2d如果需要的库不在列表中,需要先用emsdk编译需要的库(可能会涉及到库的改动)。然后编译链接输出最终目标。emcc不支持动态链接。展望未来,webassembly已经完成了最小功能版MVP的开发,表现非常出色。可以看出,未来更多的h5应用/游戏通过webassembly会有更好的体验。使用C/C++/rust进行webapp开发和混合编程也会有很多不错的探索。未来h5能否通过webassembly撼动原本的大门,让我们拭目以待。