続編です、もう少し実践的なライフゲームを作ってみました。
Demo
Electonで動かしてますが普通にブラウザでも動きます。
前の記事は試してみただけだったので少し詳しくまとめてみます。
rustifyとは
borwserifyのtransformでrustコードをwasmに変換するものです。
js内にインラインで書いたrustをwasmに変換することもできるので手軽に利用することもできます。
何をやっているかというと、wasmを生成してwasm-gcを噛ませて元のjsファイルのrustの部分をwasmで置き換えるということをやっています。
% rustc +nightly --target wasm32-unknown-unknown <file> --crate-type=cdylib --out-dir <outdir>
% wasm-gc <file> <out_file>
実行しているコマンドはこれだけなので、wasmをfetchやfsでreadFileSyncしてもほぼ同じです。
rustifyができそうなこと
rustifyはjsのプロジェクトに対して一部分をrust+wasmに置き換えるようなことができそうです。
webpackなどで行うメインのビルド前に、rustifyで変換をかけてからwebpackビルドに回すみたいなことは簡単にできるので、もともとあるjsプロジェクトの高速化に使えるのでは・・・という感想です。
メモリ共有
ライフゲームを作るにあたって配列データをwasmとjsで共有する必要があります。emscriptenを使えばModule._mallocを使えるのですがwasm32-unknown-unknownでは使えません。
正解の方法はわからなかったのでBox\<T>でヒープに確保したarrayの生ポインタを返すことで実現してみました。
var rust = require('rustify')
var wasm = rust`
const ROW: usize = 10;
const COL: usize = 10;
#[no_mangle]
pub fn allocate() -> *mut [bool; ROW * COL] {
let tmp = [true; ROW * COL];
let mut buf = Box::new(tmp);
unsafe { &mut *buf }
}
#[no_mangle]
pub fn get_element(i: usize, ptr: *mut bool, len: usize) -> bool {
let buf: &[bool] = unsafe { std::slice::from_raw_parts(ptr, len) };
if i < 0 || i > len - 1 { false } else { buf[i] }
}
`
WebAssembly.instantiate(wasm, {})
.then(function (res) {
var allocate = res.instance.exports.allocate
var getElement = res.instance.exports.get_element
var len = 100
var ptr = allocate()
console.log(getElement(1, ptr, len))
}).catch(function (e) {
console.error('Creating WASM module failed', e)
})
js側で生ポインタから値を取り出す方法がわからなかったのでgetElementしています。
この方法だとサイズを動的に確保できないのが残念なので時間があれば改良したいです。
メモリの解放はwasm-gcがやってくれると信じて使います。
開発しぐさ
rustのプロジェクトをベースにしてviewディレクトリにnode.jsのプロジェクトを作成して開発しました。
.
├── Cargo.toml
├── src
│ ├── lib.rs
│ ├── lifegame.rs
│ ├── main.rs
│ └── xorshift.rs
└── view
├── build.js
├── index.html
├── lifegame.js
├── main.js
└── package.json
wasmにするのに必要な部分(lib.rs)とrustだけでも動くロジック部分(lib.rs以外)を切り分けで開発しました(ファイル名は何でもいいです)。
wasmのみに必要な部分(lib.rs)はロジック部分とは本質的に無関係なのでインターフェースだけ決めて、ロジック部分ができるまではダミーデータを使いました。
# view部分のビルド
% cd view
% ./node_modules/.bin/browserify -t rustify lifegame.js > build.js
ロジック部分のRustには#[test]があるので今回のロジックの核であるライフゲーム部分は先に簡単なtestを書いて動作を確認しました。
% cargo test
ロジック部分ができたらダミデータを置き換えて完成です。
ライフゲーム本体
pub struct Lifegame {
row: i32,
col: i32,
room: Vec<bool>,
}
impl Lifegame {
pub fn new(buf: &mut [bool], row: usize, col: usize) -> Lifegame {
Lifegame {
row: row as i32,
col: col as i32,
room: buf.to_vec(),
}
}
pub fn next(&self) -> Vec<bool> {
self.room
.iter()
.enumerate()
.map(|(n, _)| self.get_next_state(n))
.collect()
}
fn get_next_state(&self, n: usize) -> bool {
let y = n as i32 / self.col;
let x = n as i32 - y * self.col;
let num_of_living = [(x - 1, y + 1),
(x, y + 1),
(x + 1, y + 1),
(x - 1, y),
(x + 1, y),
(x - 1, y - 1),
(x, y - 1),
(x + 1, y - 1)]
.iter()
.fold(0,
|sum, &(x, y)| if self.get_cell(x, y) { sum + 1 } else { sum });
match num_of_living {
3 => true,
2 if self.room[n] => true,
_ => false,
}
}
fn get_cell(&self, x: i32, y: i32) -> bool {
if x < 0 || y < 0 || x >= self.col || y >= self.row {
false
} else {
let n = y * self.col + x;
self.room[n as usize]
}
}
}
Rust結構書きやすいしコンパイラも賢いので本当に好きです。がまだ雰囲気で書いているので間違っているかもしれません。
もっと遊びたい
Rocket - A Rust game running on WASM
この記事を見てしゅごいとなったのでもっと遊びたくなりました。
もともとPistonで作っていたゲームをwasm+canvas(html)で置き換えた話のようです。
あとこの前extern crateができないと思っていたのですがrustcでなくcargoを使えばできることもわかったのでできることの幅も広がりそうです。
% cargo build --target wasm32-unknown-unknown
正直jsはあまり洗練されたプログラミング言語ではないのでrustを使ってwebのロジックを書けるのは魅力的です。
実用するにはどうすればいいのか考えながらもう少し遊んでいきたいです。
Show comments