この記事について
発端は、Rustを勉強する為に、ルービックキューブアプリをRust+WebAssemblyで作成し始めた事になります。
詳細はこちらの自分記事に書いてあります。
その後少しずつ進めて、ルービックキューブアプリとして遊べるぐらいまでは進めました。
ただ、ここまでくると当初は「そこまで踏み込まなくて良いだろう」と思っていた事にチャレンジしたくなってきます。
何かと言うと、見た目です。そもそもデザインのセンスは無いので、オサレな画面にしようなどという野望は持っていませんが、さすがにボタンなどのデザインは世の中のリソースを活用したくなってきます。
今後、基本操作だけでなく各種機能を付けたくなった時、seed(Rust用WebAssemblyのフレームワークの一つ。現在使用中)で全て実現するには技術的に可能だったとしても、情報を見つけられるか、見つけられたとしても調査に結構時間がかかるだろう、世のWebUIフレームワークと連携出来る様にしておきたいという判断もあります。
目標
javaScript側で、WebAssembly(seed)とやり取りを行う方法を確認します。最終目標はVue、ReactなどのリッチUIフレームワークから使用する事ですが、この記事ではjavaScript側で使用するステップまで行います。
具体的には以下の技術要素の確認を行います。
- javaScript側からWebAssemblyの関数を呼び出す
- 関数の引数で情報を渡す
- 関数の返り値をjavaScript側で受け取る
- seedのレンダリングをトリガーする
- WebAssemblyからjavaScriptの関数を呼び出す
- 関数の引数で情報を渡す
- 関数の返り値をjavaScript側で受け取る
- javaScript側でその値を使用して何か処理する
基本技術確認
seedの公式gitのexamplesに2つ良さそうなのがあります。
-
rust_from_js
実行した後に、開発者ツールで引数付きでjavaScriptコマンドを実行し、Renderボタンを押すと seed側を反映するサンプル
Readmeには、詳細なサンプルは後述update_from_js
を見てと書いてあります。 -
update_from_js
javaScriptのタイマーでjs関数を実行し、seed側で時間表示するサンプル
rust_from_js の技術要素確認
2つのうちの基本的だと思われる方を確認します。順を追って確認します。
index.html(javaScript側)
<body>
<section id="app"></section>
<script type="module">
import { set_title } from '/pkg/package.js';
window.rust = { set_title };
import init from '/pkg/package.js';
init('/pkg/package_bg.wasm');
</script>
</body>
/pkg/package.js
はseedのコンパイル生成物です。seed内のset_title
という関数をjavaScript側のrust
配下に割り当てるという事をやっているようです。
set_title
lib.rs(WebAssembly側)の該当関数を調べます。
#[wasm_bindgen]
pub fn set_title(title: String) {
TITLE.with(|title_cell| title_cell.replace(title));
}
wasm_bindgenの指定をする事で、javaScript側から呼べるようにしているようです。ちょっと寄り道して、TITLE
なるオブジェクトの中身を見てみます。なんか複雑な変数ですね。
thread_local!(static TITLE: RefCell<String> = RefCell::new("I'm TITLE!".to_owned()));
上記2ブロックで出てきた単語を調べます。
- thread_local・・・複数スレッドから安全に参照出来る様にする為の技術
- RefCell・・・情報をラッピングする事で、不変の様に見えて内部の値を可変にする仕組み
- to_owned・・・""括り文字列(&str)をStringに変更する手法の一つ
- with・・・ちょっと良い情報が見つかりませんでしたが、クロージャを使って内部の値を操作出来る様です。
- replace・・・RefCellの関数で中身の交換を行う
という事で、記述は複雑ですが、スレッドセーフな変数(?)の宣言とその変更を行う関数の宣言ですね。
開発者ツール実行
Readmeには開発者ツールで以下命令実行とあります。
rust.set_title("New title")
window.rust
は/pkg/package.jsから配下にset_title
をimportしているのでそれが呼ばれる事で、WebAssembly側内部変数を変えているという事になります。
Render実行
開発者ツールでコンソールしても画面表示は変わりません。ボタンをクリックする事で反映します。
fn title() -> String {
TITLE.with(|title_cell| title_cell.borrow().clone())
}
// ・・省略・・
fn view(_: &Model) -> Vec<Node<Msg>> {
vec![
h1![title()],
button!["Rerender", ev(Ev::Click, |_| Msg::Rerender)],
]
}
通常は、modelに入った変数を参照しますが、javaScript側からmodelの変更が出来ない為、static変数(?)を参照する関数で値を取得しています。
update_from_js の技術要素確認
rust_from_jsでは出来ていなかったWebAssembly側のレンダリング対応や、WebAssembly側からのjavaScript関数実行も含まれます。真似すれば行けそうです。ポイントを確認していきます。
index.html
<body>
<section id="app"></section>
<script src="/public/index.js" type="module"></script>
</body>
外部ファイルを読み込むようになってますね。index.jsを見てみます。
import init from '/pkg/package.js';
import { start } from '/pkg/package.js';
window.enableClock = () => {
const sendTick = () => {
tick(new Date().toLocaleTimeString());
};
sendTick();
setInterval(() => {
sendTick();
}, 1000);
};
init('/pkg/package_bg.wasm').then(() => {
const [js_ready, tick] = start();
window.tick = tick;
js_ready(true);
});
WebAssemblyの初期処理が終わった後、importしておいたWebAssemblyのstart関数を呼び、js_ready,tick関数を取得し、tickをグローバル関数に指定する事をしています。そして、js_ready命令を実行してWebAssembly側に準備完了を伝えているようです。
一度enableClock
が実行されると、1秒ごとにsendTick
が実行される形ですね。そしてsendTick
では、tickが呼ばれます。enableClock
関数はjavaScript側では呼ばれないので、WebAssembly側で呼ばれるようです。
WebAssembly側のenableClock
#[wasm_bindgen]
extern "C" {
fn enableClock();
}
extern "C"
というのは、C言語同様に外部からアクセスを提供する事を可能にするそうです。これにより、WebAssembly側でjavaScript側のenableClock関数を呼び出す事が可能になります。
WebAssembly側のstart関数
wasm_bindgen(start)
でなくwasm_bindgen
を指定しています。javaScript側で明示的に実行し、返り値を得る為に自動実行しない様にしている様です。
#[wasm_bindgen]
// `wasm-bindgen` cannot transfer struct with public closures to JS (yet) so we have to send slice.
pub fn start() -> Box<[JsValue]> {
let app = App::start("app", init, update, view);
create_closures_for_js(&app)
}
WebAssembly側、javaScript側で呼ばれる関数指定
javaScript側では、start関数で関数としての戻り値を受け取ってます。それの供給元です。
fn create_closures_for_js(app: &App<Msg, Model, Node<Msg>>) -> Box<[JsValue]> {
let js_ready = wrap_in_permanent_closure(enc!((app) move |ready| {
app.update(Msg::JsReady(ready));
}));
let tick = wrap_in_permanent_closure(enc!((app) move |time| {
app.update(Msg::Tick(time));
}));
vec![js_ready, tick].into_boxed_slice()
}
fn wrap_in_permanent_closure<T>(f: impl FnMut(T) + 'static) -> JsValue
where
T: wasm_bindgen::convert::FromWasmAbi + 'static,
{
// `Closure::new` isn't in `stable` Rust (yet) - it's a custom implementation from Seed.
// If you need more flexibility, use `Closure::wrap`.
let closure = Closure::new(f);
let closure_as_js_value = closure.as_ref().clone();
// `forget` leaks `Closure` - we should use it only when
// we want to call given `Closure` more than once.
closure.forget();
closure_as_js_value
}
2つの関数は、一度Vecに保存した後、into_boxed_slice
にBoxに変換してます。
JsValueですが、公式ページによるとjavaScript側で保有されるオブジェクトとの事です。
私の様な初心者にとっては難しい書式が並んでいます。詳細まで理解するのは今回は諦めます。この書式を真似れば、引数を取って、app(WebAssembly本体(?))のupdate関数を呼び出す関数をjavaScript側に返す事が出来るという事です。
update関数には、処理対象のMsgを指定います。update関数を使用する事で、WebAssembly側のレンダリングしています。
自分でチャレンジ
調査した情報を元に、自分でもやってみます。
準備
WebAssembly(seed)
手前味噌ですがSeed Quickstart トレースを真似します。
プロジェクト名はseed-jstest
としておきます。
javaScript側からWebAssemblyの関数を呼び出す
index.html
WebAssembly側の関数を実行する為のボタンを用意しておきます。setWasmValueという関数を実行する様にしておきます。index.jsを呼び出すようにしておきます。
<body>
<section id="seedapp"></section>
<button onclick="setWasmValue(123)">Increment</button>
<script src="/public/index.js" type="module"></script>
</body>
public/index.js
seedプロジェクトフォルダにpublicフォルダを作り、その配下にindex.jsを作成します。
WebAssemblyの準備及びstartから返ってきた関数を、setWasmValue関数をjavaScriptのグローバルオブジェクトにセットしておきます。
import init from '/pkg/package.js';
import { start } from '/pkg/package.js';
init('/pkg/package_bg.wasm').then(() => {
const [set_wasm_value] = start();
window.setWasmValue = set_wasm_value;
});
Cargo.toml
update_from_jsで使用している、encloseをdependenciesに追記します。
[dependencies]
seed = "0.8.0"
enclose = "1.1.8" # 追記
lib.rs
start、create_closures_for_js、wrap_in_permanent_closure関数は丸ごとupdate_from_jsのものをコピーして、create_closures_for_js関数の中身だけ変えます。SetWasmValueというメッセージで処理をする事にします。
use enclose::enc; // 宣言部分追記
fn create_closures_for_js(app: &App<Msg, Model, Node<Msg>>) -> Box<[JsValue]> {
let set_wasm_value = wrap_in_permanent_closure(enc!((app) move |newvalue| {
app.update(Msg::SetWasmValue(newvalue));
}));
vec![set_wasm_value].into_boxed_slice()
}
SetWasmValueメッセージの実装をします。
enum Msg {
Increment,
SetWasmValue(i32), // 追記
}
// `update` describes how to handle each `Msg`.
fn update(msg: Msg, model: &mut Model, _: &mut impl Orders<Msg>) {
match msg {
Msg::Increment => model.counter += 1,
Msg::SetWasmValue(input_val) => { // 追記
model.counter = input_val;
},
}
}
実行
cargo make build_release
cargo make serve
で実行し、ローカルエンドポイントへアクセスします。「serWasmValue」ボタン(html側で記述)を押す事で、WebAssembly側(seedの仮想DOMで生成)でのボタンのキャプションが、javaScript側でベタ指定してWebAssembly側に渡される値123
で更新されました。
残課題(返り値)
WebAssembly側実行の戻り値も取得したいと思いましたが、前述の一旦理解を諦めた部分で、ちょっと頑張ってみましたが、Closure::new部分で返り値がある関数は受け付けない様なエラーが出ていました。
後述WebAssembly側からjavaScriptの関数を呼び出すが実現出来たらコールバック的に実現できそうなので、この部分は一旦諦めます。
今ある情報では、関数オブジェクトをjavaScript側に返す時、その関数の返り値を設定するのが難しそうです。どうしてもという場合にはコールバックを使う事にして、主目的であるWebAssembly側関数の返り値の受け取り確認は別方法で行います。
シンプルな関数で返り値処理(追記)
良く考えたら、start関数は、javaScript側から呼び出され、関数として返り値を戻していました。seedのレンダリングとか考えなければ、最初に確認したrust_fom_jsの方法で出来そうです。
// 宣言部追記
use std::cell::RefCell;
// 保持用変数宣言
thread_local!(static SAMPLE: RefCell<i32> = RefCell::new(456));
// javaScript側で使用する関数宣言
#[wasm_bindgen]
pub fn wasm_function(inputnumber: i32) -> i32 {
log(format!("wasm_function: {}", inputnumber));
SAMPLE.with(|sample_cell| sample_cell.borrow().clone())
}
// 追記
window.wasmFunction = (inputnumber) => {
console.log("js-wasmFunction:" + wasm_function(inputnumber));
}
<body>
<section id="app"></section>
<button onclick="setWasmValue(123)">setWasmValue</button>
<button onclick="wasmFunction(789)">execWasmFunction</button>
<script src="/public/index.js" type="module"></script>
</body>
「execWasmFunction」ボタンを押します。
無事にWebAssembly側に引数の値が渡され「"wasm_function: 789"」のログ出力が行われ、その返り値としてベタで返している456がjavaScript側に返却され「js-wasmFunction:456」のログ出力が行われました。
WebAssembly側からjavaScriptの関数を呼び出す
javaScript関数参照
jsFunctionという名前の関数をjavaScript側に作成する予定です。宣言を追記します。
#[wasm_bindgen]
extern "C" {
fn jsFunction(i32) -> i32;
}
WebAssembly側でjavaScript関数実行部分実装
update関数内、Msg::Increment処理部分を改造します。WebAssembly側で生成されている最初からあったカウントアップボタンです。
従来のカウントアップに加えて、jsFunctionを、モデルの値を引数に実行します。
戻ってきた値をログ出力する様にします(※wasmモジュールで出力されるので、ブラウザで出力される)。
Msg::Increment => {
model.counter += 1;
log(format!("wasm: {}", jsFunction(model.counter))); // 追記
},
javaScript側の関数定義
window.jsFunction = (newnumber) => {
console.log(newnumber);
return newnumber + 1000;
}
コンソールに表示するだけにします。また、1000を足した数を返すようにしておきます。
※注意
軽くはまったのですが、この記述は、init('/pkg/package_bg.wasm')
の前に記述する必要がありました。WebAssembly側で処理する瞬間に関数の定義がされていないと認識されない様で、実行時に以下エラーが出てきました。
package.js:251 Uncaught Error: jsFunction is not defined
実行
同様に起動し、WebAssembly側のボタン(seedの仮想DOMにより生成されたボタン)を押すと、コンソールに数値が表示されました。
javaScriptで出力されたメッセージと、wasm内で出力されたメッセージが出力されています。
まとめ
seedなどのUIフレームワークでVueやReactエコシステムにあるリッチUIコンポーネントが出てくるのは時間がかかりそうです。
今回確認した手法を使えば、別のリッチUIコンポーネントと組み合わせてページデザインできると思います。
連携部分を準備するのは大変なので、基本的にseedなどのWebAssembly側フレームワークで作成したものは、ある程度まとまったUIコンポーネントとして使用するのが良さそうです。
あとは、seed側ではUIを一切使用せず処理に特化し、javaScriptでやるには重い処理をさせる(UIはjavaScript側)という感じでしょうか。
WebAssemblyとjavaScriptでの情報やり取りのオーバーヘッドはそれなりにあるという話も聞くので、その意味でもWebAssembly側はまとまった処理をやる様にするのが良いと思います。
今回の件は主目的がRustの勉強がなので、そこらへんは度外視してますが、3D視点処理+描画をWebAssembly側でやっていて結構それに沿っている気がします。
次はVueかReactを使ってリッチなページにしたいと思っています。
参考にさせて頂いたページ
公式ページ
RefCellと内部可変性パターン
クロージャ
Struct std::cell::RefCell
Struct wasm_bindgen::JsValue
個人記事
Rustのstatic変数とthread local
Rustのスレッドローカル変数について
&str を String に変換する4つの方法
Rust の Foreign Function Interface (FFI)