大きなデータはJavaScriptからRust/WebAssemblyのメモリに直接アクセス
wasm_bindgenによるデータ交換では、一方のデータがコピーされて他方に渡されます。文字列や数値をいくつか交換する程度ならばこれでも問題ありませんが、画像データのような大きなデータの場合、データコピーにより処理速度が落ちます。このような場合、WebAssemblyのメモリにJavaScriptから直接アクセスすると、より高速に処理できます。
Rust/WebAssemblyは、内部的に1次元のメモリ空間を持っています。Rustで宣言したベクタ(Vec)は、実態としてはメモリ空間内のあるアドレスを指すポインタです。JavaScriptでは、Rust/WebAssemblyのメモリ空間をWebAssembly.Memoryオブジェクトとして参照し、ポインタをそのメモリ空間の先頭からのオフセットとして受け取ることで、Rust/WebAssemblyのメモリ空間を直接操作できます(図4)。
このような例を図5のサンプルで説明します。選択した画像ファイルをCanvasに表示し、そのメモリ内容をRust/WebAssemblyに渡して、画像をモノクロに変換します。なお、このサンプルでは、読み込む画像の縦横比が4:3であると仮定して処理します。
まず、画像処理を行うImageProcessor構造体をRustでリスト9の通り記述します。
// JavaScriptにエクスポートするImageProcessor構造体 ...(1)
#[wasm_bindgen]
pub struct ImageProcesser {
width: u32,
height: u32,
data: Vec<u8>
}
// JavaScriptにエクスポートしないImageProcessor構造体のメソッド ...(2)
impl ImageProcesser {
// 行と列から、画素のインデックスを取得
fn get_index(&self, row: u32, column: u32) -> usize {
// 行 * 幅 + 列
return (row * self.width + column) as usize;
}
}
(1)で、u32(符号なし32ビット整数)でwidth(幅)とheight(高さ)を、Vec<u8>(符号なし8ビット整数のベクタ)でdata(画像データ)を保持するImageProcessor構造体を記述します。また(2)は、画素の行と列から、画素のインデックスを「行×幅+列」で計算するメソッドです(このメソッドはJavaScriptにエクスポートしません)。
JavaScriptにエクスポートするImageProcessorの処理は、リスト10の通り実装します。
#[wasm_bindgen]
impl ImageProcesser {
// 画像の幅を返却 ...(1)
pub fn width(&self) -> u32 {
return self.width;
}
// 画像の高さを返却 ...(2)
pub fn height(&self) -> u32 {
return self.height;
}
// 画像のメモリ領域へのポインタを返却 ...(3)
pub fn data(&self) -> *const u8 {
return self.data.as_ptr();
}
// 構造体を生成 ...(4)
pub fn new(width: u32, height: u32) -> ImageProcesser {
// 画像のメモリを割り当て
let data = vec![0; (width * height * 4) as usize];
// ImageProcesserを生成
return ImageProcesser {
width,
height,
data
};
}
// 画像データを変換 ...(5)
// 画像データはself.dataにR,G,B,Aの順番で格納される
pub fn convert(&mut self) {
for row in 0..self.height {
for col in 0..self.width {
let idx = self.get_index(row, col);
// Gの値を代表として、RとBに設定 ...(6)
self.data[4 * idx] = self.data[4 * idx + 1];
self.data[4 * idx + 2] = self.data[4 * idx + 1];
}
}
}
}
(1)と(2)は画像の幅と高さを取得する処理、(3)はメモリ領域先頭のポインタを返却する処理です。ImageProcessor構造体を生成する(4)のnewメソッドでは、長さwidth * height * 4のベクタをdataに設定します。カラー画像の画素は1つあたり4バイト(赤、緑、青、アルファ各1バイト)であることに注意してください。
画像データを変換するconvertメソッドは(5)です。全画素に対して、get_indexメソッドでメモリ上の位置を求め、(6)でメモリ内容を書き換えています。各画素のデータは赤、緑、青、アルファの順番で1バイトずつ格納されるため、ここでは緑の値を赤、青にも設定することで、モノクロ画像に変換しています。convertメソッドはdataベクタを直接書き換えている(別のベクタにコピーしていない)ことに注目してください。
JavaScript側の処理では、まずファイル選択時にその内容を画像として読み込み、Canvasに描画します。この処理の詳細はサンプルコードを参照してください。一方、読み込まれた画像を変換する処理は、リスト11の通りです。
// インポート
import { ImageProcesser } from 'p004-memory-access'; //(1a)
import { memory } from 'p004-memory-access/p004_memory_access_bg'; //(1b)
(略)
document.getElementById('convert-button').addEventListener('click', () => {
// Rustで実装したImageProcesserオブジェクトを生成 ...(2)
const imageProcesser = ImageProcesser.new(imageDispWidth, imageDispHeight);
// 画像情報が格納されるメモリ領域のポインタを取得 ...(3)
const dataPtr = imageProcesser.data();
// 画像情報のメモリ領域にJavaScriptからアクセスできるようにする ...(4)
// 最後にImageDataを作りたいので、dataはUint8ClampedArrayにする
let data = new Uint8ClampedArray(
memory.buffer, dataPtr, imageDispWidth * imageDispHeight * 4);
// Webページ上のCanvas要素から画像データを取得してdataに格納 ...(5)
const canvas1 = document.getElementById('image-canvas-1');
const ctx1 = canvas1.getContext('2d');
const imageData = ctx1.getImageData(0, 0, imageDispWidth, imageDispHeight)
for (let i = 0; i < imageDispWidth * imageDispHeight * 4; i++) {
data[i] = imageData.data[i];
}
// convertメソッド実行で、WebAssembly側のメモリ領域が書き換えられる ...(6)
imageProcesser.convert();
// メモリ領域(data)から新しいImageDataを取得する ...(7)
let newImageData = new ImageData(data, imageDispWidth, imageDispHeight);
// ImageDataをCanvasに描画 ...(8)
const canvas2 = document.getElementById('image-canvas-2');
const ctx2 = canvas2.getContext('2d');
ctx2.putImageData(newImageData, 0, 0);
}, false);
(1a)でImageProcessorをインポートします。(1b)で「p004_memory_access_bg」からインポートできるmemoryは、WebAssemblyのメモリ空間を表すオブジェクトです。
(2)でImageProcessorオブジェクトを生成し、画像データのメモリ領域に対応するポインタを(3)のdataメソッドで取得します。(4)では、(1b)のmemoryのbufferプロパティと(3)で取得したポインタ、および画像データ長(幅×高さ×4)を指定してUint8ClampedArrayオブジェクトを生成します。このオブジェクトを利用すると、WebAssemblyのメモリ領域にJavaScriptからアクセスできます。なおUint8ClampedArrayは、0未満や255を超える設定値を0または255に強制的に変更して保持する符号なし8ビット配列です。
(5)でCanvasから取得した画像データをWebAssemblyのメモリ領域にコピー後、(6)のconvertメソッドでメモリ領域を書き換えます。(7)で書き換え後のメモリ領域からImageDataオブジェクトを生成し、(8)でCanvasに描画します。
まとめ
本記事では、Rust/WebAssemblyとJavaScript間のデータ交換について説明しました。wasm_bindgenキーワードで、JavaScriptとRustが互いのメソッドを呼んでデータ交換できます。WebAssembly上の大きなデータをJavaScriptから直接参照する方法も説明しました。
次回は、Rust/WebAssembly開発で利用可能なツールについて、改めて説明していきます。
