WebAssemblyをNode-REDで使う (後編)


# 前編の内容

[前編](https://qiita.com/k-toumura/private/5c8c2dfb5fd87796ef6d)では、Node-REDでWebAssemblyを使う方法として、functionノードにWebAssemblyバイナリを埋め込む方法と、独自ノード内でWebAssemblyを呼び出す方法の前準備までを説明しました。

後編では、前編で作成したWebAssemblyコンパイル済みのPNGトリミングルーチンを使って独自ノードを作っていきます。

# (承前) 第二段階: PNG画像のトリミングをする独自ノードの作成

## Node-REDノードの作成
[Node-RED Nodeの作り方のドキュメント](https://nodered.org/docs/creating-nodes/)を参考にしながら、モジュールを作っていきます。

まずnpmモジュールの雛形を作ります。

```shell
% mkdir node-red-contrib-png-crop
% cd node-red-contrib-png-crop
% npm init -y
...

%
```

次に、`package.json`を更新します。

```json:package.json

  "name": "node-red-contrib-png-crop",
  "version": "1.0.0",
  "description": "Cropping PNG image",
  "keywords": [],
  "author": "",
  "license": "ISC",
  "node-red": {
    "nodes": {
      "png-crop": "png-crop.js"
    }
  },
  "dependencies": {
  }
}
```

ランタイム側のロジックは`png-crop.js`として記述します。

```js:png-crop.js
const pngcropwasm = require('./pngcrop');

module.exports =  (RED) => {
    function PngCropNode(config) {
        RED.nodes.createNode(this,config);
        const node = this;
        node.on('input', (msg, send, done) => {
            if (Buffer.isBuffer(msg.payload.img)) {
                const x = msg.payload.x || 0;
                const y = msg.payload.y || 0;
                const width = msg.payload.width || 100;
                const height = msg.payload.height || 100;
                const cropped_img = pngcropwasm.croppng(msg.payload.img, x, y, width, height);
                msg.payload.img = cropped_img;
                send(msg);
                done();
            } else {
                done('no image');
            }
        });
    };
    RED.nodes.registerType('png-crop', PngCropNode);
}
```

冒頭の`require()`でWebAssemblyバイナリにコンパイルされたPNGトリミングライブラリをモジュールとして読み込んでいます。そして、Node-REDのメッセージハンドラで、ペイロードに含まれているイメージファイルを引数としてトリミング関数を呼び出し、結果をメッセージとして送信しています。

あとは、エディタ側の記述です。とくに設定インタフェースはないので、最低限の記述のみしてあります。

```html:png-crop.html
<script type="text/javascript">
    RED.nodes.registerType('png-crop', {
        category: 'function',
        color: '#F3B567',
        defaults: {
            name: {value: ""}
        },
        inputs: 1,
        outputs: 1,
        icon: "font-awesome/fa-crop",
        label: function() { return this.name ||"png-crop";}
    });
</script>

<script type="text/html" data-template-name="png-crop">
    <div class="form-row">
        <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
        <input type="text" id="node-input-name" placeholder="Name">
    </div>
</script>

<script type="text/html" data-help-name="png-crop">
    <p>A simple node that crops the image</p>
</script>

```

## 独自ノードのインストールとフローの作成

これをインストールして、フローを作ってみましょう。

```shell
% cd ~/.node-red/
% npm install ....../node-red-contrib-png-crop
...
% cd ...../node-red
% npm start
```

functionカテゴリに"png crop"というノードができています。
![png cropノード](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889500/c5549e8d-9569-a4fd-dbe2-b03f10e51226.png)

試しにWeb上のPNG画像をトリミングしてダッシュボードに表示するフローを作ってみます。

![Web上のファイルのトリミング](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889500/dc3ee407-0ad4-2437-3c50-32c7db42017f.png)

```json
[{"id":"924181c4.4467c","type":"tab","label":"Flow 1","disabled":false,"info":""},{"id":"fc70fd9.aa3a5","type":"inject","z":"924181c4.4467c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":100,"wires":[["aef153f1.97613"]]},{"id":"aef153f1.97613","type":"http request","z":"924181c4.4467c","name":"Ferris the crab","method":"GET","ret":"bin","paytoqs":"ignore","url":"https://rustacean.net/assets/rustacean-flat-happy.png","tls":"","persist":false,"proxy":"","authType":"","x":340,"y":100,"wires":[["b33daa13.f05df8"]]},{"id":"b33daa13.f05df8","type":"change","z":"924181c4.4467c","name":"","rules":[{"t":"move","p":"payload","pt":"msg","to":"payload.img","tot":"msg"},{"t":"set","p":"payload.x","pt":"msg","to":"480","tot":"num"},{"t":"set","p":"payload.y","pt":"msg","to":"350","tot":"str"},{"t":"set","p":"payload.height","pt":"msg","to":"300","tot":"str"},{"t":"set","p":"payload.width","pt":"msg","to":"300","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":540,"y":100,"wires":[["1f7259b8.b4e1e6"]]},{"id":"7497a53c.595bdc","type":"function","z":"924181c4.4467c","name":"Base64 encode","func":"const cropped = Buffer.from(msg.payload.img);\nmsg.payload = cropped.toString('base64');\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":340,"y":180,"wires":[["6dd7310.e04afd"]]},{"id":"6dd7310.e04afd","type":"ui_template","z":"924181c4.4467c","group":"c744f26f.11e5f","name":"","order":0,"width":0,"height":0,"format":"<div style=\"height: 300px; width: 300px\">\n<img src=\"data:image/png;base64,{{msg.payload}}\"\n alt='cropped image'\n />\n </div>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","x":520,"y":180,"wires":[[]]},{"id":"1f7259b8.b4e1e6","type":"png-crop","z":"924181c4.4467c","name":"","x":160,"y":180,"wires":[["7497a53c.595bdc"]]},{"id":"c744f26f.11e5f","type":"ui_group","z":"","name":"Default","tab":"22fdfd7c.238b92","order":1,"disp":true,"width":"6","collapse":false},{"id":"22fdfd7c.238b92","type":"ui_tab","z":"","name":"Home","icon":"dashboard","disabled":false,"hidden":false}]
```

Rustマスコットの[Ferris the Crab](https://rustacean.net/assets/rustacean-flat-happy.png)の画像を持ってきて、顔の部分を切り出して`ui_template`ダッシュボードに表示するフローになっています。

結果のダッシュボードがこちらです。
![ダッシュボード](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889500/97eb06fb-5dee-1f8e-60ee-ef70c8cea73d.png)


## 速度は?

気になるのが変換速度になります。
5K画像を100回トリミングする簡単なスクリプトを書いて、速度を調べました。

```js:pure&#x20;JS版(png-crop,&#x20;pngjs利用)
const pngcrop = require('png-crop');

for (let i=0; i<100; i++) {
    pngcrop.crop('./test.png', './testout.png',
        {width: 1000, height: 1000, top: 10, left: 10},
        (err) => { if (err) throw err; });
}
```

```js:Rust+WebAssembly+JS版
const pngcrop = require('./pngcrop');
const fs = require('fs');

for (let i = 0; i < 100; i++) {
    const input = fs.readFileSync('./test.png', {flag:'r'});
    const cropped = pngcrop.croppng(input, 10,10,1000,1000);
    fs.writeFileSync('./testout.png',cropped);
}
```

結果は下記の通りです。プログラムの作りが違うので直接は比べられませんが、WebAssemblyベースのほうが約1.4倍高速という結果になっています。もちろん、ネイティブコードで実装すればさらに高速化できますが、ここではNode.jsで完結するところに利点を見出しています。

|          | 所要時間 | pure JS比 |
|---|--:|--:|
|pure JS版 | 98秒  | 1 |
|Rust+Wasm+JS版 | 68秒 | 0.69 |

今回は`wasm-bindgen`で生成したファイルを手でコピーしてnpmモジュールに追加していますが、`wasm-pack`を使うとnpmモジュールの作成まで自動的に行えます。Node-REDのノードモジュールとして使うときには、`wasm-bindgen`の生成物をそのままコピーで十分かと思います。

# 最後に

やや無理やりな使い方ではありますが、RustとWebAssemblyの組み合わせでNode-REDノードが記述できることを示しました。

WebAssemblyによって、多様な言語で記述したプログラムがブラウザ上やNode.js上で安全に実行できるようになりました。また、WebAssembly System Interface(WASI)によってWebAssemblyはサンドボックスを備えた軽量な実行環境としても使われるようになってきています。今後は、エッジコンピューティングなどより広い応用範囲で使われる技術になっていくと期待しています。

# 参考文献
- [Rust and WebAssembly](https://rustwasm.github.io/book/)
- [The `wasm-bindgen` Guide](https://rustwasm.github.io/docs/wasm-bindgen/)
- [Programming WebAssembly with Rust](https://pragprog.com/titles/khrust/programming-webassembly-with-rust/)
- [WebAssembly](https://developer.mozilla.org/ja/docs/WebAssembly)
- [Hello wasm-pack!](https://rustwasm.github.io/docs/wasm-pack/)