JavaScript
Rust
npm
チュートリアル
WebAssembly

初めてRustでWebAssemblyするときに紹介したいチュートリアル (入門)

こんにちは、kamyknと申します。

最近趣味の範囲ですがRustでWebAssemblyを試していています。
というわけで最近Rustに関して記事を漁っていたところRust × WebAssemblyに関するチュートリアルで良いものを見つけましたのでまとめてみました。

比較的新しい内容でRustによるWebAssemblyプログラミングができ、JSから数値以外もWebAssemblyに渡せたり、npmのエコシステムに乗せたりと僕が知りたかったことがすんなり体験することができました:laughing:

この記事では上記に加えてブラウザでJSから渡す文字列とalert()ファンクションを使ってWebAssemblyを通じてHello World!ができるまでをまとめます。

この記事の要約

こちらの記事は下記のGitHub Pagesの
https://rustwasm.github.io/book/introduction.html

特に『5.2. Hello, World!』を要訳した記事です
https://rustwasm.github.io/book/game-of-life/hello-world.html

このチュートリアルでできるようになること

  • この記事で取り扱う範囲
    • WebAssemblyからJSのalert()を使ったHello World.
    • 必要なライブラリ(RustとJS周り)の紹介
    • WebpackのJSからの簡単な取り扱い方について(import周り)
    • JSからWebAssemblyへの値の受け渡し(数値以外も可)
    • Emscriptenを使わないWebAssemblyバイナリビルド

さらに元記事ではさらにライフゲームを作ってみたり、npmのパッケージとして登録するところまでをゴールとしているみたいです。

チュートリアルで出てくるRust以外の知識

このチュートリアルではRust以外の周辺の技術として、下記の知識があるとより理解が進むかと思います。

  • node.js / npm
  • Webpack

これはWebpackでwasmバイナリファイルをnpmのエコシステムに乗っける(そして元記事のゴールであるnpmにpackage登録して配布する)為に必要になります。

本編

Setup

まずは環境とツールを揃えていきます。
元記事は下記リンクのページになります。
https://rustwasm.github.io/book/game-of-life/setup.html#cargo-generate

The Rust Toolchain

rustuprustccargoがある環境を用意します。

元記事にはありませんが、Rustの公式では下記の通りです
(https://www.rust-lang.org/ja-JP/install.html より)

curl https://sh.rustup.rs -sSf | sh

このチュートリアルではrust 1.30.0が必要になります。

現在(2018年10月10日)、nightlyでも1.29なので更に新しいbetaである必要があります。
現在betaが1.30なのでbeta及びそれよりも進んだnightlyが利用可能です。
なお、元記事ではbetaのセットアップでコマンドが紹介されています。
(コメントで指摘いただきまして、間違いがありましたので内容を修正させていただきました:bow:

rustup default beta

※ ちなみにRustのマイナーバージョンはガンガン上がっていきます…!

wasm-pack

RustでWebAssemblyする際に手助けしてくれるようなコマンドを提供しているツールです。
https://github.com/rustwasm/wasm-pack

[公式] https://rustwasm.github.io/wasm-pack/installer/

curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

ちなみにこのツールがrustのバージョンを1.30.0 を要求しているので、このチュートリアルではnightlyかbetaのものが必要になっています。

なお、wasm-packはwasm-bindgenを使うことが前提となっているかと思います。

cargo-generate

これはテンプレートとなるリポジトリを指定すると、関連するものをいろいろと揃えてくれるツールのようです。
初期構築でお世話になります。
https://github.com/ashleygwilliams/cargo-generate

$ cargo install cargo-generate

npm

npm init で wasm-appが指定できればlatestでなくても大丈夫だと思います。
npmを使うにもnode.jsが必要です。
node.js / npm は他にインストール記事が豊富なので、そちらに任せることにしてここでは割愛します。

元記事には下記のインストールコマンドだけが書いてあります。

$ npm install npm@latest -g

Hello, World!

さぁ、これからWebAssemblyでHello World!をしていきましょう。
ここからの内容の元記事は下記リンクのページになります。
https://rustwasm.github.io/book/game-of-life/hello-world.html

Clone the Project Template

$ cargo generate --git https://github.com/rustwasm/wasm-pack-template
 Project Name: wasm-game-of-life # ここは自分で決めたプロジェクト名を入力する
 Creating project called `wasm-game-of-life`...
 Done! New project created /var/www/test/wasm-game-of-life

元記事内では
This should prompt you for the new project's name. We will use "wasm-game-of-life".
(新しいプロジェクトの名前の入力をしてね。この記事ではwasm-game-of-lifeにするよ)
とのことなので、名前は実は何でも良かったりしますが、以後この記事におけるwasm-game-of-lifeという単語は自分のProject Nameで置き換えてください。

※なぜwasm-game-of-lifeなのかについては、元記事のチュートリアルでgame of life(ライフゲーム)というゲームを作るからですね。私の本記事では簡単にJSからWebAssemblyに文字列を渡してHello Worldするまでを対象としますので、気になる方は元記事を参照してみてください。

ちなみにライフゲームはこんな感じのゲームです(Wikipedia)

What's Inside

元記事では階層だけが書いてあるんですが、実際に確認するときにはtreeコマンドなどを使いましょう。

$ tree
.
├  Cargo.toml
├  LICENSE_APACHE
├  LICENSE_MIT
├  README.md
└  src
    ├  lib.rs
    └── utils.rs

Build the Project

wasm-packによって下記のビルドステップをオーケストレーションしているぜとのこと。

  • Rust 1.30以降とwasm32-unknown-unknownターゲットがインストールされていることを確保
  • cargoによってRustをWebAssembly (拡張子が.wasm) バイナリにコンパイルする
  • Rustで作成されたWebAssemblyから使える(wasm-bindgenにより生成される)JavaScript APIのコードを生成

ということで、やってくれることがわかったので早速使ってみます。

$ wasm-pack build

  [1/9] Checking `rustc` version...
  [2/9] Checking crate configuration...
  [3/9] Adding WASM target...
  [4/9] Compiling to WASM...
  [5/9] Creating a pkg directory...
  [6/9] Writing a package.json...
  :-) [WARN]: Field 'description' is missing from Cargo.toml. It is not necessary, but recommended
  :-) [WARN]: Field 'repository' is missing from Cargo.toml. It is not necessary, but recommended
  :-) [WARN]: Field 'license' is missing from Cargo.toml. It is not necessary, but recommended
  [7/9] Copying over your README...
  [8/9] Installing wasm-bindgen...
  [9/9] Running WASM-bindgen...
  :-) Done in 3 minutes
| :-) Your wasm pkg is ready to publish at "/var/www/test/wasm-game-of-life/pkg".

$ tree pkg
pkg
├  README.md
├  package.json
├  wasm_test.d.ts
├  wasm_test.js
└  wasm_test_bg.wasm

README.md以外は完全に新規で作成されるらしいです。
なお、通常cargo build --target=wasm32-unknown-unknownなどすると、targetディレクトリの深い階層にバイナリが置かれますが、
wasm-packではpkg配下の浅い階層に置いてくれます。

以下、簡単な説明です。

  • wasm-game-of-life/pkg/wasm_game_of_life_bg.wasm
    • greetファンクションを含むWebAssemblyのバイナリです。
  • wasm-game-of-life/pkg/wasm_game_of_life.js
    • wasm-bindgenにより作成されるRustとJS間のやり取りができるようになる内容などを含むJSファイルです
  • wasm-game-of-life/pkg/wasm_game_of_life.d.ts
    • TypeScript用の宣言が書かれたファイルです
  • wasm-game-of-life/pkg/package.json
    • JavaScriptのツール連携用またはnpmに登録するようとのこと

Putting it into a Web Page

WebPage用のディレクトリを作ってその中でJSを書き、WebPageとして動くようにしていきます。

プロジェクトのルートディレクトリで、

$ npm init wasm-app www
npx: 1個のパッケージを2.51秒でインストールしました。
🦀 Rust + 🕸 Wasm =

を実行します。(絵文字ナイスですね!)
これはwamp-appテンプレートに基づいてnpm initしています。

$ tree www
www
├  LICENSE-APACHE
├  LICENSE-MIT
├  README.md
├  bootstrap.js
├  index.html
├  index.js
├  package-lock.json
├  package.json
└── webpack.config.js
  • wasm-game-of-life/www/package.json
    • webpackで初期設定済み(dependenciesやscriptなど)のpackage.jsonです。
      • npm run startでサーバー立ち上げ
      • npm run buildでビルドのみ実行
wasm-game-of-life/www/package.json
$ cat package.json
  ~~ 中略 ~~
  "scripts": {
    "build": "webpack --config webpack.config.js",
    "start": "webpack-dev-server"
  },
  ~~ 中略 ~~
  "devDependencies": {
    "hello-wasm-pack": "^0.1.0",
    "webpack": "^4.16.3",
    "webpack-cli": "^3.1.0",
    "webpack-dev-server": "^3.1.5",
    "copy-webpack-plugin": "^4.5.2"
  }
  • wasm-game-of-life/www/webpack.config.js
    • webpackコマンドで使われる設定が書いてあります。
  • wasm-game-of-life/www/index.html
    • 変哲もないHTMLファイルです。
wasm-game-of-life/www/index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Hello wasm-pack!</title>
  </head>
  <body>
    <script src="./bootstrap.js"></script>
  </body>
</html>
  • wasm-game-of-life/www/index.js
    • WebAssemblyのバイナリをimportしてJSから実行します
    • あとで書き換えます。(hello-wasm-packの部分)
wasm-game-of-life/www/index.js
import * as wasm from "hello-wasm-pack";

wasm.greet();

Install the dependencies

上記で作成されたnpmでdependenciesになっているライブラリを読み込みます。
これは、wasm-game-of-life/wwwで実行します

$ npm install

Using our Local wasm-game-of-life Package in www

JSファイルwasm-game-of-life/www/index.jshello-wasm-packとなっていた箇所を書き換えてwasm-pack buildで生成されたwasmバイナリを使いましょう。

ここでnpm linkを使います。
npm linkは平たく言うとローカルでnpmパッケージがあたかもnpmからインストールしたライブラリのように使えるようにする作業です。

まずはwasm-game-of-life/pkgnpm linkをします。

$ npm link

次にnpm linkされたwasm-game-of-lifewwwパッケージから参照できるように把握させます。
wasm-game-of-life/www階層で下記のコマンドを実行します。

$ npm link wasm-game-of-life

最後にwasm-game-of-life/www/index.jsを書き換えます
import先を自分のプロジェクト(wasm-game-of-lifeまたは自分の付けた名前)を指定します。

wasm-game-of-life/www/index.js
import * as wasm from "wasm-game-of-life";

wasm.greet();

Serving Locally

(さぁ、お疲れ様です。いよいよブラウザで実行確認するステップです。)
wasm-game-of-life/wwwディレクトリで下記のコマンドを実行してサーバーを立ち上げます。
なお、ブラウザで確認できるURLはhttp://localhost:8080/だそうです。

$ npm run start

npm run startはwebpack-dev-serverの実行でしたね。(→ package.jsonのscriptsの内容です)
ちなみにnpm run startでwebpack-dev-serverを立ち上げなくてもnpm run buildするとwasm-game-of-life/www/distに一通りファイルが生成されますので、直接dist配下をブラウザで開いても問題ないです。

(↓手元でDockerで環境構築してしまったので直接distを参照してみる例)
スクリーンショット 2018-10-11 1.35.36.png

Exercises

今回wasmバイナリを生成した中のファイル、wasm-game-of-life/src/lib.rsを修正してJSから文字列を渡してみましょう。

なお、Rustは静的型付け言語なのですが、今回使う型は&strという参照を持った文字列スライスを使ってみます。

wasm-game-of-life/src/lib.rs
extern crate cfg_if;
extern crate wasm_bindgen;

mod utils;

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern {
    fn alert(s: &str);
}

// このfunctionを変更します
#[wasm_bindgen]
pub fn greet(name: &str) {
    alert(&format!("Hello, {}!", name));
}

wasmをビルドし、

$ wasm-pack build

次にJSを編集します

wasm-game-of-life/www/index.js
import * as wasm from "wasm-game-of-life";

wasm.greet('kamykn');

サーバー起動(またはwebpackでbuild npm run build)

$ npm run start

(↓またしても直接distを参照してみる例)
スクリーンショット 2018-10-11 1.54.36.png

Hello kamykn!! が確認できたということで、無事にJSからWebAssemblyに文字列を渡すことができました!:laughing:

今回紹介するチュートリアルはここまで

これでnpmのエコシステムに乗ったRustによるWebAssemblyプログラミングを体験することができましたね。
また、今回のチュートリアルではJSから文字列を渡しているところにも注目です。
これはwasm-bindgenライブラリによるJSとWebAssembly間の架け橋による恩恵です。
これまでは自分で同様の処理を書くとなると、メモリ上のJSのUint8ArrayをRustにアドレスを渡して〜といった具合だったので、wasm-bindgenのおかげで大分楽になりました。

また今回、Rustという言語についても少し触れることもできました。
Rustという言語自体も借用やライフタイムなどの概念を持った興味深い言語になっていますので、もし初めて触る方なのであればRustについても軽く触ることができて一石二鳥ですね:v:
(もし興味があれば借用チェッカーに怒られながらの開発に足を突っ込んでみてはいかがでしょうか…!:laughing:)

このあと実際の開発に活かすとするならば、個人的にはまずは計算量の重めなところのWebAssemblyによる置き換えが第一歩かなと思いました。
今回紹介した記事はとても素晴らしい記事ですので、気になった方はぜひ元記事の方も見てみてはいかがでしょうか。
元記事の方では更にもう一歩踏み込んだチュートリアルに進むことができます(が、ライフゲームはちょっとチュートリアルとしてはちょっと重いかなとも思ったり:sweat_smile:)。

何はともあれ、手軽にRustによるWebAssembly体験をさせてくれた元記事様、大変ありがとうございましたm(_ _)m

↓↓偉大なるネタ元

Rust 🦀 and WebAssembly 🕸

https://rustwasm.github.io/book/introduction.html