其他分享
首页 > 其他分享> > WebAssembly初探

WebAssembly初探

作者:互联网

本次分享的文章是基于WebAssembly的探索与研究。最近需要做一个与加密相关的项目,想将后端的加密方案直接放到前端使用,好处是加密方案代码只用维护一套,且后端方案更贴近系统底层,应该可以得到更好的性能。恰好发现 WebAssembly ,它是为了可移植的目标而设计的,可以满足需求。

这次研究 WebAssembly的过程中遇到了各种问题,我均记录下来,并在后期可以和大家一起分享,文末放置了参考的文章,大家可以延伸阅读。这篇文章是本系列的第一部分,主要是了解WebAssembly和WebAssembly的基本使用方法。

概述

一、 WebAssembly的诞生

当人们说 WebAssembly 更快的时候,一般来讲是与 JavaScript 相比而言的。

JavaScript 于 1995 年问世,它的设计初衷并不是为了执行起来快,在前 10 个年头,它的执行速度也确实不快。紧接着,浏览器市场竞争开始激烈起来。被人们广为传播的“性能大战”在 2008 年打响。许多浏览器引入了 Just-in-time 编译器,也叫 JIT。基于 JIT 的模式,JavaScript 代码的运行渐渐变快。正是由于这些 JIT 的引入,使得 JavaScript 的性能达到了一个转折点,JS 代码执行速度快了 10 倍。

在这里插入图片描述

随着性能的提升,JavaScript 可以应用到以前根本没有想到过的领域,比如用于后端开发的 Node.js。性能的提升使得 JavaScript 的应用范围得到很大的扩展。

在这里插入图片描述

但这也渐渐暴露出了 JavaScript 的问题:

针对以上两点缺陷,近年来出现了一些 JS 的代替语言,例如:

以上尝试各有优缺点,其中:

三大浏览器巨头分别提出了自己的解决方案,互不兼容,这违背了 Web 的宗旨; 是技术的规范统一让 Web 走到了今天,因此形成一套新的规范去解决 JS 所面临的问题迫在眉睫。

于是 WebAssembly 诞生了,WebAssembly 是一种新的字节码格式,主流浏览器都已经支持 WebAssembly。 和 JS 需要解释执行不同的是,WebAssembly 字节码和底层机器码很相似可快速装载运行,因此性能相对于 JS 解释执行大大提升。 也就是说 WebAssembly 并不是一门编程语言,而是一份字节码标准,需要用高级编程语言编译出字节码放到 WebAssembly 虚拟机中才能运行, 浏览器厂商需要做的就是根据 WebAssembly 规范实现虚拟机。

二、WebAssembly是什么?

WebAssembly(缩写 Wasm)是基于堆栈虚拟机的二进制指令格式。Wasm为了一个可移植的目标而设计的,可用于编译C/C+/RUST等高级语言,使客户端和服务器应用程序能够在Web上部署。

上面这段话是来自官方的定义。

我们可以从字面上理解,WebAssembly的名字带个汇编Assembly,所以我们从其名字上就能知道其意思是给Web使用的汇编语言,是通过Web执行低级二进制语法。但是WebAssembly并不是直接用汇编语言,而是提供了抓换机制(LLVM IR),把高级别的语言(C,C++和Rust)编译为WebAssembly,以便有机会在浏览器中运行。可以看出来它其实是一种运行机制,一种新的字节码格式(.wasm),而不是新的语言。

在这里插入图片描述

三、MAC安装Emscripten

如果要把一个C/C++程序编译成一个.wasm文件,是需要编译工具来完成的。WebAssembly 社区推荐常用工具:

1. 环境依赖

2. 编译Emscripten

接下来,您需要通过源码自己编译一个Emscripten。运行下列命令来自动化地使用Emscripten SDK。

git clone https://github.com/juj/emsdk.git

cd emsdk

# 编译源码
./emsdk install latest

# 激活sdk
./emsdk activate latest

#设置环境变量
source ./emsdk_env.sh

在运行上述命令的时候,可能会遇到如下问题:

    likai@likaideMacBook-Pro:~/resource/emsdk$ ./emsdk.py install latest

    Installing SDK 'sdk-releases-upstream-7a7f38ca19da152d4cd6da4776921a0f1e3f3e3f-64bit'..
    Installing tool 'node-12.18.1-64bit'..
    Downloading: /Users/likai/hisun/resource/emsdk/zips/node-v12.18.1-darwin-x64.tar.gz from https://storage.googleapis.com/webassembly/emscripten-releases-builds/deps/node-v12.18.1-darwin-x64.tar.gz, 20873670 Bytes
    Unpacking '/Users/likai/hisun/resource/emsdk/zips/node-v12.18.1-darwin-x64.tar.gz' to '/Users/likai/hisun/resource/emsdk/node/12.18.1_64bit'
    Done installing tool 'node-12.18.1-64bit'.
    Installing tool 'python-3.7.4-2-64bit'..
    Downloading: /Users/likai/hisun/resource/emsdk/zips/python-3.7.4-2-macos.tar.gz from https://storage.googleapis.com/webassembly/emscripten-releases-builds/deps/python-3.7.4-2-macos.tar.gz, 25365593 Bytes
    Unpacking '/Users/likai/hisun/resource/emsdk/zips/python-3.7.4-2-macos.tar.gz' to '/Users/likai/hisun/resource/emsdk/python/3.7.4-2_64bit'
    Done installing tool 'python-3.7.4-2-64bit'.
    Installing tool 'releases-upstream-7a7f38ca19da152d4cd6da4776921a0f1e3f3e3f-64bit'..
    Downloading: /Users/likai/hisun/resource/emsdk/zips/7a7f38ca19da152d4cd6da4776921a0f1e3f3e3f-wasm-binaries.tbz2 from https://storage.googleapis.com/webassembly/emscripten-releases-builds/mac/7a7f38ca19da152d4cd6da4776921a0f1e3f3e3f/wasm-binaries.tbz2, 69799761 Bytes
    Unpacking '/Users/likai/hisun/resource/emsdk/zips/7a7f38ca19da152d4cd6da4776921a0f1e3f3e3f-wasm-binaries.tbz2' to '/Users/likai/hisun/resource/emsdk/upstream'
    Done installing tool 'releases-upstream-7a7f38ca19da152d4cd6da4776921a0f1e3f3e3f-64bit'.
    Running post-install step: npm ci ...
    Done running: npm ci
    Done installing SDK 'sdk-releases-upstream-7a7f38ca19da152d4cd6da4776921a0f1e3f3e3f-64bit'.

同样激活 Emscripten也是使用 ./emsdk.py activate latest

    likai@likaideMacBook-Pro:~/resource/emsdk$ ./emsdk.py activate latest

    Setting the following tools as active:
       node-12.18.1-64bit
       python-3.7.4-2-64bit
       releases-upstream-7a7f38ca19da152d4cd6da4776921a0f1e3f3e3f-64bit

    Next steps:
    - To conveniently access emsdk tools from the command line,
      consider adding the following directories to your PATH:
        /Users/likai/hisun/resource/emsdk
        /Users/likai/hisun/resource/emsdk/node/12.18.1_64bit/bin
        /Users/likai/hisun/resource/emsdk/python/3.7.4-2_64bit/bin
        /Users/likai/hisun/resource/emsdk/upstream/emscripten
    - This can be done for the current shell by running:
        source "/Users/likai/hisun/resource/emsdk/emsdk_env.sh"
    - Configure emsdk in your bash profile by running:
        echo 'source "/Users/likai/hisun/resource/emsdk/emsdk_env.sh"' >> $HOME/.bash_profile

source ./emsdk_env.sh

    likai@likaideMacBook-Pro:~/resource/emsdk$ source ./emsdk_env.sh

    Adding directories to PATH:
    PATH += /Users/likai/hisun/resource/emsdk
    PATH += /Users/likai/hisun/resource/emsdk/upstream/emscripten
    PATH += /Users/likai/hisun/resource/emsdk/node/12.18.1_64bit/bin
    PATH += /Users/likai/hisun/resource/emsdk/python/3.7.4-2_64bit/bin

    Setting environment variables:
    EMSDK = /Users/likai/hisun/resource/emsdk
    EM_CONFIG = /Users/likai/hisun/resource/emsdk/.emscripten
    EM_CACHE = /Users/likai/hisun/resource/emsdk/upstream/emscripten/cache
    EMSDK_NODE = /Users/likai/hisun/resource/emsdk/node/12.18.1_64bit/bin/node
    EMSDK_PYTHON = /Users/likai/hisun/resource/emsdk/python/3.7.4-2_64bit/bin/python3

3. 验证

emcc -v 不报错就成功了

likai@likaideMacBook-Pro:~/resource/emsdk$ emcc -v

emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 2.0.3
clang version 12.0.0 (/b/s/w/ir/cache/git/chromium.googlesource.com-external-github.com-llvm-llvm--project a39423084cbbeb59e81002e741190dccf08b5c82)
Target: x86_64-apple-darwin19.4.0
Thread model: posix
InstalledDir: /Users/likai/hisun/resource/emsdk/upstream/bin
shared:INFO: (Emscripten: Running sanity checks)

获取帮助 emcc --help,内容过多就不展示了。

看下emcc 的版本是2.0.3

likai@likaideMacBook-Pro:~/resource/emsdk$  emcc --version

emcc (Emscripten gcc/clang-like replacement) 2.0.3 (43fcfd2938b72c57373a910ece897b27aa298852)
Copyright (C) 2014 the Emscripten authors (see AUTHORS.txt)
This is free and open source software under the MIT license.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

四、WebAssembly简单使用和分析

到这里WebAssembly的编译工具已经安装好了,我们使用两个官方样例,看一下WebAssembly是如何使用的,方便后面的学习。

当使用Emscripten来编译的时候有很多种不同的选择,我们介绍其中主要的2种:

1. 生成 HTML 和 JavaScript

先一起看下.wasm的真容,上面提到了.wasm是个二进制文件,打不开,想要看里面内容的话推荐反编译工具wasm2wast,当然浏览器也可以解析,我们通过浏览器简单看下。 右键打开控制台-->Sources-->hello_world.wasm

在这里插入图片描述

果然这个文件看得不太懂,看到了module,我猜这大概是个模块,我找到了main函数,不知道是不是hello_world.c的main,我们还是看胶水代码吧。

在这里插入图片描述

从胶水代码hello_world.js中可以看到,载入了WebAssembly汇编模块(.wasm),原来这个.wasm被胶水代码加载了一下,核心部分如下:

    function instantiateArrayBuffer(receiver) {
    return getBinaryPromise().then(function(binary) {
      return WebAssembly.instantiate(binary, info);
    }).then(receiver, function(reason) {
      err('failed to asynchronously prepare wasm: ' + reason);

      abort(reason);
    });
  }

     // Prefer streaming instantiation if available.
  function instantiateAsync() {
    if (!wasmBinary &&
        typeof WebAssembly.instantiateStreaming === 'function' &&
        !isDataURI(wasmBinaryFile) &&
        // Don't use streaming for file:// delivered objects in a webview, fetch them synchronously.
        !isFileURI(wasmBinaryFile) &&
        typeof fetch === 'function') {
      fetch(wasmBinaryFile, { credentials: 'same-origin' }).then(function (response) {
        var result = WebAssembly.instantiateStreaming(response, info);
        return result.then(receiveInstantiatedSource, function(reason) {
            // We expect the most common failure cause to be a bad MIME type for the binary,
            // in which case falling back to ArrayBuffer instantiation should work.
            err('wasm streaming compile failed: ' + reason);
            err('falling back to ArrayBuffer instantiation');
            return instantiateArrayBuffer(receiveInstantiatedSource);
          });
      });
    } else {
      return instantiateArrayBuffer(receiveInstantiatedSource);
    }
  } 

主要做了如下几件事情:

将wasm模块实例的导出对象传给了Module的子对象asm。倘若我们在上述函数中手动添加打印实例导出对象的代码。

        function receiveInstance(instance, module) {
      ... ...
      Module['asm'] = exports;
      console.log(Module['asm']);  //print instance.exports
      ... ...

在这里插入图片描述

由此可见,上述一系列代码运行后,Module['asm']中保存了WebAssembly实例的导出对象——而导出函数恰是WebAssembly实例供外部调用最主要的入口。

看看我理解的对不,wasm的编译器把C代码编译了.wasm文件,这个文件是个汇编代码,里面有C代码的内容,胶水代码去加载.wasm文件,通过WebAssembly实例对外提供了C代码里面的方法,然后使用javascript调用C代码。最后给人的感觉就是浏览器上能运行C语言的程序。

我们再一起细品下官方原话(翻译过的):

WebAssembly(缩写 Wasm)是基于堆栈虚拟机的二进制指令格式。Wasm为了一个可移植的目标而设计的,可用于编译C/C+/RUST等高级语言,使客户端和服务器应用程序能够在Web上部署。

2. 编译到 wasm,使用JavaScript调用wasm里边的方法。

这个很好理解,就是在编译的时候,不生成默认推荐的html,只生成wasm,然后直接调用wasm即可。这就要我们自己写胶水代码,下面看个简单的例子。步骤如下:

  1. 写一个test.c文件,里面是加减乘除计算。
  2. 编译成.wasm文件
  3. 写一个html,调用.wasm文件
char* toChar (char* str) {
  return str;

}

int add (int x, int y) {
  return x + y;

}

int square (int x) {
  return x * x;

}
    function loadWebAssembly (path, imports = {}) {
        return fetch(path) // 加载文件
               .then(response => response.arrayBuffer()) // 转成 ArrayBuffer
               .then(buffer => WebAssembly.compile(buffer))
               .then(module => {
                 imports.env = imports.env || {}
                 // 开辟内存空间
                 imports.env.memoryBase = imports.env.memoryBase || 0

                 if (!imports.env.memory) {
                   imports.env.memory = new WebAssembly.Memory({ initial: 256 })
                 }
                 // 创建变量映射表
                 imports.env.tableBase = imports.env.tableBase || 0

                 if (!imports.env.table) {
                   // 在 MVP 版本中 element 只能是 "anyfunc"
                   imports.env.table = new WebAssembly.Table({ initial: 0, element: 'anyfunc' })
                 }
                 // 创建 WebAssembly 实例
                 return new WebAssembly.Instance(module, imports)
               })
     }  

            // 加载wasm文件
    loadWebAssembly('test.wasm')
          .then(instance => {
            //调用c里面的方法
            const toChar = instance.exports.toChar
            const add = instance.exports.add
            const square = instance.exports.square

            console.log('return:   ', toChar("12352324"))
            console.log('10 + 20 =', add(10, 20))
            console.log('3*3 =', square(3))
            console.log('(2 + 5)*2 =', square(add(2 + 5)))
      })
有了第一个案例的理解,就大概知道这个意思了,创建了一个WebAssembly的实例,返回WebAssembly导出对象,调用了test.c里面的函数。这里面有一些胶水代码语法相关的知识。[MDN Web docs-WebAssembly](./https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly)

在这里插入图片描述

在这里插入图片描述

可以看到优化后的wasm文件,只有这几个函数了,并且可以看出包含导出test.c中的函数。

五、总结

我们今天通过两个简单的例子讲述了WebAssembly的使用,也进一步理解了WebAssembly是什么,整体的流程是这样的:

在这里插入图片描述

使用Emscripten编译C语言源代码,生成.wasm文件和胶水代码,通过javascript调用胶水代码或者.wasm,使C语言的程序在浏览器中运行。

以上就是这篇文章要分享的全部内容了,下一篇,基于wasm的加密工具。

文章参考

Webassembly官方网站

MDN Web docs-WebAssembly

中文原文


Netwarps 由国内资深的云计算和分布式技术开发团队组成,该团队在金融、电力、通信及互联网行业有非常丰富的落地经验。Netwarps 目前在深圳、北京均设立了研发中心,团队规模30+,其中大部分为具备十年以上开发经验的技术人员,分别来自互联网、金融、云计算、区块链以及科研机构等专业领域。
Netwarps 专注于安全存储技术产品的研发与应用,主要产品有去中心化文件系统(DFS)、去中心化计算平台(DCP),致力于提供基于去中心化网络技术实现的分布式存储和分布式计算平台,具有高可用、低功耗和低网络的技术特点,适用于物联网、工业互联网等场景。
公众号:Netwarps

标签:WebAssembly,likai,resource,wasm,初探,hisun,emsdk
来源: https://blog.51cto.com/14915984/2559007