WebAssembly 初体验:从零开始重构盘算模块

WebAssembly 初体验:从零开始重构盘算模块从属于笔者的 Web 前端入门与工程实践,更多相干材料文章参考WebAssembly 进修与实践材料索引 React 进修与实践材料索引。本文中运用的游戏代码修改自 WebAssembly 101: a developer’s first steps

WebAssembly 的观点、意义以及将来带来的机能提拔置信已是耳熟能详,笔者在前端每周清单系列中也是经常会引荐 WebAssembly 相干文章。不过笔者也只是相识其观点而未真正付诸实践,本文等于笔者在将我司某个简朴项目中的盘算模块重构为 WebAssembly 历程当中的总结。在简朴的实践中笔者个人感觉,WebAssembly 的笼统水平会比 JavaScript 高不少,将来关于大型项目的迁徙,关于纯前端工程师而言能够存在的坑也是不少,似乎又回到了被指针统治的年代。本文笔者运用的案例已集成到了 React 脚手架 create-react-boilerplate 中 ,能够轻易人人疾速当地实践。

编译环境搭建

我们运用 Emscripten 将 C 代码编译为 wasm 花样,官方引荐的体式格局是起首下载 Portable Emscripten SDK for Linux and OS X (emsdk-portable.tar.gz) 然后应用 emsdk 举行装置:

$ ./emsdk update
$ ./emsdk install latest
# 假如涌现异常运用 ./emsdk install sdk-1.37.12-64bit
# https://github.com/kripken/emscripten/issues/5272

装置终了后激活相应环境即能够举行编译:

$ ./emsdk activate latest
$ source ./emsdk_env.sh  # you can add this line to your .bashrc

笔者在当地实行上述搭建步骤时一向失利,因而改用了 Docker 预先设置好的镜像举行处置惩罚:

# 拉取 Docker 镜像
docker pull 42ua/emsdk

# 实行编译操纵
docker run --rm -v $(pwd):/home/src 42ua/emsdk emcc hello_world.c

对应的 Dockfile 以下所示,我们能够自行修改以顺应将来的编译环境:

FROM ubuntu

RUN \
    apt-get update && apt-get install -y build-essential \
    cmake python2.7 python nodejs-legacy default-jre git-core curl && \
    apt-get clean && \
\
    cd ~/ && \
    curl -sL https://s3.amazonaws.com/mozilla-games/emscripten/releases/emsdk-portable.tar.gz | tar xz && \
    cd emsdk-portable/ && \
    ./emsdk update && \
    ./emsdk install -j1 latest && \
    ./emsdk activate latest && \
\
    rm -rf ~/emsdk-portable/clang/tag-*/src && \
    find . -name "*.o" -exec rm {} \; && \
    find . -name "*.a" -exec rm {} \; && \
    find . -name "*.tmp" -exec rm {} \; && \
    find . -type d -name ".git" -prune -exec rm -rf {} \; && \
\
    apt-get -y --purge remove curl git-core cmake && \
    apt-get -y autoremove && apt-get clean

# http://docs.docker.com/engine/reference/run/#workdir
WORKDIR /home/src

到这里基础环境已设置终了,我们能够对简朴的 counter.c 举行编译,源文件以下:

int counter = 100;

int count() {  
    counter += 1;
    return counter;
}

编译敕令以下所示,假如当地装置好了 emcc 则能够直接运用,不然运用 Docker 环境举行编译:

$ docker run --rm -v $(pwd):/home/src 42ua/emsdk emcc counter.c -s WASM=1 -s SIDE_MODULE=1 -o counter.wasm
$ emcc counter.c -s WASM=1 -s SIDE_MODULE=1 -o counter.wasm

# 假如涌现以下毛病,则是由以下参数
# WebAssembly Link Error: import object field 'DYNAMICTOP_PTR' is not a Number
emcc counter.c -O1 -s WASM=1 -s SIDE_MODULE=1 -o counter.wasm 

如许我们就得到了 WebAssembly 代码:
《WebAssembly 初体验:从零开始重构盘算模块》

与 JavaScript 集成运用

自力的 .wasm 文件并不能直接运用,我们须要在客户端中运用 JavaScript 代码将其加载进来。最质朴的加载 WebAssembly 的体式格局就是运用 fetch 抓取然后编译,全部历程能够封装为以下函数:

    // 推断是不是支撑 WebAssembly
    if (!('WebAssembly' in window)) {
      alert('当前浏览器不支撑 WebAssembly!');
    }
    // Loads a WebAssembly dynamic library, returns a promise.
    // imports is an optional imports object
    function loadWebAssembly(filename, imports) {
      // Fetch the file and compile it
      return fetch(filename)
        .then(response => response.arrayBuffer())
        .then(buffer => WebAssembly.compile(buffer))
        .then(module => {
          // Create the imports for the module, including the
          // standard dynamic library imports
          imports = imports || {};
          imports.env = imports.env || {};
          imports.env.memoryBase = imports.env.memoryBase || 0;
          imports.env.tableBase = imports.env.tableBase || 0;
          if (!imports.env.memory) {
            imports.env.memory = new WebAssembly.Memory({ initial: 256 });
          }
          if (!imports.env.table) {
            imports.env.table = new WebAssembly.Table({ initial: 0, element: 'anyfunc' });
          }
          // Create the instance.
          return new WebAssembly.Instance(module, imports);
        });
    }

我们能够运用上述东西函数加载 wasm 文件:

    loadWebAssembly('counter.wasm')
      .then(instance => {
        var exports = instance.exports; // the exports of that instance
        var count = exports. _count; // the "_count" function (note "_" prefix)
        // 下面即能够挪用 count 函数
      }
    );

而在笔者的脚手架中,运用了 wasm-loader 举行加载,如许能够将 wasm 直接打包在 Bundle 中,然后经由过程 import 导入:

import React, { PureComponent } from "react";

import CounterWASM from "./counter.wasm";
import Button from "antd/es/button/button";

import "./Counter.scss";

/**
 * Description 简朴计数器示例
 */
export default class Counter extends PureComponent {
  state = {
    count: 0
  };

  componentDidMount() {
    this.counter = new CounterWASM({
      env: {
        memoryBase: 0,
        tableBase: 0,
        memory: new window.WebAssembly.Memory({ initial: 256 }),
        table: new window.WebAssembly.Table({ initial: 0, element: "anyfunc" })
      }
    });
    this.setState({
      count: this.counter.exports._count()
    });
  }

  /**
   * Description 默许衬着函数
   */
  render() {
    const isWASMSupport = "WebAssembly" in window;

    if (!isWASMSupport) {
      return (
        <div>
          浏览器不支撑 WASM
        </div>
      );
    }

    return (
      <div className="Counter__container">
        <span>
          简朴计数器示例:
        </span>
        <span>{this.state.count}</span>
        <Button
          type="primary"
          onClick={() => {
            this.setState({
              count: this.counter.exports._count()
            });
          }}
        >
          点击自增
        </Button>
      </div>
    );
  }
}

在运用 wasm-loader 时,其会挪用 new WebAssembly.Instance(module, importObject);

  • moduleWebAssembly.Module 实例。

  • importObject 即默许的由 wasm-loader 供应的对象。

简朴游戏引擎重构

上文我们议论了应用 WebAssembly 重构简朴的计数器模块,这里我们以简朴的游戏为例,交互式的感觉 WebAssembly 带来的机能提拔,能够直接检察游戏的在线演示。这里的游戏引擎等于实行部份盘算与从新赋值操纵,比如这里的盘算下一个位置状况的函数在 C 中实现为:

EMSCRIPTEN_KEEPALIVE
void computeNextState()
{
  loopCurrentState();

  int neighbors = 0;
  int i_m1, i_p1, i_;
  int j_m1, j_p1;
  int height_limit = height - 1;
  int width_limit = width - 1;
  for (int i = 1; i < height_limit; i++)
  {
    i_m1 = (i - 1) * width;
    i_p1 = (i + 1) * width;
    i_ = i * width;
    for (int j = 1; j < width_limit; j++)
    {
      j_m1 = j - 1;
      j_p1 = j + 1;
      neighbors = current[i_m1 + j_m1];
      neighbors += current[i_m1 + j];
      neighbors += current[i_m1 + j_p1];
      neighbors += current[i_ + j_m1];
      neighbors += current[i_ + j_p1];
      neighbors += current[i_p1 + j_m1];
      neighbors += current[i_p1 + j];
      neighbors += current[i_p1 + j_p1];
      if (neighbors == 3)
      {
        next[i_ + j] = 1;
      }
      else if (neighbors == 2)
      {
        next[i_ + j] = current[i_ + j];
      }
      else
      {
        next[i_ + j] = 0;
      }
    }
  }
  memcpy(current, next, width * height);
}

而对应的 JS 版本引擎的实现为:

computeNextState() {
  let neighbors, iM1, iP1, i_, jM1, jP1;

  this.loopCurrentState();

  for (let i = 1; i < this._height - 1; i++) {
    iM1 = (i - 1) * this._width;
    iP1 = (i + 1) * this._width;
    i_ = i * this._width;
    for (let j = 1; j < this._width - 1; j++) {
      jM1 = j - 1;
      jP1 = j + 1;
      neighbors = this._current[iM1 + jM1];
      neighbors += this._current[iM1 + j];
      neighbors += this._current[iM1 + jP1];
      neighbors += this._current[i_ + jM1];
      neighbors += this._current[i_ + jP1];
      neighbors += this._current[iP1 + jM1];
      neighbors += this._current[iP1 + j];
      neighbors += this._current[iP1 + jP1];
      if (neighbors === 3) {
        this._next[i_ + j] = 1;
      } else if (neighbors === 2) {
        this._next[i_ + j] = this._current[i_ + j];
      } else {
        this._next[i_ + j] = 0;
      }
    }
  }
  this._current.set(this._next);
}

本部份的编译依旧是直接将 [engine.c]() 编译为 engine.wasm,不过在导入的时刻我们须要动态地向 wasm 中注入外部函数:

    this.module = new EngineWASM({
      env: {
        memoryBase: 0,
        tableBase: 0,
        memory: new window.WebAssembly.Memory({ initial: 1024 }),
        table: new window.WebAssembly.Table({ initial: 0, element: "anyfunc" }),
        _malloc: size => {
          let buffer = new ArrayBuffer(size);
          return new Uint8Array(buffer);
        },
        _memcpy: (source, target, size) => {
          let sourceEnd = source.byteLength;

          let i, j;

          for (
            (i = 0), (j = 0), (k = new Uint8Array(target)), (l = new Uint8Array(
              source
            ));
            i < sourceEnd;
            ++i, ++j
          )
            k[j] = l[i];
        }
      }
    });

到这里文本告一段落,笔者末了须要声明的是因为这只是顺手做的试验,末了的代码包含关于内存的操纵能够存在潜伏题目,请读者批评指正。

    原文作者:王下邀月熊_Chevalier
    原文地址: https://segmentfault.com/a/1190000009792801
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞