お久しぶりでございます。Scalaでバックエンドを開発しているaxtstar(@axtstart)です。
みなさまゴールデンウィークはいかがお過ごしだったでしょうか?
我が家はあまり旅行に行くということもなく、近場のドライブや、ちょい大き目の公園などで過ごすことが多かったです。
さて、そのおかげというわけではありませんが、この連休を利用して、 新たにRustとWebAssemblyに入門してきたので今回はそのあたりの話を、書きたいと思います。
Image by prettysleepy1 from Pixabay
前書き
遅ればせながら、前々から気になっていた、Rust Programming Languageの勉強をGWを利用して初めて見ました。
随分昔ですが、Visual C++でDLLを作ってそれをフロントのVisual Basicで呼び出すのが最強と思っていた時があります。
それと似た世界が、JavascriptとRust(WebAssembly)なのではないかと、最近考えていました。
少々遅い気もしますが、何事もはじめないよりは、はじめた方が面白いし何かの役に立つことがあればいいなと。
普段はScalaで開発する事が多いので、そのScalaとの比較として書いていきたいと思います。
Rust、Scala比較
さて、早速RustとScalaを比較してみます。
◎RustはScalaとは違ってガーベージコレクションを持たない
→自分でメモリ管理をする必要がある*1
とは言え、強力な言語システムのサポートがあるため、昔のように、メモリの解放もれを気にすることはかなり減っているようです。
Scopeを抜けると確保したメモリは解放される*2などなど
→OwnershipメカニズムやScopeメカニズムによって、極力バグをcompile時に発見できる仕組みがある。
・stackとheapを常に意識できるような書き方になる
例えば、これはダメ
//Stringはstructであるため、その実体はheap領域に確保される let s1 = String::from("hello"); //s2はs1をディープコピーするのではなく、参照をコピーしている(シャローコピー) let s2 = s1; //memory safetyの観点から、Rustはこの時点でs1をOut of Scopeにする println!("{}, world!", s1);//<---コンパイルエラー(既にOut of Scopeのため) println!("{}, world!", s2);//OK
関数に渡しても、Out of Scopeになる
fn bollow(c:String){ } let s1 = String::from("hello"); bollow(s1); println!("{}, world!", s1);//<---コンパイルエラー(既にOut of Scopeのため)
これはOK
// 固定文字列はstack上に確保される let s1 = "hello!"; //これはスタック上の操作 let s2 = s1; println!("{}, world!", s1);
これもOK
fn bollow(c:&str) { } // 固定文字列はstack上に確保される let s1 = "hello!"; bollow(s1); println!("{}, world!", s1);
参照渡しにした場合はこの限りではありません。
fn calculate_length(s: &String) -> usize { s.len() } let s1 = String::from("hello"); let len = calculate_length(&s1); // 参照渡しの場合、ここでOwnereshipの変更は無い println!("{}", s1); //OK
もちろん上記のようなことはScalaではおこらず、コンパイルは通ります*3。
◎StatementとExpressionが異なる概念として扱われる
Scalaだとあまり意識しない*4セミコロン(「;」)の有る無しによって、StatementなのかExpressionなのかが決まります。
これはダメ
fn add(x:i32, y:i32) -> i32 { // これはStatement x + y; // <--i32を戻り値として指定しているので、戻り値=()は、コンパイルエラー }
これはOK
fn add(x:i32, y:i32) -> i32 { // これはExpression x + y //ここが戻り値になる }
◎immutable、mutableはScalaと非常に似ている
immutable
let x = 5; x = 10; // <-- コンパイルエラー let x = 6; // <-- これはOK(シャードーイング)※ScalaはScopeで実現
mutable
let mut x = 5; x = 10; // OK let x = 6; // <-- OK(シャードーイング)※ScalaはScopeで実現
◎enumが非常に強力
・Option
enum Option<T> { Some(T), None, }
◎nullが無い
その発明者自身*6が10億ドルの過ちといった「null」がRustには存在しません。
Scalaはnullそのものを消去することはできていません。極力使わないようにプログラミングすることはもちろん可能ではあります。
◎match文はScalaと非常に似ている
Rust
fn fib(n: u32) -> u32 { match n { 0 => 0, 1 => 1, _ => fib(n-2) + fib(n-1), } }
順序と case、「,」以外同じように見える
Scala
def fib(n:Int):Int= { n match { case 0 => 0 case 1 => 1 case _ => fib(n-2) + fib(n-1) } }
◎文字の取り扱いはそこそこ大変
let h = String::from("hello"); let w = String::from(", world!"); let hw = h + &w; println!("{}", w);// OK println!("{}", h);// コンパイルエラー、hはStringのadd関数、演算子オーバロード(+)により、Out of Scopeになっている
◎Cargoという、パッケージ管理ツールがある
→Scalaのsbt、gradle、mavenのような存在。
tomlという形式で、設定ファイルを記述します。
ここで紹介した機能以外にも、似た機能や全然似てない機能はたくさんあります。
WebAssembly
MDN Web Docsによると、WebAssemblyとは、
以前ではできなかったようなウェブ上で動作するクライアントアプリケーションのために、複数の言語で記述されたコードをウェブ上でネイティブに近いスピードで実行する方法を提供します。
というもので、IEを除くだいたいの最新ブラウザなら動き、今回Hello, world!した、Rustとも相性の良いものです。
Hello, world! for WebAssembly
RustでWebAssemblyでHello, world!するには今は、wasm-bindgenを使用するのが一番簡単なようですね。
Cargo.toml
[package] name = "hello-world" version = "0.1.0" authors = ["axt <axt_star@hotmail.com>"] edition = "2018" [lib] path = "src/lib.rs" crate-type = ["cdylib", "rlib"] [dependencies] wasm-bindgen = "0.2" wee_alloc = { version = "0.4.2", optional = true }
src/lib.rs
use wasm_bindgen::prelude::*; // 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; // Rust側からJavascriptのalertを呼べるようにする。 #[wasm_bindgen] extern { fn alert(s: &str); } // JavascriptからRustの関数を呼び出せるようにする。 #[wasm_bindgen] pub fn greet() { alert("Hello, hello-world!"); }
上記の2ファイルの状態で、下記を実行すると、しばらくかかりますが、target、pkgというフォルダができて、そこにJavascript*8からRust側を呼び出す、glueスクリプト*9みたいなものを吐き出してくれます。
wasm-pack build
さらに下記で、wwwディレクトリを作成してそこに、webの開発に必要なwebpackなど一式を作成してくれます。
npm init wasm-app www
www配下でnpm installして必要なライブラリを取得します。
cd www && npm install & cd ..
pkgをlinkします。
cd pkg/ && npm link && cd ..
pkgを利用したい側でリンクを呼び出します。
cd www && npm link hello-world && cd ..
ローカルのwebサーバを起動します。
cd www && npm run start ; cd ..
ブラウザでhttp://127.0.0.1:8080/にアクセスすると、alertのダイアログでhello, world!が表示されます。
ここまでここの内容だいたい、そのままです。
RustからのDomアクセスなど
WebのDocumentやElementなどにアクセスしたい場合は、Cargo.tomlにfeatureを追加する必要があります。
[dependencies.web-sys] version = "0.3.4" features = [ 'console', 'CanvasRenderingContext2d', 'Document', 'Element', 'HtmlElement', 'HtmlBodyElement', 'HtmlButtonElement', 'HtmlCanvasElement', 'EventTarget', 'Node', 'Window', ]
bodyの最後にpタグでHello from Rust!の追加
#[wasm_bindgen] pub fn hello_p() -> Result<(), JsValue> { let window = web_sys::window().expect("no global `window` exists"); let document = window.document().expect("should have a document on window"); let body = document.body().expect("document should have a body"); // Manufacture the element we're gonna append let val = document.create_element("p")?; val.set_inner_html("Hello from Rust!"); body.append_child(&val)?; Ok(()) }
速度を比較してみる
あまりいい例ではないのですが、(スタックオーバフローになるような何も考えてない方法の)フィボナッチ数列による速度比較をして見ました。
Javascript版
function fib(n) { return n < 2 ? n : fib(n - 2) + fib(n - 1); }
WebAssembly(Rust)版
#[wasm_bindgen] pub fn fib(n: u32) -> u32 { match n { 0 => 0, 1 => 1, _ => fib(n-2) + fib(n-1), } }
3回実施した平均です。
fib(n)
n | Javascript(ms) | Rust(ms) | 割合(R/J) |
---|---|---|---|
10 | 0 | 0 | - |
30 | 17 | 14 | 1.2 |
40 | 1138 | 910 | 1.25 |
41 | 1842 | 1463 | 1.26 |
42 | 2995 | 2388 | 1.25 |
43 | 4821 | 3802 | 1.27 |
43 | 7868 | 6134 | 1.27 |
Rust版の方が2割増し程度の性能が出ました。
アルゴリズムが大変悪いので比較的低いnでPageがクラッシュしました。
ただこれだけなのに、速度差を体感できます。
画像を受け渡してみる
Rust側に、canvasのイメージバイナリを渡して透過度をあげてみます。
↓こちらに置いておきます。
Rust側は比較的簡単に実装できます。
ただバイナリは直列データとして扱う必要があるようです。
線の描画などはもっと別の方法が良いかもです。
Rust側
#[wasm_bindgen] pub fn to_tranparent(screen: & mut Screen, bytes: & mut [u8]) { for i in 0..screen.height { for j in 0..screen.width { let offset = 4 * (screen.width * i + j); bytes[offset] = bytes[offset]; bytes[offset + 1] = bytes[offset + 1]; bytes[offset + 2] = bytes[offset + 2]; bytes[offset + 3] = 25;//透明度を上げる } } }
htmlはVue.jsの読み込みと、canvas、inputを置いています。
html
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="./bootstrap.js"></script> <canvas id="canvas" height="600" width="400"> </canvas> <br /> <input @change="uploadFile" name="image" type="file" /> <button v-on:click="to_transparent">to_transparent</button>
glue以外の記述部分です。
javascript
var app = new Vue({ el: '#app', data: { text: '', left_val: '', right_val: '', number:10, message: '', canvas: null, duration: '' }, methods: { to_transparent: function() { // canvas取得 const canvas = document.querySelector('canvas'); // Rust上のScreen構造体取得 const screen = new sample.Screen(canvas.width, canvas.height); // context取得 const ctx = canvas.getContext('2d'); // canvas var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); // Rustの透過処理を呼び出し sample.to_transparent(screen, imageData.data); // 透過処理後のバイナリをimageに変換 const image = new ImageData(imageData.data, screen.width, screen.height); // canvasに書き戻す ctx.putImageData(image, 0, 0); }, uploadFile: function(e){ // ローカルのファイルアップロード部分 var canvas = document.querySelector('canvas'); var ctx = canvas.getContext("2d"); let files = e.target.files; var file = files[0]; var image = new Image(); var reader = new FileReader(); reader.onload = function(evt) { image.onload = function() { // canvasにイメージを書き込む ctx.drawImage(image, 0, 0); } image.src = evt.target.result; } reader.readAsDataURL(file); }, } })
上記でアップロードした画像の透明化ができます。
まとめ
Rustは非常に面白い特徴を持った言語ですね。WebAssemblyへの対応も進んでおり、比較的楽に開発を進めて行く事ができそうです。
全ての環境では動かない部分もあるので、まずは社内的なプロジェクトのフロントに採用するとか、 限定した環境をうたえるなら結構アリだなと思わせるものでした。
最後になりましたが、アスタミューゼでは現在、エンジニア・デザイナーを絶賛大大大募集中です! 興味のある方はぜひ下記バナーからご応募ください!!
*1:Scalaが全くしなくて良いという意味でも無いですが
*2:C++に見るRAIIに近いが、さらにエレガントな方法
*3:Rustのstructに近い概念はcase classでしょうか?
*4:Scalaではほぼ記述しない
*5:ScalaはOptionとEnumは別のもの
*6:https://ja.wikipedia.org/wiki/%E3%82%A2%E3%83%B3%E3%83%88%E3%83%8B%E3%83%BC%E3%83%BB%E3%83%9B%E3%83%BC%E3%82%A2
*7:日本語版:https://doc.rust-jp.rs/book/second-edition/
*8:Typescriptのglueも!
*9:なんというべきなのか?