初次尝试WebAssembly X Rust

Posted by Yinode on Wednesday, January 9, 2019

TOC

这个东西其实我大约一年之前听到了解到,但是一直没有尝试。

正巧最近自己写了个文本校对工具,公司内部的一些人员在用,功能上没有问题,主要就是因为需求是全部一行来进行对比,然后复杂度又高达 O(nm),在我们公司的使用情况下,n 基本等于 m 所以复杂度也就是 O(n^2)。简单来说就是一旦 5000 字以上就爆炸(内存直接溢出,超出 v8 引擎大约 1.5G 的限制),虽然这种高字数的对比并不多见,但身为一个有责任感的码农,很想找办法解决这个问题,所以我重新考虑了这个东西,想要用更高性能的语言来进行计算,也进行了一番尝试。

在一众语言中选择了 Rust,因为据说对于WebAssembly的支持是最健全的,正好我也玩过一点点的 Rust。

提前告知一下结果,可能会让大家失望。我做了一个 demo(模拟 LCS 需要的那个表结构)之后,发现比 JS 还慢,并且内存占用上也并没有特别大的优势。所以我想告诉大家,高性能不是那么好拿的,V8 引擎本身已经很快了,算法上一些复杂度降低不了,语言的那点性能差异可能完全不够看。

当然 这只是我个人的使用场景。

起步

第一部当然是安装 Rust 的整个编译环境

curl https://sh.rustup.rs -sSf | sh // 安装 Rust rustup target add wasm32-unknown-unknown –toolchain nightly // 安装 To wasm 的组件 cargo +nightly install wasm-bindgen-cli

搭建

我这里会用到wasm-bindgen,他的作用是方便 JS 与 Rust 进行函数调用上的变量交互,否则你获取 Rust 运行的结果还需要读取内存(arrayBuffer) 然后进行编码。特别麻烦,就像下面这样

function getStr(rust, fnKey, lenKey, args) {
  const memory = rust.instance.exports.memory
  const offset = rust.instance.exports[fnKey](args)
  const stringBuffer = new Uint8Array(
    memory.buffer,
    offset,
    rust.instance.exports[lenKey]()
  )
  let str = ''
  for (let i = 0; i < stringBuffer.length; i++) {
    str += String.fromCharCode(stringBuffer[i])
  }
  return str
}

async function main() {
  let res = await fetch(
    './target/wasm32-unknown-unknown/debug/rust_diff_core.wasm'
  )
  const bytes = await res.arrayBuffer()
  const rust = await WebAssembly.instantiate(bytes)
}

main()

让我们正式开始

cargo +nightly new js-hello-world –lib 建立一个 project

打开Cargo.toml 修改如下

[package]
name = "hello_world"
version = "0.1.0"
authors = ["The wasm-bindgen Developers"]
edition = "2018"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2.30"

修改src/lib.rs

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
    fn alert(s: &str);
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
    fn log_str(s: String);
}


#[wasm_bindgen]
pub fn say(woo: &str) -> String {
    woo.to_string()
}

接下来我们需要一个 build.sh来帮助我们自动进行从 Rust > wasm > wasm-bindgen 包装 的步骤

cargo +nightly build --target wasm32-unknown-unknown &&
wasm-bindgen ./target/wasm32-unknown-unknown/debug/hello_world.wasm  --out-dir .

现在 运行一下 build.sh 我们会发现根目录出现了 hello_world.wasm 以及 hello_world.js。没错,wasm-bindgen 的原理就是建立.js 的包装,方便我们进行通信

创建index.js

const rust = import('./hello_world')

rust.then(m => console.log(m.say('hello'))).catch(console.error)

创建package.json

{
  "scripts": {
    "build": "webpack",
    "serve": "webpack-dev-server"
  },
  "devDependencies": {
    "text-encoding": "^0.7.0",
    "html-webpack-plugin": "^3.2.0",
    "webpack": "^4.11.1",
    "webpack-cli": "^3.1.1",
    "webpack-dev-server": "^3.1.0"
  }
}

以及 webpack 的设置 建立 webpack.config.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpack = require('webpack')

module.exports = {
  entry: './index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'index.js'
  },
  plugins: [
    new HtmlWebpackPlugin(),
    new webpack.ProvidePlugin({
      TextDecoder: ['text-encoding', 'TextDecoder'],
      TextEncoder: ['text-encoding', 'TextEncoder']
    })
  ],
  mode: 'development'
}

安装依赖 开始运行

npm i npm run server

你的控制台应该已经成功输出了一个 hello,实际上这就是最简单的变量交互。你现在可以做更多的事情了。如果修改了 Lib.rs 请重新运行build.sh

总结

Rust真的有点难。