RustとWebAssemblyによるゲーム開発を読み進めたらつまづきまくったので備忘録
findyのoreilly learningプラットフォームが90日無料で試せるヤツの抽選に当たってました
1ヶ月もそれに気付かず放置していたら、findyさんの方から「抽選当たってるでー気づいてー」とメッセージが来たので早速試しています(findyさんごめんなさいありがとう)
手始めに、諸事情で序盤以降が読めなくなってしまっていた
RustとWebAssemblyによるゲーム開発
という本を読み進めています
本の扱っている内容が発展途上の分野な為変化が早く、ツールの使い方などは特に詰まりやすいなと思ったので備忘録として残すことにしました
読み進めていく際詰まるところが出てくると思うので、記事の方もその都度更新していく方針で書いていきます
[!caution]
以下、2025/10時点での情報です
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,>;
コレ要る?って言われると微妙だけどこう言う細かいのが大事って信じてる うん
Discussion