第5回は、WebAssemblyにおいて最も人気があるとされるRustにおける開発例を紹介します。RustとJavaScriptの関数を相互に呼び出す事例を紹介します。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
本連載のサンプルコードをGitHubで公開しています。こちらからダウンロードしてみてください。
今回は、前回紹介したC/C++と並んでWebAssembly開発の本命の一翼とされる、プログラミング言語Rustによる開発を紹介します。これには、Rust and WebAssemblyワーキンググループが開発するツールやライブラリを利用します。Rust and WebAssemblyによる開発では、標準ライブラリを含めたRustの言語仕様を十分に生かしたWebAssemblyプログラムが作成可能です。
Rustは、C/C++と同様にネイティブコードをターゲットにしたプログラミング言語であるので、その高い効率と高速性を生かした使い方が、真価を発揮しやすいと言えます。また、Rustはメモリ安全が考慮されているので、ダングリングポインタやメモリリークの危険性を極力排除した開発も可能です。連載第1回でも紹介したように、まずは速度が重視される部分をRustが受け持つというのが自然な使われ方でしょう。
本稿では、それを前提にJavaScriptコードとの連携まで含めたRust and WebAssemblyの使い方を紹介していきます。
ダングリングポインタとメモリリークは、動的なメモリ利用の際に発生しがちなプログラミング上の問題です。前者は有効な領域を指さないポインタ、後者は確保したメモリの解放漏れをいいます。いずれも、プログラムの動作に重篤な問題を与えますので、安全なプログラムの開発にはこれらを完全に排除する必要があります。
Rust and WebAssemblyとは、文字通りRustとWebAssemblyの連携のためのオープンソースプロダクトをリリースするワーキンググループです。主なプロダクトは以下の通りです。本稿では、これらを使ってRustとWebAssemblyの連携を紹介していきます。
Rust and WebAssemblyのプロダクトは、多くのプラットフォームで利用可能です。macOS、Linux、Windowsとプラットフォームを選ばずに利用できます。
Rust and WebAssemblyのプロダクトを使うために、以下に挙げるソフトウェアを準備しておきます。
Rustの概要とインストールについては、@ITの連載「基本からしっかり学ぶRust入門」を参考にしてください(主要な言語仕様も同様です)。Node.jsのバージョンは、公式ドキュメントでも特に言及はありませんが、最新版をインストールしておくのがよいでしょう。Node.jsのインストールは、macOS/Windowsともに公式サイトからインストーラを入手して実施してください。本稿では、Node 18.12.1を使用しました。
必要なソフトウェアを準備できたら、基本となるツールであるwasm-packをインストールします。macOS環境では、下記のWebページに記載されているコマンドでターミナルからwasm-packのインストールを実施します。
Windows環境では、wasm-pack-init.exeをダウンロードしてインストールを実施します。
wasm-packのインストールによって、wasm-packコマンドが利用可能になります。-Vオプションを与えてコマンドを実行して、以下のようにバージョンが出力されればwasm-packのインストールは成功です。
% wasm-pack -V wasm-pack 0.10.3
RustのCargoパッケージマネジャーで、テンプレートからプロジェクトを生成可能にするcargo-generateをインストールしておきます。
% cargo install cargo-generate
テンプレートから自動生成される簡単なサンプルを通じて、Rust and WebAssemblyの動作を概観しましょう。まずは、テンプレートからプロジェクトを作成します。
本連載のためのatmarkit_wasmフォルダの中に、プロジェクトrust-wasmをテンプレートwasm-pack-templateから作成します。コマンドを実行するとプロジェクト名を尋ねられますが、ここでは「rust-wasm」を指定しました。同名のフォルダが作成されますので、以降、このフォルダを基点に作業します。
% cargo generate --git https://github.com/rustwasm/wasm-pack-template 🤷 Project Name: rust-wasm プロジェクト名を指定 …略… ✨ Done! New project created …/atmarkit_wasm/rust-wasm
generateサブコマンドを指定してプロジェクトを作成しています。--gitは、GitHubリポジトリにあるプロジェクトを使用するオプションです。
Rustでは、パッケージ名は英数字とハイフンで構成します。このため、例えばアンダースコアを含んだrust_wasmというパッケージ名としたくても、Cargoによってrust-wasmに修正されます。
テンプレートからプロジェクトを作成した時点で、既定のファイルが用意されます。このファイルから直ちに.wasmファイルが生成できますが、Webページとして動作させるにはさらに幾つかの手順が必要です。ですので、ファイルの内容に踏み込む前に、まずはWebページとして動作させて既定の動作を見てみましょう。まずは、プロジェクトのビルドをwasm-packコマンドにbuildサブコマンドを指定して実行します。
% wasm-pack build [INFO]: 🎯 Checking for the Wasm target... …略… [INFO]: 📦 Your wasm pkg is ready to publish at …/atmarkit_wasm/rust-wasm/pkg.
コマンドの実行でpkgフォルダが作成され、.wasmファイルなどの生成物はそこに置かれます。既定では、Webpackなどのバンドラーから利用可能なようにファイルが生成されます。--targetオプションの指定で、以下のようにCommonJSモジュール、ESモジュールとして利用するためのファイル構成にもできます。
wasm-pack build --target nodejs CommonJSモジュールとして構成 wasm-pack build --target web ESモジュールとして構成
次に、生成された.wasmファイルを呼び出すWebページのパッケージを作成します。以下のコマンドで、siteパッケージを作成します(名前は何でもOKです)。
% npm init wasm-app site 🦀 Rust + 🕸 Wasm = ❤
ここで指定されているwasm-appが、Rust and WebAssemblyのプロダクトcreate-wasm-appに対応します。siteフォルダが作成されますので、先ほどのwasm-packコマンドの実行で生成されたpkgフォルダの中身を、siteフォルダに丸ごとコピーします。
% cp -r pkg site/
そして、siteフォルダにあるpackage.jsonファイルに、コピーしたpkgフォルダへの依存関係を追記します(★〜★の内容)。これにより、JavaScriptのimport文で"rust-wasm"が指定されたら、pkgフォルダのパッケージが参照されるようになります。
{ "dependencies": { ★ "rust-wasm": "file:./pkg" }, ★ "name": "rust_wasm", …略…
同じく、siteフォルダにあるindex.jsファイルのimport文も修正します。なお、インポートしたwasmオブジェクトから呼び出しているgreetは、WebAssembly化されたRustによる関数です(具体的な内容は後述します)。
import * as wasm from "rust-wasm"; package.jsonに合わせて修正 wasm.greet();
siteフォルダに移動し、npm installコマンドを実行してモジュールをインストールし、npm run startコマンドでサーバを起動します。
% cd site % npm install % npm run start
Webブラウザからhttp://localhost:8080/にアクセスして、以下のようにポップアップが表示されれば成功です。このポップアップは、Rustのコードから表示されています。
動作確認できたので、ここまでの手順で作成されたファイルを見ていきましょう。
Rustプロジェクトでは、src/lib.rsファイルが重要です。Rustの決まりでは、lib.rsにはライブラリのためのコードを記述します。以下は、テンプレートによって作成されたlib.rsの内容です。
mod utils; (1) use wasm_bindgen::prelude::*; (2) #[cfg(feature = "wee_alloc")] (3) #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; #[wasm_bindgen] (4) extern { (5) fn alert(s: &str); } #[wasm_bindgen] (4) pub fn greet() { (6) alert("Hello, rust-wasm!"); }
(1)は、utilsモジュールを使用するという指定です。utilsモジュールはlib.rsと同じフォルダにあるutils.rsファイルであり、panic発生時にその内容をコンソールに出力する指定が記述されています。panicとはRustのエラーのことで、通常はそのままプログラムが異常終了します。
(2)は、wasm_bindgenというモジュールを使用するという指定です。このモジュールは、RustとJavaScriptを結び付ける役割を持っている、冒頭で紹介したwasm-bindgenのライブラリです。2か所ある(4)のアノテーション(注釈)のために必要となっています。
(3)は、wee_allocというメモリアロケータ(Rustにおけるメモリの動的利用のためのクレート)を使用するための記述です。wee_allocを使うと、Rust既定のメモリアロケータと比較して、生成されるバイナリのサイズをはるかに小さいものとできます。
(4)は、関数をwasm_bindgenの対象とするためのアノテーションです。(5)は、RustからJavaScriptの関数を呼び出すための宣言で、この場合はalert関数を呼び出すことを宣言しています。
(6)は、JavaScriptから呼び出す関数greetの定義です。処理内容として、(5)で宣言したalert関数を呼び出しています。このように、RustとJavaScriptの関数は、相互に呼び出しあうことが可能となっています。
wasm-packコマンドの実行では、以下のファイルがpkgフォルダに作成されます(「rust_wasm」の部分はプロジェクト名で変化する)。記述の通り、パッケージとしてインポートすることが可能な構成になっています。外部から直接利用するのはrust_wasm.jsファイルです。
最後に、HTMLファイルとJavaScriptファイルを見てみます。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hello wasm-pack!</title> </head> <body> <noscript>This page contains webassembly and javascript content, please enable javascript in your browser.</noscript> <script src="./bootstrap.js"></script> (1) </body> </html>
このファイルで重要なのは(1)のみです。bootstrap.jsファイルを読み込んでいるだけです。紛らわしいのですが、このファイルはCSSフレームワークのBootstrapとは無関係です。index.jsを読み込んでエラー時にはメッセージをコンソールに出力する処理だけが記述されています。
import("./index.js") .catch(e => console.error("Error importing `index.js`:", e));
ここで読み込んでいるindex.jsは前節で紹介(修正)しましたが、再掲します。
import * as wasm from "rust-wasm"; (1) wasm.greet(); (2)
(1)では、rust-wasmモジュールを読み込んでいますが、package.jsonファイルに記述した通り、実際にはpkgフォルダの内容が読み込まれます。(2)では、インポートされたwasmオブジェクトによってRustの関数greetを呼び出しています。つまり、必要な処理はこのindex.jsに書き足していけばよいのです。
wasm-bindgenには、Web操作のためのクレートweb_sysが用意されています。これを使うことで、例えばRustのコードからDOMの操作が可能になります。ここでは、Rustで円周率を計算し、それをDOMに反映させる事例を紹介します。
まず、Cargo.tomファイルにweb_sysへの依存関係を[dependencies]セクションの後に追加します。既定で入っている[features]セクションはコメントアウトして無効にします。
…略… # [features] 無効化 # default = ["console_error_panic_hook"] [dependencies] wasm-bindgen = "0.2.63" [dependencies.web-sys] 追加 version = "0.3.4" features = [ 他に使うものがあれば追加 'Window', 'Document', 'HtmlSpanElement', ] …略…
featuresエントリには、Rustのコードから参照する構造体などを追加します。以下は、web_sysクレートのドキュメントです。利用可能な構造体は、ここを参照すれば分かります。ここでは、Window、Document、HtmlSpanElementの3つを使うので、これらを列挙しましたが、他に使うものがあれば追加します。
src/lib.rsファイルに、円周率を計算してDOMに反映させる関数を追加します。円周率は、ライプニッツの公式で計算しています(アルゴリズムの詳細は省略します)。
…略… use web_sys::HtmlSpanElement; (1) …略… #[wasm_bindgen] pub fn calc_pi(count: u32) { // 円周率の計算 let mut i = 1_u32; (2) let mut value = 1.0_f64; while i <= count { let index = 2 * i - 1; value -= 1.0 / ((2 * index + 1) as f64); value += 1.0 / ((2 * (index + 1) + 1) as f64); i += 1; } value *= 4.0; // DOMへの設定 let window = web_sys::window().unwrap(); (3) let document = window.document().unwrap(); let span = document.get_element_by_id("pi").unwrap(); let span = span.dyn_ref::<HtmlSpanElement>().unwrap(); span.set_inner_html(&value.to_string()); }
(1)は、HtmlSpanElement構造体を使うためのuse文です。JavaScriptでは、HTML要素のオブジェクトの型は動的に解釈されますが、Rustでは静的な解釈が必要なため、実際の要素に合わせた構造体の型を使用しなければなりません。
(2)は、ライプニッツの公式による円周率の計算です。関数calc_piの引数countで計算回数を受け取り、その回数だけ計算を実行して結果をvalueにセットします。
(3)以降は、DOMへの設定です。順番に、Windowオブジェクト、Documentオブジェクト、Elementオブジェクト(実際にはHtmlSpanElementオブジェクト)を取得し、set_inner_html関数で文字列化した計算結果をspan要素に設定しています。
この時点で、既述の手順の通り、wasm-pack buildコマンドの実行、pkgフォルダのsiteフォルダへのコピー、siteフォルダでのnpm installコマンドの実行を、それぞれ済ませておきます。
最後に、Rustの関数を呼び出すJavaScriptファイルと、計算結果を書き戻すHTMLファイルを編集します。
…略… const count = prompt('計算回数は?'); wasm.calc_pi(count);
…略… <body> <div>計算した円周率: <span id="pi">???</span></div> …略…
index.jsファイルでは、JavaScriptのprompt関数で計算回数を入力させていますが、フォームで入力させてイベント処理でcalc_pi関数を呼び出すようにしてもよいでしょう。index.htmlファイルに記述したspan要素のidは、calc_pi関数から呼び出しているget_element_by_id関数の引数に対応していることに注意してください。
siteフォルダでnpm run startコマンドを実行し、Webブラウザでhttp://localhost:8080/にアクセスして、以下のように計算結果が表示されれば成功です。
今回は、wasm-packを使用したRustによるWebAssemblyプログラムの開発を紹介しました。
次回は、同じくネイティブコンパイラ言語であるGoを用いたWebAssemblyプログラムの開発を紹介します。
WINGSプロジェクト
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティー(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手掛ける。2021年10月時点での登録メンバーは55人で、現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
・サーバーサイド技術の学び舎 - WINGS(https://wings.msn.to/)
・RSS(https://wings.msn.to/contents/rss.php)
・Twitter: @yyamada(https://twitter.com/yyamada)
・Facebook(https://www.facebook.com/WINGSProject)
Copyright © ITmedia, Inc. All Rights Reserved.