Posted at

Rust on WebAssembly on PWA

Rust LT #5向けのスライドとなります。



目次


  • 登場人物

  • Rustセットアップ

  • Nuxtセットアップ

  • Hello Wasm

  • 全文検索してみる

  • Go、Kotlin



IAM

key
value

なまえ
みなみじま

ぞくせい
WEBエンジニア(なんでもやる)

Rust歴
4カ月(初心者)



何するか

5分間ガンバル

Rust+wasmとpwaの組み合わせを検証する



Rust


  • 安全

  • 速い

  • Mozilla

  • クセが強い



WebAssembly


  • 速い

  • Webブラウザで動かせるバイトコード

  • C, C++, Rust, Go, Kotlinが対応

  • おおよそのブラウザが対応済み(IE除く)



NuxtJS


  • Vuejsアプリケーションを作成するフレームワーク

  • SSR、SPA

  • ルーティング

  • 簡単



PWA


  • Progressive Web Apps

  • Webサイトをネイティブアプリっぽく使える

  • プッシュ通知もできる

  • キャッシュを使えばオフラインでも使える



Rust on WebAssembly on PWA


  • wasmファイルをキャッシュできればよりネイティブアプリに近づくのではないか。

  • PWAであればファイルが大きかったとしてもそれほど気にならないのでは?

  • さくさく?



Set up: Rust, wasm-pack

$ npm --version

6.9.0
$ cargo --version
cargo 1.34.0 (6789d8a0a 2019-04-01)
$ cargo generate --git https://github.com/rustwasm/wasm-pack-template
Project Name: RuxtWasm
Renaming project called `RuxtWasm` to `ruxt-wasm`...
Creating project called `ruxt-wasm`...
Done! New project created /home/ec2-user/ruxt-wasm
$ curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
$ cd ruxt-wasm
$ wasm-pack build



Set up: NuxtJS

$ npm install -g vue-cli

$ vue init nuxt-community/starter-template www
$ cd www && npm install && npm install --save-dev @nuxtjs/pwa
$ mkdir modules && cp -a ../pkg ./modules/ruxt-wasm
(nuxt.config.jsとpage/index.vueをvim)
$ npm run build


Set up: pwa


nuxt.config.js(の一部分)


modules: [
'@nuxtjs/pwa'
],
manifest: {
name: 'ruxt-wasm',
lang: 'ja'
}



Set up: pages/index.vue


www/pages/index.vue(※scriptだけ)

<script>

import AppLogo from '~/components/AppLogo.vue'
import * as wasm from '~/modules/ruxt-wasm'

export default {
components: {
AppLogo
},
mounted: function() {
wasm.greet();
}
}
</script>




Set up: firebase (1/2)

プロジェクト作成: https://console.firebase.google.com/

107.png



Set up: firebase (2/2)

$ npm install -g firebase-tools

$ firebase login
$ cd <project-name>
$ firebase init
$ npm run generate
$ firebase deploy



cacheの様子

$ find ./dist -type f -ls

34299086 4 -rw-rw-r-- 1 ec2-user ec2-user 1059 May 3 03:00 ./dist/sw.js
34299087 4 -rw-rw-r-- 1 ec2-user ec2-user 1620 May 3 03:00 ./dist/icon.png
34299088 4 -rw-r--r-- 1 ec2-user ec2-user 1150 May 3 03:00 ./dist/favicon.ico
34299089 24 -rw-rw-r-- 1 ec2-user ec2-user 20981 May 3 03:00 ./dist/crab.png
34299090 4 -rw-r--r-- 1 ec2-user ec2-user 354 May 3 03:00 ./dist/README.md
38617411 4 -rw-rw-r-- 1 ec2-user ec2-user 799 May 3 03:00 ./dist/_nuxt/manifest.36054caa.json
3970850 4 -rw-rw-r-- 1 ec2-user ec2-user 2574 May 3 03:00 ./dist/_nuxt/icons/icon_64.9qcoYVJwzAP.png
3970851 56 -rw-rw-r-- 1 ec2-user ec2-user 55098 May 3 03:00 ./dist/_nuxt/icons/icon_512.9qcoYVJwzAP.png
3970852 36 -rw-rw-r-- 1 ec2-user ec2-user 34872 May 3 03:00 ./dist/_nuxt/icons/icon_384.9qcoYVJwzAP.png
3970853 12 -rw-rw-r-- 1 ec2-user ec2-user 12265 May 3 03:00 ./dist/_nuxt/icons/icon_192.9qcoYVJwzAP.png
3970854 12 -rw-rw-r-- 1 ec2-user ec2-user 9024 May 3 03:00 ./dist/_nuxt/icons/icon_152.9qcoYVJwzAP.png
3970855 8 -rw-rw-r-- 1 ec2-user ec2-user 8130 May 3 03:00 ./dist/_nuxt/icons/icon_144.9qcoYVJwzAP.png
3970856 8 -rw-rw-r-- 1 ec2-user ec2-user 5400 May 3 03:00 ./dist/_nuxt/icons/icon_120.9qcoYVJwzAP.png
38617412 40 -rw-rw-r-- 1 ec2-user ec2-user 40012 May 3 03:00 ./dist/_nuxt/d174999b4965f00627c2.js
38617413 4 -rw-rw-r-- 1 ec2-user ec2-user 3077 May 3 03:00 ./dist/_nuxt/c47da5aa0a73269598d6.js
38617415 12 -rw-rw-r-- 1 ec2-user ec2-user 12205 May 3 03:00 ./dist/_nuxt/bd22632b15312d566e1b.js
38617416 4 -rw-rw-r-- 1 ec2-user ec2-user 427 May 3 03:00 ./dist/_nuxt/LICENSES
38617418 8 -rw-rw-r-- 1 ec2-user ec2-user 4252 May 3 03:00 ./dist/_nuxt/51db91afabe7c3392626.js
38617424 8 -rw-rw-r-- 1 ec2-user ec2-user 4356 May 3 03:00 ./dist/_nuxt/4d57da47643d33aa7414.js
38617425 148 -rw-rw-r-- 1 ec2-user ec2-user 149796 May 3 03:00 ./dist/_nuxt/2340463d96e5f0bad1e2.js
38617426 4 -rw-rw-r-- 1 ec2-user ec2-user 1934 May 3 03:00 ./dist/_nuxt/0ae67e94edd4b41d300f.module.wasm
34299091 0 -rw-rw-r-- 1 ec2-user ec2-user 0 May 3 03:00 ./dist/.nojekyll
34299092 4 -rw-rw-r-- 1 ec2-user ec2-user 3558 May 3 03:00 ./dist/index.html
34299093 4 -rw-rw-r-- 1 ec2-user ec2-user 3768 May 3 03:00 ./dist/200.html



Hello wasm

2.gif



まだまだつづく

SQLite使ってみます。

https://crates.io/crates/rusqlite/

日本郵便の郵便番号データ

$ wget http://www.post.japanpost.jp/zipcode/dl/kogaki/zip/ken_all.zip

$ unzip -p ken_all.zip | iconv -f sjis -t utf8 | head -n 1
01101,"060 ","0600000","ホッカイドウ","サッポロシチュウオウク","イカニケイサイガナイバアイ","北海道","札幌市中央区","以下に掲載がない場合",0,0,0,0,
$ unzip -p ken_all.zip |iconv -f sjis -t utf8 | cut -d ',' -f 8-9 | sed -e 's/","//' | sed -e 's/"//g' > addr.csv



せっかくなので全文検索にしておく


  • sqlite3はFTS(Full Test Search)に対応している。

  • addressをmecabで分かち書きにしてkeywordsカラムに入れています

$ echo 'Rust界隈、猛者ばかり' | mecab -Owakati

Rust 界隈 、 猛者 ばかり



SQLiteにinsert

$ sqlite3 asset_dir/addr.db

SQLite version 3.28.0 2019-04-16 19:49:53
sqlite> create virtual table addr using fts4( id integer primary key, address text);
sqlite>
$ i=0; for l in `cat ../addr.csv | head -n 50000`; do i=`expr $i + 1`; sqlite3 -echo addr.db "INSERT INTO addr(id, address) VALUES(${i},'$(echo $l|mecab -Owakati)');"; done
$ sqlite3 addr.db 'SELECT count(*) FROM addr;'
50000

なお、dbファイルは6.2MBになりました。



いざbuild

しかし、、、

wasm-pack buildが、sqlite3のライブラリ関係で四苦八苦した末うまくできず。。

[INFO]: Compiling to Wasm...

Compiling libsqlite3-sys v0.14.0
error: failed to run custom build command for `libsqlite3-sys v0.14.0`
process didn't exit successfully: `/home/ec2-user/ruxt-wasm/target/release/build/libsqlite3-sys-16fb9aff8b127c8d/build-script-build` (exit code: 1)



Emscripten

別の方法を試します

https://emscripten.org/docs/getting_started/downloads.html

[target.wasm32-unknown-emscripten]

rustflags = [
"-Clink-args=-s EXPORTED_FUNCTIONS=['_cnt','_ins','_add','_sel','_cntall'] -s EXTRA_EXPORTED_RUNTIME_METHODS=['cwrap'] --embed-file asset_dir/addr.db -O1 -WASM1 -o ruxt.wasm.js -memory-init-file1 -ALLOW_MEMORY_GROWTH1 -s SINGLE_FILE=1",
]



簡単なソース(KSC)


main.rs

#[no_mangle]

pub fn cnt(raw: *mut u8, len : usize) -> i32{
let buffer: &[u8] = unsafe { std::slice::from_raw_parts_mut(raw, len) };
let rust_str: &str = str::from_utf8(buffer).unwrap();
let keyword: String = rust_str.to_owned();
let conn = Connection::open("asset_dir/addr.db").unwrap();
let row0 = conn.query_row(&format!("SELECT count(*) as cnt FROM addr WHERE keywords MATCH '{}*'", keyword), params![], |row| {
row.get(0)
});

let result:rusqlite::types::Value = row0.unwrap();
match result {
Integer(i) => i,
_ => 0
}
}


$ rustup target add wasm32-unknown-emscripten

$ env CC=emcc cargo build --release --target=wasm32-unknown-emscripten --verbose
...
Finished dev [unoptimized + debuginfo] target(s) in 4m 27s

でけた!

・・jsは31Mとかになってます。。



簡単なjs


index.vue

 methods: {

cnt: function(){
var utf8 = (new TextEncoder).encode(this.cnttext);
var len = utf8.byteLength;
var bufptr = Module._malloc(len);
Module.HEAPU8.set(utf8, bufptr);
var _cnt = cwrap('cnt', 'number', ['number', 'number']);
alert("SELECT count(*) FROM addr WHERE keywords MATCH '" + this.cnttext + "' = "+ `${_cnt(bufptr, len)}`);
Module._free(bufptr);
},
}



いちおう動いた

WPVmbmSiWa4qfnBdyz5zmu.gif



JSでもやる 下ごしらえ編

$ echo "[" > addr.json

$ sqlite3 asset_dir/addr.db "select '[\"' || replace(replace(keywords,' ' ,'\",\"') || '],',',\"]',']') from addr;" >> addr.json
$ echo "]" >> addr.json



[
["札幌","市","中央","区","以下","に","掲載","が","ない","場合"],
["札幌","市","中央","区","旭ケ丘"],
...



JSでもやる

import arrAddr from '~/assets/data/addr.json'

...

cntjs: function(){
var reg = new RegExp('^'+this.cnttext);
var count = (arrAddr.filter((line) => line.some((v) => str.match(reg)))).length;
alert(`${count}`);
},



performance(msec)

wasm
js

7.2
56.3

3.4
67.0

3.3
53.1

3.3
48.7

4.6
50.4

(win10Pro 64bit corei5 MEM4GB)



やってみて


  • とりあえず早い

  • wasm-packとlibsqliteがうまくいかなかった

  • Rust to jsでString返すのが実装間に合わず

  • とにかくファイルが大きい(31MB)

  • 使いどころ次第?

  • Go+wasmも良さそう



おまけ

GOでやってみる


main.go

package main

import "syscall/js"

func main() {
var cb js.Func
cb = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
js.Global().Call("alert", "Hello, go-wasm!")
cb.Release() // release the function if the button will not be clicked again
return nil
})
js.Global().Get("document").Call("getElementById", "go-alert").Call("addEventListener", "click", cb)

select {}
}




build

$ go version

go version go1.12.4 linux/amd64
$ GOARCH=wasm GOOS=js go build -o alert.wasm main.go
$ ls static/wasm/
alert.wasm wasm_exec.js

github:golang/go/tree/misc/wasmのhtmlとjsを使います。



nuxtにalert.wasm追加


alert.vue(の一部)

    window.GoWasm = {

functions: {},
init () {
if (!WebAssembly.instantiateStreaming) { // polyfill
WebAssembly.instantiateStreaming = async (resp, importObject) => {
const source = await (await resp).arrayBuffer
return await WebAssembly.instantiate(source, importObject)
}
}
const go = new Go()
let mod, inst
WebAssembly.instantiateStreaming(fetch("/wasm/alert.wasm"), go.importObject)
.then(result => {
mod = result.module
inst = result.instance
go.run(inst)
})
.catch(console.error)
}
}
let ScriptGo = document.createElement('script');
ScriptGo.setAttribute('src', '/wasm/wasm_exec.js');
ScriptGo.onload = ScriptGo.onreadystatechange = function() {
GoWasm.init()
};
document.head.appendChild(ScriptGo);
},



kotlinでもやってみる

$ cd ~/workdir && git clone https://github.com/JetBrains/kotlin-native.git

$ cd kotlin-native && ./gradlew dependencies:update && ./gradlew bundle
$ cd ~/workdir && git clone https://github.com/ftomassetti/kotlin-wasm-examples.git
$ cd kotlin-wasm-examples && ./build.sh



main.kt


main.kt

import kotlinx.interop.wasm.dom.*

import kotlinx.wasm.jsinterop.*

fun main(args: Array<String>) {
val s: String = "Hello Kotlin-wasm!!"
s.forEach {
it -> jsaddchar(it.toInt())
}
}

@SymbolName("imported_jsaddchar")
external public fun jsaddchar(i: Int)


Stringの受け渡しとかやるまでは余力がなく、コールバックで文字を埋め込んでるだけ



nuxtにkotlin.wasm追加


alert.vue(の一部)

  mounted() {

let ScriptElm = document.createElement('script');
ScriptKtln.setAttribute('src', '/kotlin.wasm.js');
ScriptKtln.setAttribute('wasm', '/kotlin.wasm');
ScriptKtln.onload = ScriptKtln.onreadystatechange = function() {
konan.libraries.push({"imported_jsaddchar":function(i){ document.getElementById('hellokotlin').value = document.getElementById('hellokotlin').value + String.fromCharCode(i);}});
};
document.head.appendChild(ScriptKtln);
},



おわり

6.gif