RustとWebAssemblyによるゲーム開発を読み進めたらつまづきまくったので備忘録
findyのoreilly learningプラットフォームが90日無料で試せるヤツの抽選に当たってました
1ヶ月もそれに気付かず放置していたら、findyさんの方から「抽選当たってるでー気づいてー」とメッセージが来たので早速試しています(findyさんごめんなさいありがとう)
手始めに、諸事情で序盤以降が読めなくなってしまっていた
RustとWebAssemblyによるゲーム開発
という本を読み進めています
本の扱っている内容が発展途上の分野な為変化が早く、ツールの使い方などは特に詰まりやすいなと思ったので備忘録として残すことにしました
読み進めていく際詰まるところが出てくると思うので、読み進めながら記事の方もその都度更新していく方針で書いております
[!caution]
- 以下、2025/10時点での情報です
- 想定するメイン読者は2025/10/01時点での自分です
1章
rustupの代わりにnixを使っている場合の注意点
自分は fenix というoverlayを利用してrustのツールチェーンをインストールしています
普通にインストールしているとcargoひいてはrustcコマンドのターゲットはホストプラットフォームになっているかと思います
たとえば自分は、apple siliconのmacなので
❯ rustc -v -V
rustc 1.92.0-nightly (dc2c3564d 2025-09-29)
binary: rustc
commit-hash: dc2c3564d273cf8ccce32dc4f47eaa27063bceb9
commit-date: 2025-09-29
host: aarch64-apple-darwin # ←これがデフォルトのターゲット
release: 1.92.0-nightly
LLVM version: 21.1.2
となっています
wasm(具体的に、今回の場合はwasm32-unknown-unknown)をターゲットにビルドする際、
rustupを使っている場合は単純にrustupコマンド経由でwasm32-unknown-unknownをインストールすれば良いのですが、rustupを使わない場合は別の方法でターゲットプラットフォームをインストールする必要があります
自分の場合はdevshellを利用して環境構築をしているので、flake.nixを以下のように設定します
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils";
fenix.url = "github:nix-community/fenix";
fenix.inputs.nixpkgs.follows = "nixpkgs";
};
outputs =
{
self,
nixpkgs,
flake-utils,
fenix,
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
fx = fenix.packages.${system};
toolchain = fx.combine [
fx.latest.rustc
fx.latest.cargo
fx.latest.rustfmt
fx.latest.clippy
fx.latest.rust-src
fx.targets.wasm32-unknown-unknown.latest.rust-std
];
in
{
devShells.default = pkgs.mkShell {
buildInputs = [
pkgs.nodejs_24
pkgs.wasm-pack
toolchain
];
shellHook = ''
echo -e "\033[1;32m\nRust + WASM dev environment loaded"
echo -e "System: ${system}"
echo -e "wasm-pack: $(which wasm-pack 2>/dev/null || echo 'not found')"
echo -e "rustc: $(which rustc 2>/dev/null || echo 'not found')"
echo -e "cargo: $(which cargo 2>/dev/null || echo 'not found')\033[0m\n"
'';
};
}
);
}
[!tip]
shellHookで実行している echo 内に\033[1 ... と見慣れぬコードがありますが、これはansi escape
sequence というものです
ansi escape sequence自体の説明は
https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences
↑のリンクの回答がわかりやすいです
fx.combineに渡される配列内で示されたコンポーネントをひとまとまりのツールチェーンとして利用できる(という理解なんですけど合ってる?)
ので、あとはtoolchainをbuildInputsに指定してやればwasm32-unknown-unknown問題は解決します
shellでrustcのパスを確認してみましょう
❯ wh rustc
rustc is /nix/store/nixqji36ybcpjipvq5xh17i9zimmn58a-rust-mixed/bin/rustc
rustc is /Users/a/.nix-profile/bin/rustc
devshellのパスが先に来てたらOKです
稀にneovimのterminalバッファを使っているとグローバルな方が先に来てしまうのですが、
その状態でnpm run startすると
Error: wasm32-unknown-unknown target not found in sysroot: "/nix/store/7qqwr576zn72lib96j4kig162agh4cb9-rust-nightly-latest-2025-09-30"
Used rustc from the following path: "/Users/a/.nix-profile/bin/rustc"
It looks like Rustup is not being used. For non-Rustup setups, the wasm32-unknown-unknown target needs to be installed manually. See https://rustwasm.github.io/wasm-pack/book/prerequisites/non
-rustup-setups.html on how to do this.
というエラーが出るので、devshellをリロードしましょう
自分の場合はdirenvを使っているので一旦ディレクトリを抜けて戻ると解決します
テンプレートが古い
本の中でも言及されていますが、テンプレートがだいぶ古くなっています
npm init rust-webpackで生成されるCargo.tomlのエディションはなんと2018..
本の中でも軽くアップデートしていますが、それでもまだ古いのでエラーが出てしまいました
ということで、本腰を入れて更新作業をする必要があります
目標
ブラウザのコンソールで"Hello World!"と出力されているのを確認する
因みに、npm init rust-webpackで生成されるsrc/lib.rsは↓です
use wasm_bindgen::prelude::*;
use web_sys::console;
// When the `wee_alloc` feature is enabled, this uses `wee_alloc` as the global
// allocator.
//
// If you don't want to use `wee_alloc`, you can safely delete this.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
// This is like the `main` function, except for JavaScript.
#[wasm_bindgen(start)]
pub fn main_js() -> Result<(), JsValue,> {
// This provides better error messages in debug mode.
// It's disabled in release mode so it doesn't bloat up the file size.
#[cfg(debug_assertions)]
console_error_panic_hook::set_once();
// Your code goes here!
console::log_1(&JsValue::from_str("Hello world!",),);
Ok((),)
}
Rust側
実はRust側の更新はそんなに大変ではありません
Cargo.tomlをいじるだけです
自分の場合は↓な感じにしました
# You must change these to your own details.
[package]
name = "rust-webpack-template"
version = "0.1.0"
authors = ["You <you@example.com>"]
categories = ["wasm"]
edition = "2024"
readme = "README.md"
description = "My super awesome Rust, WebAssembly, and Webpack project!"
[lib]
crate-type = ["cdylib"]
[profile.release]
# This makes the compiled code faster and smaller, but it makes compiling slower,
# so it's only enabled in release mode.
lto = true
[features]
# If you uncomment this line, it will enable `wee_alloc`:
# default = ["wee_alloc"]
[dependencies]
# The `wasm-bindgen` crate provides the bare minimum functionality needed
# to interact with JavaScript.
wasm-bindgen = "*"
console_error_panic_hook = "*"
# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
# compared to the default allocator's ~10K. However, it is slower than the default
# allocator, so it's not enabled by default.
wee_alloc = { version = "*", optional = true }
# The `web-sys` crate allows you to interact with the various browser APIs,
# like the DOM.
[dependencies.web-sys]
version = "*"
features = ["console"]
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so it's only enabled
# in debug mode.
# These crates are used for running unit tests.
[dev-dependencies]
futures = "*"
js-sys = "*"
wasm-bindgen-futures = "*"
wasm-bindgen-test = "*"
js(ツール)側
こちらは大変です..というか大変でした
が、結局やる事としては
- package.jsonの修正
- webpack.config.jsの修正
の二つです
package.jsonの修正
{
"author": "You <you@example.com>",
"name": "rust-webpack-template",
"version": "0.1.0",
"scripts": {
"build": "webpack --mode production",
"start": "webpack serve --open --mode development",
"test": "cargo test && wasm-pack test --headless"
},
"devDependencies": {
"@wasm-tool/wasm-pack-plugin": "*",
"copy-webpack-plugin": "^5.0.3",
"webpack": "^5.102.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "*"
}
}
正直nodeの使い方とかwebpack側の事情とか知ったこっちゃなので荒い修正なのは自覚しています
とりあえずこれで動くからええねんの精神で自分に言い訳していますが、指摘などあったら教えてください
涎垂らして喜びます
修正点としてはwebpackパッケージのアップデート(テンプレだと4系なので5系に)とそれに伴ったビルドコマンドの変更です
webpack.config.js
const path = require("path");
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
module.exports = {
mode: "development",
entry: "./js/index.js",
// 必要
output: {
path: path.resolve(__dirname, "dist"),
filename: "main.js",
publicPath: "auto",
clean: true,
},
experiments: {
asyncWebAssembly: true,
topLevelAwait: true,
},
module: {
rules: [{ test: /\.wasm$/, type: "webassembly/async" }],
},
resolve: { extensions: [".js", ".wasm"] },
// 必要
devServer: {
static: path.join(__dirname, "static"),
hot: true,
port: 8081,
},
devtool: "source-map",
plugins: [
new WasmPackPlugin({
crateDirectory: path.resolve(__dirname),
outDir: path.resolve(__dirname, "pkg"),
extraArgs: "--target bundler",
forceMode: "development",
}),
// 必要
new CopyWebpackPlugin([
{ from: path.resolve(__dirname, "static"), to: path.resolve(__dirname, "dist") },
]),
],
};
こちらもと・動・え精神でごちゃごちゃを許容しています
指摘などあったら(ry
保証はできませんが、↑で必要とコメントされているブロックに関しては最低限必要な設定かと思われます
こいつらがないとビルドアーティファクトをちゃんと使ってくれなくなります
確認してみよう
ここまで修正すれば、ブラウザのコンソールにconsole::log_1へ渡した値が出力されているかと思います
されていない場合はwasmを読み込むjs側のエントリーポイントで、生成されたスクリプトをロードしているか確認しましょう
import "../pkg/index.js";
webpack.config.jsを再掲すると、自分は↓のようにjs/index.jsをエントリーポイントに指定しています
const path = require("path");
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
module.exports = {
mode: "development",
entry: "./js/index.js",
// 必要
output: {
path: path.resolve(__dirname, "dist"),
...
個人的tips
rustで外部クレートを利用する際によくお世話になる docs.rs
ですが、実はtype based searchができます
例えば、web_sysクレートで
&JsValue -> HtmlCanvasElement
と検索すると↓のような結果が得られます
意外と知られてないこの機能ですが、🔍(Search)ボタンの右にある?(Help)ボタンをおすとガッツリ紹介されています
結構柔軟にクエリが書けて便利です
docs.rsを徘徊する時に記憶の端っこにあるとオイシイかもしれません
ランダムな数取得
ランダムな数を取得するために rand
クレートを利用していますが、そのままでwasm32-unknown-unknown向けにビルドするとエラーが出ます
error: The wasm32-unknown-unknown targets are not supported by default; you may need to enable the "wasm_js" configuration flag. Note that enabling the `wasm_js` feature flag alone is insufficient. For more information see: https://docs.rs/getrandom/0.3.3/#webassembly-support
--> /Users/a/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.3.3/src/backends.rs:168:9
|
168 | / compile_error!(concat!(
169 | | "The wasm32-unknown-unknown targets are not supported by default; \
170 | | you may need to enable the \"wasm_js\" configuration flag. Note \
171 | | that enabling the `wasm_js` feature flag alone is insufficient. \
172 | | For more information see: \
173 | | https://docs.rs/getrandom/", env!("CARGO_PKG_VERSION"), "/#webassembly-support"
174 | | ));
| |__________^
error[E0425]: cannot find function `fill_inner` in module `backends`
--> /Users/a/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.3.3/src/lib.rs:99:19
|
99 | backends::fill_inner(dest)?;
| ^^^^^^^^^^ not found in `backends`
error[E0425]: cannot find function `inner_u32` in module `backends`
--> /Users/a/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.3.3/src/lib.rs:128:15
|
128 | backends::inner_u32()
| ^^^^^^^^^ not found in `backends`
|
help: consider importing this function
|
33 + use crate::util::inner_u32;
|
help: if you import `inner_u32`, refer to it directly
|
128 - backends::inner_u32()
128 + inner_u32()
|
error[E0425]: cannot find function `inner_u64` in module `backends`
--> /Users/a/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.3.3/src/lib.rs:142:15
|
142 | backends::inner_u64()
| ^^^^^^^^^ not found in `backends`
|
help: consider importing this function
|
33 + use crate::util::inner_u64;
|
help: if you import `inner_u64`, refer to it directly
|
142 - backends::inner_u64()
142 + inner_u64()
|
これはrandが依存するgetrandomクレート由来のエラーですね
本書ではCargo.tomlでgetrandomのjsフィーチャーを有効にすればええねんで、って書いてますがこれは古い情報です
じゃあどうすれば良いの?っていうとエラーメッセージ内のリンク
が正しく求めていたものなんですが、見落としやすい箇所があるのでめも
getrandomクレートの設定
要は以下の2つを両方とも設定すると解消します
- "wasm_js"フィーチャーを設定する
- wasm32-unknown-unknownターゲットにビルドする際はgetrandom_backendフラグを"wasm_js"にする
1番はそのまま
getrandom = { version = "*", features = ["wasm_js"] }
としてやればおk
個人的には結構びっくりなんですが、利用クレートが依存してるクレートの設定って自身のCargo.tomlから弄れるんですね
まぁそれが出来ないと困るケースがあると言うのは想像に難くないというか正しく↑のケースが該当しますよね
多分一番驚いているのは、その設定の仕方だと思います
randクレートが依存しているgetrandomの設定は~~って言う書き方なら分かるんですが、なんの縛りも無しにグローバル?にボンっと置いちゃって大丈夫なんでしょうか
菱形依存してる時とかどうするんだ..?余談終わり
2番は、コマンドを走らせる際に逐一RUST_FLAG環境変数を設定する方法と、.cargo/config.tomlに設定する方法があります
正直後者を差し置いて前者を選ぶ利点が今回のケースでは思い当たらないので後者のみ書き残します
Cargo.tomlが存在するディレクトリから見て.cargo/config.tomlにあたるファイルを作成・編集すれば良いわけです
内容は
# It's recommended to set the flag on a per-target basis:
[target.wasm32-unknown-unknown]
rustflags = ['--cfg', 'getrandom_backend="wasm_js"']
wasm32-unknown-unknownをターゲットにビルドする際のみ指定のフラグを有効にしますと言う設定です
[!tip]
neovimを使っている方向けの小ネタですが、例えば.cargo/ディレクトリが存在しない状態でnvim .cargo/config.tomlとすると、保存の際にエラーが出てしまいます
これは、.cargo/ディレクトリが存在しないにも関わらず、.cargo/配下にファイルを保存しようとしている為です
もうちょっと柔軟に対応してくれこの場合:write ++pと、
writeコマンドに++pオプションを指定してやれば、必要に応じてディレクトリの作成を行なってくれます
2章
spawn_localが引数にasyncブロックを受け付けない
これは進め方によって出ないこともあるかと思います
自分の場合は、本に沿って実装を進め、wasm_bindgen_futures::spawn_localの引数にasyncブロックを渡すと
• the trait bound `{async block@src/lib.rs:292:36: 292:46}: futures::future::Future` is not satisfied
the following other types implement trait `futures::future::Future`:
&'a mut F
futures::future::and_then::AndThen<A, B, F>
futures::future::catch_unwind::CatchUnwind<F>
futures::future::either::Either<A, B>
futures::future::empty::Empty<T, E>
futures::future::flatten::Flatten<A>
futures::future::from_err::FromErr<A, E>
futures::future::fuse::Fuse<A>
and 44 others [E0277]
lib.rs:291:1: required by a bound introduced by this call
legacy_shared.rs:100:7: required by a bound in `wasm_bindgen_futures::spawn_local`
と言うエラーが出てしまいました
先に対症療法を書くと、このようなエラーが出た場合は
[dependencies.wasm-bindgen-futures]
version = "*"
新しいバージョンのwasm-bindgen-futures(おそらく0.4系以上)を使っていることを確認した上で
Cargo.lockを削除すれば治るはず
原因(多分)
これだけだと自分が気持ち悪いので、後付けですがエラーが出た原因を推測してみます
まずエラーが出ている時のCargo.lockには以下のような記述があります
[[package]]
name = "wasm-bindgen-futures"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83420b37346c311b9ed822af41ec2e82839bfe99867ec6c54e2da43b7538771c"
dependencies = [
"cfg-if 0.1.10",
"futures 0.1.31",
"js-sys",
"wasm-bindgen",
"web-sys",
]
wasm-bindgen-futuresのバージョンが0.3.27になっています
この状態でhover docしてwasm_bindgen_futures::spawn_localのシグネチャを確認すると
pub fn spawn_local<F>(future: F)
where
F: Future<Item = (), Error = ()> + 'static,
となっています
一方、asyncブロックの型は

なんか見難いですが
impl Future<Output = ()>
になっています
asyncブロックは言語機能として提供されているわけなのでcore::future::Futureトレイトを実装します
一方
wasm_bindgen_futuresのバージョン0.3.27時点ではspawn_localの引数は以下の通り
バージョン0.1.28のfuturesトレイトが提供するfutures::future::Futureトレイトを実装した型、になっています
ここがミスマッチしていた為エラーになっていたわけですね
憶測ですが、wasm_bindgen_futuresのバージョン0.3.27がリリースされて以降に非同期Rustの言語使用に変更が入り、
その影響を受けてwasm_bindgen_futuresクレートはcore::future::Futureに準拠するようになった、って事があったんだと思います(適当)
[!tip]
またまた余談です
docs.rsでは過去バージョンのドキュメントを読む事ができます
set_onloadに渡すクロージャーの引数
つまづいた事では無いのですが、気になったのでやってみた!と言う節です
本ではset_onloadとset_onerrorに対して別々のコールバックを用意しています
oneshotチャンネルをMutexで共有するという非同期Rustの導入としてぶち込んだものと思われますが、この2つのコールバックは1つに纏められます
と言うのもMDNを見ていて気になったのですが、
onloadとonerrorにあたるコールバックの引数(JS側の話です)がどちらもEventオブジェクトになっています
UI由来のエラーの場合はErrorEventやでーって書いてありますが、今回はそのケースでは無いので実は2つのコールバックのシグネチャは同じことがわかります
シグネチャが同じなら纏められるのでは?と思って試してみました↓
let (tx, rx,) = futures::channel::oneshot::channel::<JRslt<(),>,>();
let callback = Closure::once(|e: Event| {
console::log_1(&e,);
if e.type_() == "load" {
tx.send(Ok((),),)
.expect("failed to send success message from callback",)
} else {
tx.send(Err(e.into(),),)
.expect("failed to send error message from callback",)
};
},);
let image = HtmlImageElement::new()?;
// set callback when loading asset finished
image.set_onload(Some(callback.as_ref().unchecked_ref(),),);
image.set_onerror(Some(callback.as_ref().unchecked_ref(),),);
image.set_src("Idle (1).png",);
match rx.await {
Ok(sent_msg,) => match sent_msg {
Ok(_,) => {
console_log!("load success");
ctx.draw_image_with_html_image_element(
&image, 0.0, 0.0,
)?
},
Err(e,) => {
console_log!("error happen while loading asset");
console::error_1(&e,)
},
},
Err(e,) => {
let e = JsValue::from_str(&e.to_string(),);
console_log!(
"error happen while sending message from callback"
);
console::error_1(&e,);
},
};
Event型を利用する為にはweb_sysのEventフィーチャーを有効にする必要があります
[dependencies.web-sys]
version = "*"
features = [
"console",
"Window",
"Document",
"Element",
"HtmlCanvasElement",
"CanvasRenderingContext2d",
"HtmlImageElement",
"Event", # ←コレ!!
]
コールバックの最初でe引数をコンソールに表示しています
web_sys::console::log_1が受け付ける引数の型は&JsValueですが、&Eventを渡しています
コレがなぜ許されるのかというと、Event型がDeref<Target = JsValue>を実装しているからです
丁度&Stringが&strと見なされるのと同じです
String型はDeref<Target = str>を実装している為、String型の参照はstr型の参照、つまり&strとしても解釈することが出来ます
そしてEvent型はDeref<Target = JsValue>を実装している為、その参照を&JsValueとして渡すことが出来ます
ifの部分でe.type_() == "load"とあります
if e.type_() == "load" {
tx.send(Ok((),),)
.expect("failed to send success message from callback",)
} else {
tx.send(Err(e.into(),),)
.expect("failed to send error message from callback",)
};
コレはjsにおける
e.type === "load"
と同じです
とまぁ偉そうに解説面をしてきましたが、
試しにコンソールにeを出力するだけのコードを実行したところ
[Log] Event (main.js, line 3999)
bubbles: false
cancelBubble: false
cancelable: false
composed: false
currentTarget: null
defaultPrevented: false
eventPhase: 0
isTrusted: true
returnValue: true
srcElement: <img>
target: <img>
timeStamp: 793
type: "load"
と言う出力を得たのでそれを元に書いたと言うだけ
into_serdeの循環参照
2章の後半ではserdeを使ってjsのオブジェクト(の表現)からjsonを解析するくだりがあります
本では、wasm-bindgenのフィーチャーを追加し
[dependencies.wasm-bindgen]
version = "*"
features = ["serde-serialize"]
into_serdeメソッドを使ってdeしています
ですが訳註の3にもある通り、現在into_serdeを循環参照を引き起こす為非推奨となっています
訳註には代替としてgloo-utilsクレートを使う方法が提示されていますが、rust-analyzerのエラー出力を見た感じgloo-utilsを使うよりserde_wasm_bindgenクレートを使った方が現時点ではオススメな方法なのかなと思います
serde_wasm_bindgenを使ってデシリアライズ
まず、Cargo.tomlのwasm-bindgenの設定はそのままにしておきましょう
dependenciesテーブルにserde-wasm-bindgenを追加すれば導入は完了です
多分ここまでで気になってる人も多いかと思うのですが、自分が個人的に開発する際は基本的に全て最新の物を使うようにしています
いつも脳死でバージョンにワイルドカード指定しているので正直コレ以外の表記をあんまり知らない
serde-wasm-bindgen = "*"
[dependencies.wasm-bindgen]
version = "*"
デシリアライズする際はinto_serdeの代わりにserde_wasm_bindgen::from_valueを使います↓
// 例
async fn fetch_sprite_sheet_mapper(json_path: &str,) -> JRslt<Sheet,> {
let json = fetch_json(json_path,).await?;
let sheet: Sheet = serde_wasm_bindgen::from_value(json,).expect(
"failed to parse js object representation into rust struct: `Sheet`",
);
Ok(sheet,)
}
因みに↑のコード例で出てくるJRsltという型名、↓のように定義しています
type JRslt<T,> = Result<T, JsValue,>;
コレ要る?って言われると微妙だけどこう言う細かいのが大事って信じてる うん
3章
この辺りから環境要因のエラーは落ち着きます
やっとコーディングに集中できるぜ!
とは言いつつ杉下右京なら気になるだろう点がちらほらあるので、そいつらを拾い上げて紹介します
anyhowクレートの使い方
3章序盤でanyhowクレートが導入され、エラーハンドリングの部分のリファクタをします
筆者の言う通り、anyhowクレートは必要十分な機能のみを提供する小さな(でも画期的)クレートです
ですが意外と知られていないanyhowクレートの機能があったりするので紹介します
まず、OptionをResultに変換する際、本では
pub fn window() -> Result<Window> {
web_sys::window().ok_or_else(|| anyhow!("No Window Found"))
}
としています
勿論これで何も問題ないのですが、以下のようにも書けます
// `use anyhow::Result as Rslt;` しています
fn window_obj() -> Rslt<Window,> {
web_sys::window().context("no window object found",)
}
contextメソッドはanyhow::Contextトレイトで定義されているメソッドです
anyhowの方でOption、Result型に実装してくれているので、anyhow::Contextをインポートすれば↑の様に使えます
個人的な使い分けですが、"{}"なマクロ特有の機能(コレ、interpolationって言うんですね
最近知りました)が使いたい時はmapなりok_orなりを使い、そうでない時は後者を使っています
Closure::onceのシグネチャ
browserモジュール内にClosure::onceのラッパーを作る際、本では
pub fn closure_once<F, A, R>(fn_once: F) -> Closure<F::FnMut>
where
F: 'static + WasmClosureFnOnce<A, R>,
{
Closure::once(fn_once)
}
としていますが、エラーが出てしまいます
例に漏れず新しいバージョンにすると出るエラーと思われます
修正しましょう
結論
忙しい人向けのコピペ
use wasm_bindgen::closure::WasmClosure;
use wasm_bindgen::closure::WasmClosureFnOnce;
pub fn closure_once<F, A, R, T,>(fn_once: F,) -> Closure<T,>
where
F: WasmClosureFnOnce<T, A, R,>, // + 'static,
T: WasmClosure + ?Sized, // + 'static
{
Closure::once(fn_once,)
}
コピペとか用意しなくても ご活用くださいClosure::onceのシグネチャがまんま答えなんだよね..
解剖
まず最初に困るだろう点は、WasmClosureFnOnceのインポートかと思います
エディタの変換に出てこないので心配になります
とりあえず実装を見てみましょう
現時点でwasm-bindgenの最新バージョンは0.2.104なので0.2.104リリース時のClosureの実装周りを見てみます
こちらは0.2.104リリース時のリポジトリ全体です
Closureの実装はsrc/closure.rsにあります
src/closure.rsの内、Closure::onceと関係のある部分がこちら
pub struct Closure<T: ?Sized> {
js: ManuallyDrop<JsValue>,
data: OwnedClosure<T>,
}
impl<T> Closure<T>
where
T: ?Sized + WasmClosure,
{
pub fn wrap(data: Box<T>) -> Closure<T> {
let data = OwnedClosure {
inner: ManuallyDrop::new(data),
};
Self {
js: ManuallyDrop::new(crate::__rt::wbg_cast(&data)),
data,
}
}
pub fn once<F, A, R>(fn_once: F) -> Self
where
F: WasmClosureFnOnce<T, A, R>,
{
Closure::wrap(fn_once.into_fn_mut())
}
}
#[doc(hidden)]
pub trait WasmClosureFnOnce<FnMut: ?Sized, A, R>: 'static {
fn into_fn_mut(self) -> Box<FnMut>;
fn into_js_function(self) -> JsValue;
}
#[doc(hidden)]
pub unsafe trait WasmClosure: WasmDescribe {
const IS_MUT: bool;
}
お目当てのWasmClosureFnOnceトレイトにdoc(hidden)と言う属性が付与されています
これがついているとdocs.rsでドキュメントが生成されなかったり、rust-analyzerの補完候補に出なくなったりするんですって
あら便利
ただパブリックに宣言されているのでuse wasm_bindgen::closure::WasmClosureFnOnce;によるインポートは問題ない、と言う訳です
WasmClosureトレイトについても同様
こちらはおどろおどろしくもunsafe修飾されてますが、トレイトに付くunsafeは用法がちょっと特殊なので今回の様にインポートする分には問題ありません
次に、結局closure_onceのシグネチャはどうすればいいの?問題
まず整理すると、ラッパーなのでClosure::onceと同じシグネチャにしたいと言う前提があります
↑のコードをそのまま転用すると
pub fn closure_once<F, A, R>(fn_once: F) -> Self
where
F: WasmClosureFnOnce<T, A, R>,
{
Closure::once(fn_once)
}
ほぼ完成形ですが、ジェネリック型変数TやSelfはimpl Closureの文脈外では通じません
こいつらが何を指してるのかを書き下ろせば完成です
// 再掲
use wasm_bindgen::closure::WasmClosure;
use wasm_bindgen::closure::WasmClosureFnOnce;
pub fn closure_once<F, A, R, T,>(fn_once: F,) -> Closure<T,>
where
F: WasmClosureFnOnce<T, A, R,>,
T: WasmClosure + ?Sized,
{
Closure::once(fn_once,)
}
F,Tのライフタイムにstaticを指定していないですが、どうやらWasmClosureFnOnceの制約自体に'staticがある様ですし、今の所指定しなくてもコンパイル通っているので無くてもいっか、と言うお気持ち
Closureのジェネリック引数の指定
3章終盤でキーボードイベントの処理を実装します
手始めにprepare_input関数を実装していくのですが、1つ詰まった点がありました
本では、キーボードイベントが発火した際のコールバックを以下のように定義しています
fn prepare_input() {
let onkeydown = browser::closure_wrap(
Box::new(move |keycode: web_sys::KeyboardEvent| {})
as Box<dyn FnMut(web_sys::KeyboardEvent)>);
let onkeyup = browser::closure_wrap(
Box::new(move |keycode: web_sys::KeyboardEvent| {})
as Box<dyn FnMut(web_sys::KeyboardEvent)>);
...
本の通りにclosure_wrapを実装している場合はこれで何も問題はありません
自分の場合、代わりにClosure::newのラッパーであるclosure_newを実装していました
pub fn closure_new<F, T,>(f: F,) -> Closure<T,>
where
F: IntoWasmClosure<T,> + 'static,
T: WasmClosure + ?Sized,
{
Closure::new(f,)
}
先程のコードの一部を抜き出してclosure_wrapの代わりにclosure_newを使ってみます
取り敢えず、型の辻褄だけ合わせてみます
let onkeydown = browser::closure_new(
move |keycode: web_sys::KeyboardEvent| {}
as dyn FnMut(web_sys::KeyboardEvent));
すると以下の様なエラーが出ます
• cast to unsized type: `dyn FnMut(KeyboardEvent)` [E0620]
• consider using a box or reference as appropriate [E0620]
engn.rs:261:60: original diagnostic
要はキャスト先がdyn FnMutだとコンパイル時に型の大きさが確定しないから参照にするかBoxに包んでねと言っています
このエラーへの対処法として素直にclosure_wrapを実装するのが読み進め方としては一番ストレートかなと思います
自己流を貫きたい場合でも修正は簡単↓(導き出すのは大変でした..力が..欲しい..)
let onkeydown = brwsr::closure_new::<_, dyn FnMut(KeyboardEvent,),>(
|keycode: KeyboardEvent| {},
);
そもそも型の指定が必要な理由はなんでしょう
それは引数として渡されるクロージャがFnなのかFnMutなのか判別がつかないからです
Closure<T>のジェネリック変数Tは、Rustにおける関数の型を表現しているので、コイツ経由でFnMutであることを指定すれば良さそうです
closure_newの文脈では、関数の第2ジェネリック引数に、渡すクロージャの型情報を指定できます
pub fn closure_new<F, T,>(f: F,) -> Closure<T,>
これらを愚直に反映すると↑のコードになります
4章
strumクレートの利用
4章ではステートマシーンパターンを実装するためにRustの列挙型をフル活用します
列挙型を使う際に便利なstrumと言うクレートがあり個人的にかなり愛用しているのでその紹介です✨
strumクレートはREADMEに書いてある様に列挙型と文字列を扱う際に便利な機能を提供してくれるクレートです
deriveマクロを利用する場合は"derive"フィーチャーを有効にします
[dependencies.strum]
version = "*"
features = ["derive"]
ここではstrum::Displayマクロを使ってみます
このマクロは列挙子の名前を文字列として取得する機能を提供してくれます
以下のRustコードを実行すると
#[derive(strum::Display,)]
enum QwQ {
A,
B(String,),
#[strum(to_string = "C has {x}")]
C {
x: i32,
},
#[strum(to_string = "technically, E")]
D,
}
fn main() {
let a = QwQ::A;
let b = QwQ::B("wth".to_string(),);
let c = QwQ::C { x: 666, };
let d = QwQ::D;
println!("a: {a}");
println!("b: {b}");
println!("c: {c}");
println!("d: {d}");
}
↓と出力されます
❯ cargo run --example strum -q
a: A
b: B
c: C has 666
d: technically, E
strum::Displayの大体のノリが伝わったかなと思います
自分の実際の使い所としては
#[derive(Clone, Copy, strum::Display,)]
enum RedHatBoyStateMachine {
Idle(RedHatBoyState<Idle,>,),
#[strum(to_string = "Run")]
Running(RedHatBoyState<Running,>,),
}
impl RedHatBoy {
...
fn draw(&self, rndrr: &Renderer,) -> Rslt<(),> {
let frame_name = format!(
"{} ({}).png",
self.state_machine, // ココで自動的に`strum::Display`の機能が使われる `std::fmt::Display`みたいなもんです
(self.state_machine.frame() / 3) % 24 + 1 // 個人的にフレームの扱いにマイナーチェンジを入れてるためこうなってます キニシナイデ
);
...
}
}
の様に使っています
enumの定義部分がごちゃっとしちゃいますが、ここはトレードオフ
自分は実装の楽さとシンプルさを優先しました
strumクレート、お勧めです

Discussion