SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

Rust言語で作るWebバイナリファイル「WebAssembly」入門

Rust/WebAssemblyとJavaScriptのデータ交換を使いこなす!

Rust言語で作るWebバイナリファイル「WebAssembly」入門 第4回

  • X ポスト
  • このエントリーをはてなブックマークに追加

大きなデータはJavaScriptからRust/WebAssemblyのメモリに直接アクセス

 wasm_bindgenによるデータ交換では、一方のデータがコピーされて他方に渡されます。文字列や数値をいくつか交換する程度ならばこれでも問題ありませんが、画像データのような大きなデータの場合、データコピーにより処理速度が落ちます。このような場合、WebAssemblyのメモリにJavaScriptから直接アクセスすると、より高速に処理できます。

 Rust/WebAssemblyは、内部的に1次元のメモリ空間を持っています。Rustで宣言したベクタ(Vec)は、実態としてはメモリ空間内のあるアドレスを指すポインタです。JavaScriptでは、Rust/WebAssemblyのメモリ空間をWebAssembly.Memoryオブジェクトとして参照し、ポインタをそのメモリ空間の先頭からのオフセットとして受け取ることで、Rust/WebAssemblyのメモリ空間を直接操作できます(図4)。

図4 Rust/WebAssemblyのメモリの概念図
図4 Rust/WebAssemblyのメモリの概念図

 このような例を図5のサンプルで説明します。選択した画像ファイルをCanvasに表示し、そのメモリ内容をRust/WebAssemblyに渡して、画像をモノクロに変換します。なお、このサンプルでは、読み込む画像の縦横比が4:3であると仮定して処理します。

図5 Rust/WebAssemblyで画像をモノクロ化するサンプル(p004-memory-access)
図5 Rust/WebAssemblyで画像をモノクロ化するサンプル(p004-memory-access)

 まず、画像処理を行うImageProcessor構造体をRustでリスト9の通り記述します。

[リスト9]ImageProcesser構造体の定義1(p004-memory-access/src/lib.rs)
// 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の通り実装します。

[リスト10]ImageProcesser構造体の定義2(p004-memory-access/src/lib.rs)
#[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の通りです。

[リスト11]ImageProcesserで画像を変換する処理(p004-memory-access/www/index.js)
// インポート
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開発で利用可能なツールについて、改めて説明していきます。

参考資料

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
Rust言語で作るWebバイナリファイル「WebAssembly」入門連載記事一覧

もっと読む

この記事の著者

WINGSプロジェクト  吉川 英一(ヨシカワ エイイチ)

WINGSプロジェクトについて>有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表 山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手がける。2018年11月時点での登録メンバは55名で、現在も執筆メンバを募集中。興味のある方は、どしどし応募頂きたい。著書記事多数。 RSS X: @WingsPro_info(公式)、@WingsPro_info/wings(メンバーリスト) Facebook

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

山田 祥寛(ヤマダ ヨシヒロ)

静岡県榛原町生まれ。一橋大学経済学部卒業後、NECにてシステム企画業務に携わるが、2003年4月に念願かなってフリーライターに転身。Microsoft MVP for Visual Studio and Development Technologies。執筆コミュニティ「WINGSプロジェクト」代表。主な著書に「独習シリーズ(Java・C#・Python・PHP・Ruby・JSP&サーブレットなど)」「速習シリーズ(ASP.NET Core・Vue.js・React・TypeScript・ECMAScript、Laravelなど)」「改訂3版JavaScript本格入門」「これからはじめるReact実践入門」「はじめてのAndroidアプリ開発 Kotlin編 」他、著書多数

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/16904 2022/12/12 11:00

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング