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


はじめに

Node-REDの独自ノードを作るには、一般にはJavaScriptとHTMLを使います。高速化のためにNode.jsのnative addonを使ってランタイム側にC++などで書いたコードをつかうこともできますが、環境に応じて再コンパイルする必要がでてきます。

そこで、WebAssemblyを使ってみることを考えてみます。WebAssemblyは

  • JavaScriptより高速な処理が期待できる
  • Node.js(8以上)が動けばどこでも動く
  • C,C++,Rustなどの言語のコンパイル対象になっている

という特長があります。

ここでは、Rustで書いたコードからWebAssemblyのバイナリにコンパイルし、それをNode-REDから活用する方法を説明します。

環境整備

ここでは下記の環境を使いました。

  • macOS Catalina version 10.15.7
  • Node-RED v1.2.6
  • Node.js v14.15.1
  • Rustc 1.48.0 (target=wasm32-unknown-unknown)
  • wasm-bindgen 0.2.69

各ツールのインストール方法はそれぞれのツールのWebページの説明をご覧ください。

第一段階: Node-REDから呼び出せることを確認する

最初から独自ノードを作っていくのも大変なので、まずは動くことを確認するためにfunctionノードをつかって無理やり使ってみましょう。

Rustのライブラリを作る

Cargoコマンドで、プロジェクトの雛形を作りましょう。この時点ではとくにWebAssemblyを意識することはありません。

% cargo new --lib hellowasm
     Created library `hellowasm` package
%

C形式の動的ライブラリを生成してWebAssemblyにコンパイルできるようにするためにCargo.tomlを編集します。最後の2行は、最適化のための設定です。

Cargo.toml
[package]
name = "hellowasm"
version = "0.1.0"
edition = "2018"

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

[profile.release]
lto = true

プログラム本体は、src/lib.rsに書きます。ここでは単純な浮動小数点の掛け算をします。なお、WebAssembly自体には32,64bitの整数、浮動小数点しか型がありません。

src/lib.rs
#[no_mangle]
pub extern "C" fn mul_f64_f64(x: f64, y: f64) -> f64 {
    x * y
}

WebAssemblyにコンパイルする

それでは、これをWebAssemblyにコンパイルしましょう。

% cargo build --release --target wasm32-unknown-unknown
   Compiling hellowasm v0.1.0 (.../hellowasm)
    Finished release [optimized] target(s) in 0.70s

WebAssemblyのバイナリがtarget/wasm32-unknown-unknown/release/hellowasm.wasmに生成されています。

% ls -l target/wasm32-unknown-unknown/release/hellowasm.wasm
-rwxr-xr-x  2 ktoumura  staff  243 Dec  3 16:03 target/wasm32-unknown-unknown/release/hellowasm.wasm

243byteと小さいですね。なお、最適化の設定(link time optimization)を行わないと1513648byteのファイルになります。

functionノードでwasmをロードする

今回は小さいファイルですので、Base64エンコードをして直接functionノードから読み込ませてしまいましょう。

% base64 target/wasm32-unknown-unknown/release/hellowasm.wasm
AGFzbQEAAAABBwFgAnx8AXwDAgEABAUBcAEBAQUDAQAQBhkDfwFBgIDAAAt/AEGAgMAAC38AQYCAwAALBzMEBm1lbW9yeQIAC211bF9mNjRfZjY0AAAKX19kYXRhX2VuZAMBC19faGVhcF9iYXNlAwIKCQEHACAAIAGiCwAPDi5kZWJ1Z19hcmFuZ2VzABUEbmFtZQEOAQALbXVsX2Y2NF9mNjQATQlwcm9kdWNlcnMCCGxhbmd1YWdlAQRSdXN0AAxwcm9jZXNzZWQtYnkBBXJ1c3RjHTEuNDguMCAoN2VhYzg4YWJiIDIwMjAtMTEtMTYp
%

これを使って、functionノードのsetupタブでWebAssemblyのロードを行います。Setup時にロードすることで、メッセージを受けるたびにWebAssemblyのロードが行われるコストを削減します。

const wasmcode = "AGFzbQ...(略)...";
WebAssembly.instantiate(Buffer.from(wasmcode, 'base64'),{})
    .then(result => {
        if (context.get("mul_f64_f64") === undefined) {
            context.set("mul_f64_f64", result.instance.exports.mul_f64_f64)
        }
    });

このような形で関数定義を取り込み、ノードのコンテキストにセットしておきます。本体のコードはFunctionタブにセットしましょう。

const mul_f64_f64 = context.get("mul_f64_f64");
msg.payload = mul_f64_f64(msg.payload, 2.5);
return msg;

それではデプロイして実行してみましょう。

$12345.6 \times 2.5=30864$が計算できてますね。

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

さて、もう一歩実用的な使い方に進みましょう。次は、独自ノードの中でWebAssemblyを使うことにします。ここでは「PNG画像をうけとって、それをトリミングした画像を出力する」ノードを作ります。

前節の説明ではRust-WebAssembly-JavaScriptの間のつなぎを全て手作業で実施しました。このままですと文字列を渡すのも一苦労です。このような作業を補助してくれるツールとしてwasm-bindgenがあります。

RustでPNGのトリミングを記述

前節と同様に、Rustのプロジェクトを作りましょう。

% cargo new --lib pngcrop
     Created library `pngcrop` package
%

今回はwasm-bindgenを使うためにCargo.tomlを下記のように変更します。

Cargo.toml
[package]
name = "pngcrop"
version = "0.1.0"
edition = "2018"

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

[dependencies]
image = "0.23.12"
wasm-bindgen = "0.2"

[profile.release]
lto = true

dependenciesに画像を扱うためのimageクレートと、先ほど述べたwasm-bindgenを加えています。

RustでのPNG画像のトリミングのコード(src/lib.rs)は下記になります。まだRustに慣れていないので不自然なコードになっているかもしれません...

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

#[wasm_bindgen]
pub fn croppng(buf: &[u8], x: u32, y: u32, width: u32, height: u32) -> Vec<u8> {
    let mut img = load_from_memory(buf).unwrap().to_rgb8();
    let cropped = imageops::crop(&mut img, x, y, width, height).to_image().into_raw();
    let mut data = Vec::new();
    let encoder = codecs::png::PngEncoder::new(&mut data);
    encoder.encode(&cropped, width, height, ColorType::Rgb8).unwrap();
    data
}

croppng()関数は、PNG形式のバイナリのスライスとトリミングのパラメータを受け取って、トリンミング済みのPNG形式のバイナリを返します。#[wasm_bindgen]というアトリビュートをつけることで、後に置かれた関数がバインディング生成の対象であることを示します。

このライブラリをビルドし、wasm-bindgenコマンドでNode.js用のバインディングを生成します。

% cargo build --release --target wasm32-unknown-unknown
   Compiling autocfg v1.0.1
...
   Compiling wasm-bindgen-macro v0.2.69
   Compiling image v0.23.12
   Compiling pngcrop v0.1.0 (.../pngcrop)
    Finished release [optimized] target(s) in 44.06s
% wasm-bindgen target/wasm32-unknown-unknown/release/pngcrop.wasm --target nodejs --out-dir . --no-typescript

% ls -l

total 1640
-rw-r--r--  1 ktoumura  staff   11555 Dec  3 17:57 Cargo.lock
-rw-r--r--  1 ktoumura  staff     178 Dec  3 18:10 Cargo.toml
-rw-r--r--  1 ktoumura  staff    1945 Dec  3 19:07 pngcrop.js
-rw-r--r--  1 ktoumura  staff  815413 Dec  3 19:07 pngcrop_bg.wasm
drwxr-xr-x  3 ktoumura  staff      96 Dec  3 16:52 src
drwxr-xr-x  6 ktoumura  staff     192 Dec  3 18:55 target
%

pngcrop_bg.wasmがコンパイルされたWebAssemblyバイナリ、pngcrop.jsがWebAssemblyへのインタフェース用のJavascriptプログラムになります。Javascriptのプログラム側からは、pngcrop.jsrequire()して使います。

後編では、この2つのファイルを使って、Node-REDの独自ノードに仕立てていきます。