Rustでwasmを使ってWebアプリを作る
もうすぐ年末?!
早いもので、年末まであと2週間ですね。大学生活最後の冬休みになる...はずなので、今回の正月は思う存分、寝正月をしながらゲーム発展国++やろうと思っています。
さて、この記事はCyberAgent20新卒エンジニアAdvent Calendar 2019の11日目の記事です。
前の人は、Shotaro Taoさんで、次の人はAyumuさんです。
ところで、2021年度新卒採用 エンジニアコースが始まったそうなので良ければどうぞ
自己紹介
余談はさておき、簡潔に自己紹介をさせていただきます。 poccariswet ( ぽっかりスウェット ) って言います。twitter: https://twitter.com/poccariswet でやってます。 深夜ラジオを聴くことが好きで、よくこの名前でハガキ職人っぽいことをしています。(最近読まれてない)
好きな言語は、goとrustで、よくこの2つを使って個人開発だったりをしています。 あと、動画配信系の技術にも興味があって色々と情報を追っています。
rust で wasm がやりたいんだ
自分がwasmをやろうと思ったきっかけは、rustでライブラリを作ったことにあります。もともと、rustを勉強をする際、僕は何かを作りながらじゃないと勉強に身が入らないのでじゃあrustで何かライブラリを作ればいいじゃん!という考えからライブラリを作りました。
RustでAPNGのライブラリを作ってみた - Qiita ここには経緯や構造などについても少し語っているので良ければどうぞ。
で!せっかくライブラリを作ったのでこれをどうにかアプリ(使える形)にできないかと考えていて、wasm使えばrustでフロントもかけるしめっちゃいい!ってことでwasmでwebアプリを作り始めました。
rustでwasmをやるメリットについては Why should you use Rust in WebAssembly? | Opensource.com を見てください
いくぜ、rust で wasm で webアプリ
とにかく、最初は勝手がわからなかったのでTutorial を読んでいきました。
基礎的や応用的なことまで詳細に書いてあって、実際にライフゲームを作るパートも含まれているので、rustでwasmやりたいだけど...どうしていいかわからないって人にも、もってこいだと思います。一度やればwasmマスターと言っても過言ではない(なれるとは言ってない)と思ってます。
それで、今回自分が作ったのはAPNGを作成するウェブアプリです。(出力まではっや)
やったこととしては、PNG画像をreadし そのbufferを ArrayBuffer
型にCastし、配列の中に入れwasm(rust)側に渡します。
そして、APNGのエンコードが終わったら、逆にwasmから PNG の bufferを受け取り、blobにして image.url
に入れます。
ざっくり説明するとこんな感じです。
wasm-bindgenという優れしもの
rustでwasmをやるうえで必須なライブラリが wasm-bindgen です。また、 wasm-pack というrust コードを npm 向けに正しくパッケージングをすることだけでなく、WebAssembly にコードをコンパイルにも役立つものもあります。(今回はwasm-bingenについて)
どのようなサポートをしてくれるかというと
- rustのメソッド、構造体をjsのクラスやメソッドに変換
- wasm とjs間 のinterface
- js-sys: jsのobjectとmethodとかpropertyを結合するためのライブラリ <- 今回使った
- web-sys: RustからDOM APIを触るためのライブラリ
- wasm_bindgen_futures: JavaScript Promiseタイプを操作するためのブリッジを提供
大雑把に説明すると、この3点かなって僕は思ってます。(ぬけがあったらすいません。)
また、実際に実装していくなかで、wasmをやる上でのエコシステムは発展途上ですが割と揃っているのかなと感じます。
つまづき、気づき、コードを解説
1. js <-> wasm 間のデータのやり取り
ここは自分が最大に詰まったポイントで、PNG file をどのようにwasm側でcastするのかがわかりませんでした。
コード部分でいうとここです。
pub fn apng_encode_all(data: js_sys::Array, frame_speed: f64) -> Vec<u8> { ...
js側ではこんな感じで値を入れています。
let buffer = wasm.apngEncodeAll(buffers, frame_speed);
wasmの実装上、PNGのデータをVec<u8>
でもつ必要があるため、js側ではUint8ArrayBuffer
を配列に入れて複数のPNG画像を渡しています。
wasm側で受け取ったdataの中身を見ると
Array { obj: Object { obj: JsValue([Uint8Array, Uint8Array]) } }
となっており、こんなにも複雑になってしまいます。理由としては現状、2次元配列(Vec<Vec<u8>>
) がサポートされていないことです。そのためArrayオブジェクト内のオブジェクトをUint8Arrayにキャストし、copy_to
または to_vec
を使用してその中身を抽出するしかありません。
実際にissueで質問して Vec<Vec<u8>>
がサポートできていないことを知りました。
相当試行錯誤しながら contributorの人の意見を参考にしながら 受け取った値を Vec<Vec<u8>>
の形にしました。
解決方法
// Array { obj: Object { obj: JsValue([Uint8Array, Uint8Array]) } } let data: Vec<Vec<u8>> = data .values() // return Iterator .into_iter() // return JsValue .map(|x| { x.unwrap_throw() .unchecked_ref::<js_sys::Uint8Array>()// JsValueの中身をcastする .to_vec() // Uint8ArrayをVec<u8>にする }) .collect(); // Vec<u8> をVec<>の中にcollectする
現状だと上記のように複雑な形で変換するようになっています。
2. rust特有のシステムである ‟アレ‟
はい、rustを使ったことがある人ならお馴染みの所有権システムのことです。
コードでいうと、この部分にあたります。
※以下からはサンプルコードになるのですが、本コードでもこのように使っています。
use std::io::{BufWriter, Write}; fn write() -> Vec<u8> { let mut buf = vec![]; let test = "Hello, string io!"; let mut f = BufWriter::new(&mut buf); // ここで buf がBufWriterに所有権が渡される f.write(test.as_bytes()).unwrap(); println!("{:?}", buf); // <--- ERRORRRRRRRRRR!!!! buf } fn main() { let b = write() ... }
サンプルコードにあるようにこれえはエラーが起きてしまいます。
理由としては、変数 buf
を 標準出力ライブラリである BufWriter
に譲渡しているため、buf
を参照することはできないということです。これで起きた問題としては、変数 bufを戻り値として返せなくなるということです。
解決方法
fn write() -> Vec<u8> { let mut buf = vec![]; { // ブロックスコープ let test = "Hello, string io!"; let mut f = BufWriter::new(&mut buf); f.write(test.as_bytes()).unwrap(); } // BufWriter で貸したbufはここを抜けると返却されて使えるようになる buf }
ここでやった解決方法は、ブロックスコープを用いて、変数buf
の参照の有効なの部分を可視化&示すをしています。この場合、buf
はブロックスコープ内でBufWriter
に所有権を譲渡していてるため、その所有権はもちろん、スコープを抜けたら解放され所有権がスコープ外側のbuf
に戻されます。これでbuf
はreturnできるようになりました👏
future develop
今後もちょこちょこ修正していくのはもちろんなんですが、web上で絵をかいてそれをAPNGとして出力されるものを作りたいなと考えています。上の方で説明している通りwasm側からDOMを操作できるライブラリ(web_sys)があるのでそれをうまく利用して作れればなと思っています。
あ、あと Rust and WebAssembly のコミュニティにものすごくお世話になったので自分もコントリビュートしていくことで感謝を伝えたいなと感じました。
おわりに
はじめての Advent Calendar 結構長くなってしまいました。ここまで読んでくださった方、本当にありがとうございます。またなんか作ったり気づきがあれば載せます。