Claude Media
Claude CodeでGodotのGDExtensionを作る — C++とRustのビルドエラーをhookで返す

Claude CodeでGodotのGDExtensionを作る — C++とRustのビルドエラーをhookで返す

godot-cppとgodot-rustのビルド失敗をPostToolUse hookでClaudeに返し、編集のたびに直させる設定を、.gdextensionの落とし穴と合わせて書きます。

GDExtensionは、C++やRustで書いたコードを動的ライブラリとしてGodotに読み込ませる仕組みです。GDScriptと違い、書き換えた結果はコンパイルを通るまで確かめられません。Claude Codeに任せるなら、ビルドが落ちたことをClaudeが自分で知る経路が要ります。

その経路がPostToolUse hookです。ファイルを編集するたびにビルドを走らせ、失敗したら出力の末尾をClaudeに返します。この記事では、godot-cpp(C++)とgodot-rust(Rust)の2系統について、hookの組み方と、.gdextensionファイルまわりの見落としやすい点をまとめます。

先にビルドが落ちたと知らせる仕組みを置く

GodotプロジェクトのGDScript側は、エディタを開かずにgodot --headlessで確かめられます。手順はClaude CodeでGodotゲーム開発を始める手順にまとめています。GDExtensionはその手前に、ネイティブのビルドという関門が増えます。

Claudeはファイルを書いた直後に、ビルドが通ったかを知りません。放っておくと、壊れたままの.cppや.rsの上に次の変更が積まれます。エラーは数ファイル後にまとめて出て、どの編集が原因か切り分けにくくなります。

そこでhookを使います。hooksリファレンスによると、PostToolUseは「ツール呼び出しが成功した直後」に走るイベントです。matcherにEdit|Writeを書けば、ファイル編集の直後だけを狙えます。

仕組み

hookで返せる経路は2つ

  • exit 2 と stderr

    PostToolUseでhookが終了コード2を返すと、標準エラー出力がClaudeに見えます。ツールはすでに実行済みなので、編集自体は取り消されません。

  • JSON の decision

    終了コード0でJSONを標準出力に書く方法もあります。decision: "block"とreasonを返すと、理由がツール結果の隣に付きます。

どちらか一方に統一します。リファレンスには、終了コードだけで通知するか、終了コード0でJSONを返すかを、hook1本につき1つ選ぶよう書かれています。以降は前者で書きます。ビルドログをそのまま流せて、スクリプトが短く済むためです。

C++版 — godot-cppのSConsビルドを編集ごとに回す

godot-cppは、godot-cppリポジトリと自分のsrc、Godotのprojectを並べ、SConsでビルドする構成が標準です。godot-cppのチュートリアルでは、ビルドはscons platform=<platform>で、platformを省くと現在のプラットフォーム向けにビルドされます。成果物はproject/bin/に出ます。

手元にjq、scons、cargo、godotをPATHに通しておきます。使わない系統のツールは不要です。

hookスクリプトは、編集されたファイルの種類を見て、必要なときだけビルドします。入力JSONからtool_input.file_pathを読む前提で、jqを使います。

#!/bin/bash
# .claude/hooks/gdext-build.sh
f=$(jq -r '.tool_input.file_path // empty')
 
fail() {
  echo "$1" >&2
  tail -n 40 <<<"$2" >&2
  exit 2
}
 
case "$f" in
  */src/*.cpp|*/src/*.h|*/src/*.hpp)
    cd "$CLAUDE_PROJECT_DIR" || exit 0
    out=$(scons 2>&1) || fail "scons が失敗しました" "$out"
    ;;
  */rust/*.rs|*/rust/Cargo.toml)
    cd "$CLAUDE_PROJECT_DIR/rust" || exit 0
    out=$(cargo build 2>&1) || fail "cargo build が失敗しました" "$out"
    ;;
esac
exit 0

tail -n 40はログの末尾だけを返すための絞り込みです。コンパイラのエラーは、最後に出る要約と直前の数件が手がかりになることが多く、全文を渡すと文脈を無駄に使います。

settings側の登録は次のとおりです。スクリプトの場所には、リファレンスにある${CLAUDE_PROJECT_DIR}を使います。Claudeがworktreeに入っても、この値はセッション開始時のプロジェクトルートのままです。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/gdext-build.sh"
          }
        ]
      }
    ]
  }
}

スクリプトに実行権限が要ります。chmod +x .claude/hooks/gdext-build.shを忘れると、hookは起動に失敗します。この失敗は非ブロッキングのエラー扱いで、編集は止まらず、ビルドも走りません。

ビルドの種類を取り違えない

チュートリアルでは、sconsの既定はデバッグビルドで、最適化ビルドにはtarget=template_releaseを付けます。hookで回すのは既定のデバッグビルドで足ります。

.gdextensionのlibraries欄には、macos.debugとmacos.releaseのようにデバッグ用とリリース用が別々に書かれます。デバッグのhookが通っても、リリース側のパスが誤っていることもあります。エクスポート前にリリースビルドを1度通す手順は、CIかCLAUDE.mdに書いておく範囲です。

Rust版 — godot-rustのcargo buildを同じ形で回す

godot-rustは、Cargoプロジェクトをcdylibクレートとして作り、cargo add godotで依存を足します。godot-rustのHello Worldでは、反復中のビルドはcargo buildで、成果物はtarget/debug/に出ます。上のスクリプトの*.rsの分岐がそのまま使えます。

cargo buildの出力には、error[E...]の行と、発生位置を示す-->の行が出ます。Claudeはそれをそのまま読めます。クレート側のpanicを実行時に調べる場合は、RustのpanicをClaude Codeで読む手順が使えます。cargo checkとclippyでの検証の組み方は、Axumの記事に書いてあります。

.gdextensionの設定は編集のたびにずれることがある

ビルドが通っても、Godotがライブラリを見つけられなければ動きません。godot-rustのガイドは、初回設定でよくある失敗を挙げています。ライブラリが見つからない、エントリポイントのシンボルが解決できない、といったエラーです。

確認項目は、そのまま依頼文のチェックリストに使えます。

切り分け

読み込みに失敗したときの切り分け

  1. Qcargo build は成功していますか?
    答えがいいえのとき

    先にビルドを通します。hookが失敗を返していれば、ここで止まります。

    答えがはい
  2. QCargo.tomlのcrate-typeはcdylibですか?
    答えがいいえのとき

    cdylibでないと、Godotが読み込める動的ライブラリになりません。

    答えがはい
  3. Qentry_symbol が gdext_rust_init になっていますか?
    答えがいいえのとき

    godot-rustの既定のエントリはgdext_rust_initで、ほかの名前は動かないと明記されています。godot-cppは自分で決めた名前(チュートリアルではexample_library_init)を使うので、C++側のソースと.gdextensionの値を揃えます。

    答えがはい

libraries のパスを実ファイルと突き合わせる

target/debug/の実ファイル名と、.gdextensionの値を見比べます。パスはproject.godotのあるディレクトリが基準で、godot-rustの典型はres://../rust/target/debug/lib{クレート名}.soです。

この確認は、Claudeにファイル名を推測で書かせると外しやすい箇所です。「ls target/debugの結果を見てから.gdextensionを書く」と依頼文に入れておくと、手戻りが減ります。

Godotを起動して読み込みを確かめる

ビルドとは別に、Godot自体がライブラリを読み込めるかも確かめられます。Godotのコマンドライン資料では、--headlessは表示とオーディオを無効にした実行で、--pathでプロジェクトを指定し、--quitで1回回して終了できます。

godot --headless --path project --quit

godot-rustのガイドも、起動時にクラッシュするなら端末からgodot4 path/to/project.godotを実行してエラー出力を見るよう案内しています。実行ファイルの名前は環境で違い、この記事ではgodotと書きます。godot4で入れている場合は読み替えてください。

Godotのコマンドライン資料には、拡張の読み込みに失敗したときの終了コードの記載がありません。読み込みエラーを出力しても0で終わる可能性があるため、下のスクリプトは終了コードに加えて、出力にERRORを含む行があるかも見ます。この文字列は例で、出力の文言は環境で変わります。判定は手元の出力に合わせて調整し、結果は出力の末尾をそのままClaudeに渡します。

これは編集ごとではなく、Stop hookに置くのが向きます。Stop hookは終了コード2でClaudeが止まるのを防げるので、「ビルドは通ったが読み込めない」状態のまま作業を終えるのを避けられます。ただし無限ループを避けるため、入力のstop_hook_activeを見て、すでに続行中なら2回目は何もせず終えます。

#!/bin/bash
# .claude/hooks/gdext-load-check.sh
active=$(jq -r '.stop_hook_active // false')
[ "$active" = "true" ] && exit 0
 
cd "$CLAUDE_PROJECT_DIR" || exit 0
out=$(godot --headless --path project --quit 2>&1)
code=$?
if [ "$code" -ne 0 ] || grep -q "ERROR" <<<"$out"; then
  echo "Godot が読み込みに失敗しました(終了コード $code)" >&2
  tail -n 40 <<<"$out" >&2
  exit 2
fi
exit 0

ホットリロードと「Claudeに任せた変更」の相性

.gdextensionのreloadable = trueは、再コンパイルのたびにエディタが拡張を再読み込みする設定です。godot-cppのチュートリアルは、これが有効なのはデバッグビルドのときだと書いています。godot-rustのガイドは、Godot 4.2以降でホットリロードできると説明し、一方でエディタのウィンドウがフォーカスを取り戻したときにリロードされること、Godotがクラッシュするならこの設定を外してみることを挙げています。

Claudeにビルドを回させる運用では、この性質が効いてきます。編集ごとにhookがビルドすると、エディタを開いたままの場合に、再読み込みが頻繁に起きます。クラッシュが出たら、reloadableの有無を最初に疑えます。

compatibility_minimumも要注意です。godot-cppのREADMEは、古いGodot向けの拡張が新しい版でも動く一方、逆は動かないと書いています。Claudeが新しいAPIを使ったコードを足したとき、compatibility_minimumは自動では上がりません。バージョンを上げる変更は、人が確認する項目に入れておきます。

CLAUDE.mdに書く内容

hookはエラーを返すだけです。どこに何があるかは、CLAUDE.mdに書いておきます。

## GDExtension
- C++: `godot-cpp`(サブモジュール)、`src/`、`project/bin/`。`scons` でビルド
- Rust: `rust/`(cdylib)。`cargo build` でビルド
- `project/*.gdextension` の `libraries` は実ファイルと一致させる。
  書く前に `ls rust/target/debug` を実行する
- `compatibility_minimum` は変更しない。必要なら理由を示して確認する
- ビルドが失敗したら hook の出力を読み、そのファイルだけを直す

最後の1行は、エラーを返された後の動き方を縛るためのものです。「別の変更を足して直す」のではなく、直前の編集に戻る流れにします。

つまずきやすい点

  • Bashでの書き換えはhookに乗らない: Edit|Writeに一致するPostToolUseは、Bashコマンドや外部プロセスによるファイルの書き換えでは走りません。sed -iで.cppを直させると、ビルドは走りません。ファイル変更を拾うなら、監視するファイル名を指定するFileChangedイベントなどを使います
  • ビルドが長いと、編集ごとに待たされる: C++の初回ビルドはgodot-cpp本体を含むため長くなります。hookの既定タイムアウトはcommandで600秒です。重い場合はtimeoutで短く区切るか、.cppだけに絞ります
  • サブモジュールの初期化: godot-cppをGitサブモジュールで入れた場合、クローン後はgit submodule update --initが要ります。Claudeが別のworktreeで作業すると、godot-cppが空のままのことがあります
  • godot-cppのブランチ: チュートリアルは、対象のGodot版に合うブランチを使うよう求めています。READMEでは、godot-cpp 10以降はGodotとは別にバージョンが付き、api_versionで対象を指定します

まとめ

GDExtensionでClaude Codeに任せやすくなる分かれ目は、ビルド失敗を編集の直後に返すかどうかです。PostToolUse hookとexit 2の組み合わせなら、C++でもRustでも同じ形で組めます。Godotの起動確認はStop hookに置くと、「作り終えたが読み込めない」状態を減らせます。終了コードとERROR行のどちらでも拾えない場合があるので、読み込み確認は最後に人が1度見る項目として残します。.gdextensionのパスとcompatibility_minimumは、機械的に直させる対象から外しておくのが安全です。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/hooks
  2. github.com/godotengine/godot-cpp
  3. godot-rust.github.io/book/intro/hello-world.html
この記事を共有:XはてブLinkedIn