astamuse Lab

astamuse Labとは、アスタミューゼのエンジニアとデザイナーのブログです。アスタミューゼの事業・サービスを支えている知識と舞台裏の今を発信しています。

Scalaから始めるRust入門

お久しぶりでございます。Scalaでバックエンドを開発しているaxtstar(@axtstart)です。

みなさまゴールデンウィークはいかがお過ごしだったでしょうか?

我が家はあまり旅行に行くということもなく、近場のドライブや、ちょい大き目の公園などで過ごすことが多かったです。

さて、そのおかげというわけではありませんが、この連休を利用して、 新たにRustとWebAssemblyに入門してきたので今回はそのあたりの話を、書きたいと思います。

f:id:astamuse:20190508020532j:plain

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として実装できる*5

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という形式で、設定ファイルを記述します。

ここで紹介した機能以外にも、似た機能や全然似てない機能はたくさんあります。

私も今回はこの*7チュートリアルで学習しました。

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のイメージバイナリを渡して透過度をあげてみます。

↓こちらに置いておきます。

github.com

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:なんというべきなのか?

Copyright © astamuse company, ltd. all rights reserved.