お久しぶりでございます。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を常に意識できるような書き方になる
例えば、これはダメ
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
println!("{}, world!", s2);
関数に渡しても、Out of Scopeになる
fn bollow(c:String){
}
let s1 = String::from("hello");
bollow(s1);
println!("{}, world!", s1);
これはOK
let s1 = "hello!";
let s2 = s1;
println!("{}, world!", s1);
これもOK
fn bollow(c:&str) {
}
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);
println!("{}", s1);
もちろん上記のようなことはScalaではおこらず、コンパイルは通ります*3。
◎StatementとExpressionが異なる概念として扱われる
Scalaだとあまり意識しない*4セミコロン(「;」)の有る無しによって、StatementなのかExpressionなのかが決まります。
これはダメ
fn add(x:i32, y:i32) -> i32 {
x + y;
}
これはOK
fn add(x:i32, y:i32) -> i32 {
x + y
}
◎immutable、mutableはScalaと非常に似ている
immutable
let x = 5;
x = 10;
let x = 6;
mutable
let mut x = 5;
x = 10;
let x = 6;
◎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);
println!("{}", h);
◎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::*;
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[wasm_bindgen]
extern {
fn alert(s: &str);
}
#[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");
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() {
const canvas = document.querySelector('canvas');
const screen = new sample.Screen(canvas.width, canvas.height);
const ctx = canvas.getContext('2d');
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
sample.to_transparent(screen, imageData.data);
const image = new ImageData(imageData.data, screen.width, screen.height);
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() {
ctx.drawImage(image, 0, 0);
}
image.src = evt.target.result;
}
reader.readAsDataURL(file);
},
}
})
上記でアップロードした画像の透明化ができます。
まとめ
Rustは非常に面白い特徴を持った言語ですね。WebAssemblyへの対応も進んでおり、比較的楽に開発を進めて行く事ができそうです。
全ての環境では動かない部分もあるので、まずは社内的なプロジェクトのフロントに採用するとか、
限定した環境をうたえるなら結構アリだなと思わせるものでした。
最後になりましたが、アスタミューゼでは現在、エンジニア・デザイナーを絶賛大大大募集中です!
興味のある方はぜひ下記バナーからご応募ください!!