undefined

bokuweb.me

wasm-bindgenを使ってRustのモジュールをnode_modulesに持ってくる

この記事はWebAssembly Advent Calendar 2018の21日目です。wasm-bindgenを使用して何かしてみたいと思っていたので、今回は以前Rustで実装した画像の差分を取るツールをwasm-bindgenを使用してnode_modulesとして使用できるようにしてみたいと思います。

adventar.org

移植元

github.com

これはもともと、go-diff-image(https://github.com/murooka/go-diff-image)というgolang製のツールをRustへポーティングしたものになります。

github.com

同じピクセル同士を比較して差分を出力するのではなく、githubのdiffのような感じで画像の差分を可視化するツールです。 以下のような比較画像を生成します。

f:id:bokuweb:20181221223051p:plain

成果物

github.com

手順

さっそくミニマムなプロジェクトを作ってみます。

  • cargo.toml
[package]
name = "node-lcs-img-diff"
version = "0.1.0"
authors = ["bokuweb"]
edition = "2018"

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

[dependencies]
wasm-bindgen = "0.2"

wasm-bindgen-cliが入ってない場合はインストールします。

rustup target add wasm32-unknown-unknown --toolchain nightly
cargo +nightly install wasm-bindgen-cli

まずは1を加算する関数で試してみます。

  • src/lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn add_one(n: usize) -> usize {
    n + 1
}

次にMakefileを用意しておきます。 wasm-bindgenはデフォルトブラウザ向けのコードを吐きますが、今回はnodejs向けに--nodejsつけて実行するようにします。

build:
    cargo +nightly build --target wasm32-unknown-unknown --release
    mkdir -p dist
    wasm-bindgen ./target/wasm32-unknown-unknown/release/node_lcs_img_diff.wasm --out-dir ./dist --nodejs

以下でビルド。

$ make build
  • node_lcs_img_diff_bg.d.ts
  • node_lcs_img_diff_bg.js
  • node_lcs_img_diff_bg.wasm
  • node_lcs_img_diff.d.ts
  • node_lcs_img_diff.js

が吐かれる

  • node_lcs_img_diff_bg.d.ts
/* tslint:disable */
export const memory: WebAssembly.Memory;
export function add_one(a: number): number;

内部で使用される定義

  • nodde-lcs_img_diff.d.ts
/* tslint:disable */
export function add_one(arg0: number): number;

公開関数の定義

  • node_lcs_img_diff_bg.js
const path = require('path').join(__dirname, 'node_lcs_img_diff_bg.wasm');
const bytes = require('fs').readFileSync(path);
let imports = {};

const wasmModule = new WebAssembly.Module(bytes);
const wasmInstance = new WebAssembly.Instance(wasmModule, imports);
module.exports = wasmInstance.exports;

wasmの読み込みからinstanciateまで。

  • node_lcs_img_diff.js
/* tslint:disable */
var wasm;

/**
* @param {number} arg0
* @returns {number}
*/
module.exports.add_one = function(arg0) {
    return wasm.add_one(arg0);
};

wasm = require('./node_lcs_img_diff_bg');

使用方法は以下のように呼ぶだけ。

  • index.ts
import { add_one } from './dist/node_lcs_img_diff';

add_one(1); // -> 2

良さそうです。 後はせっせと移植していきます。 注意点としてはVecは返り値として返せないので、そのような場合JSONにしStringを返すことになりそうです。

github.com

細かい部分は省略しますが、Rust側は以下のようになりました。去年以下の記事を書きましたがwasm-bindgenのおかげで受け取る値も返す値もシンプルになっています。以前はArrayBufferのオフセットやデータの長さを受け取り、自分でバッファに変換する必要がありましたが、そのあたりの処理をwasm-bindgenが受け持ってくれているからですね。

qiita.com

どういうことをやっているかざっくり言うと、画層データを2枚受け取ってデコード。差分が発生した領域を計算して、元画像に緑/赤色をブレンドしたあとpngにエンコードして返しています。細かい処理は省略していますが、mainとなるdiff関数は以下のような感じです。

  • lib.rs
#[wasm_bindgen]
pub fn diff(before: &[u8], after: &[u8]) -> String {
    let mut before = load_from_memory(before).expect("Unable to load image from memory");
    let mut after = load_from_memory(after).expect("Unable to load image from memory");
    let encoded_before = create_encoded_rows(&before.raw_pixels(), before.dimensions().0 as usize);
    let encoded_after = create_encoded_rows(&after.raw_pixels(), after.dimensions().0 as usize);
    let result = lcs_diff::diff(&encoded_before, &encoded_after);
    let mut added: Vec<usize> = Vec::new();
    let mut removed: Vec<usize> = Vec::new();
    for d in result.iter() {
        match d {
            &lcs_diff::DiffResult::Added(ref a) => added.push(a.new_index.unwrap()),
            &lcs_diff::DiffResult::Removed(ref r) => removed.push(r.old_index.unwrap()),
            _ => (),
        }
    }
    create_marked_image(&mut after, (99, 195, 99), RATE, &added);
    create_marked_image(&mut before, (255, 119, 119), RATE, &removed);
    serde_json::to_string(&Result {
        after: to_png(&after),
        before: to_png(&before)
    }).unwrap()
}

typescriptの型まで吐いてくれるので以下のように使用できます。

  • index.ts
import { diff } from './dist/node_lcs_img_diff';

const [before, after] = await Promise.all([readFile("YOUR_IMAGE"), readFile("YOUR_IMAGE")]);
JSON.parse(diff(before, after));

実際にはcliや画像の読み書き処理を追加しています。詳しくは以下を参照してみてください。

github.com

あとはbuildしてnpn publishすれば完了です。

速度

自分はよくJavaScriptとwasmの速度比較を行うのですが、今回はJavaScript実装がないのでRust版と速度比較をしてお茶を濁しときます。

  • wasm(node v10.11.0)
Benchmark #1: node . test/images/before.png test/images/after.png --dist test/expected
  Time (mean ± σ):     720.2 ms ±  57.1 ms    [User: 1.150 s, System: 0.145 s]
  Range (min … max):   687.9 ms … 821.9 ms    5 runs
  • Rust 1.31
Benchmark #1: lcs-image-diff test/images/before.png test/images/after.png aaa.png
  Time (mean ± σ):      29.3 ms ±   0.8 ms    [User: 26.2 ms, System: 5.0 ms]
  Range (min … max):    28.2 ms …  32.4 ms    89 runs

ベンチマークにはhyperfineを使用しました。(MacBook Air (11-inch, Early 2015), 1.6 GHz Intel Core i5, 8 GB 1600 MHz DDR3)です。 結構な差がでましたね。どうもwasmの方はwasmファイルのリードからinstansiateまでで200msくらい持ってかれてるようです。diff関数は185msくらいですね。