Open2

Hello WASM! 🦀

tomkeitomkei

Rustで WebAssembly

Setup

# rust周辺ツールのアップデート
rustup update

# wasm packのインストール
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

# cargo generate のインストール
cargo install cargo-generate

# npmのインストール
npm install npm@latest -g

cargo generateでテンプレートを取得

cargo generate --git https://github.com/rustwasm/wasm-pack-template

Project Name を聞かれるので、sampleなどとしておく。

ビルド

cd sample
wasm-pack build

webページ化

npm init wasm-app www

webページとして実行

cd www
npm install  # 1度だけ
npm run start
tomkeitomkei

マンデルブロ集合を作る

参考(一部改変)

src/logic.rsを作成し編集

src/logic.rs
fn get_n_diverged(x0: f64, y0: f64, max_iter:usize)-> u8 {
  let mut xn = 0.0;
  let mut yn = 0.0;

  for i in 1..max_iter {
    let x_next = xn * xn - yn * yn + x0;
    let y_next = 2.0 * xn * yn + y0;
    xn = x_next;
    yn  = y_next;
    if yn * yn + xn * xn > 4.0 {
      return i as u8;
    }
  }
  max_iter as u8
}

pub fn generate_mandelbrot_set (
  canvas_w: usize,
  canvas_h: usize,
  x_min: f64,
  x_max: f64,
  y_min: f64,
  y_max: f64,
  max_iter: usize,
) -> Vec<u8> {
  let canvas_w_f64 = canvas_w as f64;
  let canvas_h_f64 = canvas_h as f64;
  // 色情報
  let mut data = vec![];
  for i in 0..canvas_h {
    let i_f64 = i as f64;
    let y = y_min + (y_max - y_min) * i_f64 / canvas_h_f64;
    for j in 0..canvas_w {
      let x = x_min + (x_max - x_min) * j as f64 / canvas_w_f64;
      let iter_index = get_n_diverged(x, y, max_iter);
      let v = iter_index % 8 * 32;
      data.push(v);
      data.push(v);
      data.push(v);
      data.push(255);
    }
  }
  data
}

#[cfg(test)]
mod tests {
  use super::*;
  #[test]
  fn test_get_n_diverged(){
    let max_iter = 10;
    assert_eq!(get_n_diverged(1.0, 0.0, max_iter), 3);
    assert_eq!(get_n_diverged(0.0, 0.0, max_iter), max_iter as u8);
    assert_eq!(get_n_diverged(0.0, 1.0, max_iter), max_iter as u8);
  }

  #[test]
  fn test_generate_mandelbrot_set(){
    let canvas_w = 2;
    let canvas_h = 2;
    let x_min = -1.0;
    let x_max = 1.0;
    let y_min = -1.0;
    let y_max = 1.0;
    let max_iter = 8;
    assert_eq!(
      generate_mandelbrot_set(canvas_w,canvas_h,x_min,x_max,y_min,y_max,max_iter),
      vec![96,96,96,255,0,0,0,255,0,0,0,255,0,0,0,255]
    );
  }
}

src/lib.rsを編集

src/lib.rs
mod utils;
mod logic;

use wasm_bindgen::prelude::*;

use wasm_bindgen::{Clamped, JsCast}; 
use web_sys::ImageData; 

// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;


#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace=console)]
    fn log(a: &str);
}

macro_rules! console_log {
    ($($t:tt)*) => (log(&format_args!($($t)*).to_string()));
}

macro_rules! measure_elapsed_time {
    ($t:tt, $s:block) => {{
        let window = web_sys::window().expect("should have a window in this context");
        let performance = window.performance().expect("performance should be available");
        let start = performance.now();
        let result = {$s};
        let end = performance.now();
        console_log!("{}:{}[ms]", $t, end-start);
        result
    }};
}

#[wasm_bindgen]
pub fn generate_mandelbrot_set(
    canvas_w: usize,
    canvas_h: usize,
    x_min: f64,
    x_max: f64,
    y_min: f64,
    y_max: f64,
    max_iter:usize,
) -> Vec<u8> {
    measure_elapsed_time!("generate:wasm\telapsed:", {
        logic::generate_mandelbrot_set(canvas_w, canvas_h,
            x_min, x_max, y_min, y_max, max_iter)
    })
}

#[wasm_bindgen]
pub fn draw_mandelbrot_set(){
    const CANVAS_ID: &str = "canvas_wasm";
    let document = web_sys::window().unwrap().document().unwrap();
    let canvas = document.get_element_by_id(CANVAS_ID).unwrap();
    // Element型からキャスト
    let canvas: web_sys::HtmlCanvasElement = canvas
    .dyn_into::<web_sys::HtmlCanvasElement>()
    .map_err(|_| ())
    .unwrap();

    // Object型からCanvasRenderingContext2d型にキャストする。
    let context = canvas.get_context("2d")
    .unwrap()
    .unwrap()
    .dyn_into::<web_sys::CanvasRenderingContext2d>()
    .unwrap();

    let canvas_w = canvas.width() as usize;
    let canvas_h = canvas.height() as usize;

    const X_MIN: f64 = -1.5;
    const X_MAX: f64 = 0.5;
    const Y_MAX: f64 = -1.0;
    const Y_MIN: f64 = 1.0;
    const MAX_ITER: usize = 64;

    let mut result = measure_elapsed_time!(
        "generate:wasm\telapsed:", {
            logic::generate_mandelbrot_set(canvas_w, canvas_h, X_MIN,X_MAX,Y_MIN,Y_MAX,MAX_ITER)
        }
    );
    measure_elapsed_time!("draw:wasm\telapsed:", {
        let data = ImageData::new_with_u8_clamped_array_and_sh(
            Clamped(&mut result),
            canvas.width(),
            canvas.height()
        );
        if let Ok(data) = data {
            let _ = context.put_image_data(&data, 0.0, 0.0);
        }
    })
}

www/index.htmlを編集

www/index.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>Hello wasm-pack!</title>
</head>

<body>
  <noscript>This page contains webassembly and javascript content, please enable javascript in your browser.</noscript>

  <button id="render">render</button>

  <div>
    <canvas id="canvas_wasm" height="600" width="600"></canvas>
  </div>

  <script src="./bootstrap.js"></script>
</body>

</html>

www/index.jsを編集

www/index.js

function draw(ctx, canvas_w, canvas_h, data) {
  let img = new ImageData(new Uint8ClampedArray(data), canvas_w, canvas_h);
  ctx.putImageData(img, 0, 0);
}

const X_MIN = -1.5;
const X_MAX = 0.5;
const Y_MIN = -1.0;
const Y_MAX = 1.0
const MAX_ITER = 64;

console.log("start loading wasm");
const mandelbrot = import('../pkg')
  .catch(console.error);

Promise.all([mandelbrot]).then(async function ([
  { generate_mandelbrot_set, draw_mandelbrot_set }
]) {
  console.log("finished loading wasm");
  const renderBtn = document.getElementById('render');
  renderBtn.addEventListener('click', () => {
    draw_mandelbrot_set();
    let wasmResult = null;
    {
      const CANVAS_ID = "canvas_hybrid";
      let canvas = document.getElementById(CANVAS_ID);
      let context = canvas.getContext("2d");
      const canvasWidth = canvas.width;
      const canvasHeight = canvas.height;

      const generateStratTime = Date.now();
      wasmResult = generate_mandelbrot_set(
        canvasWidth, canvasHeight, X_MIN, X_MAX, Y_MIN, Y_MAX, MAX_ITER
      );
      const generateEndTime = Date.now();
      const drawStratTime = Date.now();
      draw(context, canvasWidth, canvasHeight, wasmResult);
      const drawEndTime = Date.now();
      const elapsed = generateEndTime - generateStratTime;
      console.log(`\tgenerate:wasm\tgenerate_elapsed:${elapsed}[ms]`);
      console.log(`\tdwaw: js\tdraw_elapsed: ${drawEndTime - drawStratTime}[ms]`);
    }
  })
})

Cargo.tomlを編集

Cargo.toml
[package]
name = "sample"
version = "0.1.0"
authors = #hidden
edition = "2018"

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

[features]
default = ["console_error_panic_hook"]

[dependencies]
wasm-bindgen = "0.2.63"

# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
console_error_panic_hook = { version = "0.1.6", optional = true }

# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
# compared to the default allocator's ~10K. It is slower than the default
# allocator, however.
wee_alloc = { version = "0.4.5", optional = true }

js-sys="0.3.40"

[dev-dependencies]
wasm-bindgen-test = "0.3.13"

[profile.release]
# Tell `rustc` to optimize for small code size.
opt-level = "s"

[dependencies.web-sys]
version="0.3.4"
features= [
   'CanvasRenderingContext2d',
   'Document',
   'Element',
   'HtmlCanvasElement',
   'ImageData',
   'Performance',
   'Window',
]

ビルド&実行

wasm-pack build
cd www
npm run start

http://localhost:8080 を開く