Heliodor 2018/12/20 17:28

謎のエラー

プログラム話です。

今回もすんごいへんてこなエラーに悩まされた話です。
結果から言うと、1日1回クリーンビルドしようね、ってことです。


以前お話しした通り、現在製作中のアクションゲームの開発ツールには Visual Studio を使っています。いつものように、ビルド(Ctrl+Shift+B)して実行、実行結果を確認、プログラムをいじって再ビルド、という風に作業していたところ、突然、見慣れないエラーが出てビルドできなくなりました。

しかも、全く触っていないはずのファイルでエラーが出ています。ためしに、今変更したファイルだけをピンポイントでコンパイル(Ctrl+F7)すると、そのファイルに関してのコンパイルは成功します。

たった今変更したファイルではコンパイルが通るのに、全く無関係な別のソースファイルでいきなりエラーが起きるようになったのです。

おかしい。さっきまで普通にビルドできてたのに。そもそも、そのファイルは1バイトたりとも変更してないぞと。
具体的には、名前空間 std が定義されていませんとか、cでは許されないキーワード使ってるとか、その他諸々のエラーが1000件ぐらい出てきます。見てみると、標準のc++ライブラリのヘッダでエラーが起きてるんですね。

まず疑ったのは、何かのミスで標準ライブラリのヘッダファイルに文字を書き込んでしまったのでは?という事です。試しに適当なc++ライブラリのヘッダにわざと無意味なコードを書き込んで保存しようとしましたが、当然ながら書き込みロックがかかっていて保存できません。これなら知らないうちに間違って書き込んで、さらに無意識にCtrl+Sして上書き保存してしまったなんて事は無さそうです。

次に疑ったのは、c++ではなくcでコンパイルするように設定が変わってしまったのでは?という事です。c++のソースをcコンパイラでコンパイルすれば、上記のようなエラーが山ほど出るのもうなずけます。

が、確認してみたところ普通の設定になってました。拡張子が.cppならc++コンパイラを、.cならcコンパイラを使うって設定ですね。まあ、普段いじるような設定ではないので、デフォルトのままになっています。当たり前です。

もしかしたら何かの具合で、それ以外のどこか細かい設定がおかしくなったのかもしれません。プロジェクトファイルなど、ソリューションファイル .slnを始め、キャッシュファイルなどを全て削除して、プロジェクトをクリーンな状態に戻してみます。

こういう時にcmakeの出番です。プロジェクトファイル一式を全て削除し、cmakeの設定ファイルであるCMakeLists.txtだけが残った状態にして、改めてcmakeでプロジェクトを再生成してみます。

CMakeLists.txtの内容が1バイトも変更されていないというのはTortoiseHgによる編集有無の検出機能で確認済みですから、最初にビルドができていた時と比べて、プロジェクト全体を通して1バイトも差分がないはずです。が、ビルド結果は変わらず、大量のエラーが出ます。

謎なのは、TortoiseHgで数日前の状態にソースを戻してビルドしても、同じエラーが起きるという事です。いやいや、昨日とかも普通にビルドして実行していたぞと。

ところが、さらにもっと前の状態に戻すとビルドが成功するんですね。そして、一度でもビルドが成功した後にソースを現在のものに戻すと、普通にビルドが通るんです。で、その状態でまたプログラムを再開すると、何度かビルドに成功したあと、あるとき突然エラーが出てビルドが失敗します。

そこまできてようやく気づいたのですが、普通にビルドが通っていたはずの昨日や数日前のソースが、クリーンビルドすると通らないんですよ。

つまり昔のソースでビルドした後、あるファイルに変更が加わって本来ならばコンパイルが通らない状態になっていたが、差分ビルド(変更があったファイルだけコンパイルし、それ以外は以前のコンパイル結果を使う)のためにそのファイルについては再ビルドが行われず、正しくコンパイルできた時の結果が使われていた、と言うことになります。

ただ、ファイルに変更があったのに差分ビルドの対象にならないって事があるのでしょうか?ファイル自体は変わってなくても、そのファイルがincludeしているヘッダに変更があれば連鎖的に再ビルドされるはずですが...。

そこで、ソースを昔に戻しながら、どの時点でクリーンビルドが通らなくなったのか調べました。

TortoiseHgでソースを戻し、cmakeのキャッシュを消してクリーンなプロジェクトファイルを作り、フルビルドする。これを何回も繰り返して調べた結果、1週間ほど前のソースを境にエラーが出るようになっていたことが分かりました。その時行った変更は、ソースファイルの分割です。

とあるファイルが巨大になりすぎて扱いにくくなったので、その中からゲームキャラクターやシステム間の通信(様々なコマンドを送ったり、特定のイベントが発生したときに通知したりする)に関する部分だけを抜き出し、signal.h と signal.cpp というファイルを追加して、そこに移動しました。

この変更のうち、どの部分がまずかったのかをさらに調べます。まず、新規ファイルの追加だけをしてみます。すると、signal.h と signal.cpp をプロジェクトに追加した時点でビルドが通らなくなりました。最初は???だったのですが、少ししてピーンと来ました。

なんか signal.h ってすごく標準ライブラリにありそうな名前だぞ、と。

もしかして名前がかぶっているのでは?と。

見てみると、やはり singal.h というのは標準ライブラリに存在するヘッダファイルでした。普段使わないので全く気にしていませんでしたが…。

そこで signal.h ではなく event_signal.h という名前に変えてみたところ、あっさりとビルドできるようになったのです。ここまでくると原因追究は簡単でした。

このゲームのプログラムではスクリプトシステムとして lua を使っているのですが、lua.lib などのコンパイル済みファイルを使わずに lua をソースごとプロジェクトに取り込んでありました。

その中に、#include <signal.h> という、標準ライブラリを include している部分があったのですが、lua のソースは c で書かれているため、lua のファイルをコンパイルする時は自動的に c コンパイラが選択されます。#include <signal.h> の部分で、普通なら c コンパイラは標準ライブラリの signal.h を参照するはずですが、プロジェクト内に同名のファイルが見つかったためにそちらを優先し、 c コンパイラが c++ のヘッダファイルを処理しようとして大量のエラーが発生した、という事でした。

普通ならインクルード先のヘッダファイルが変わっていたら連鎖的に再コンパイルされるはずですが、ファイル名が同じで参照先が変わるだけ、というのは盲点だったのかもしれません。とにかく、再コンパイルが行われずに中途半端に過去の結果が使われてしたために変な挙動になっていました。

そういえば別々の cpp ファイルに同名のローカルクラスを作って挙動がおかしくなった、という事故が以前ありました。

例えば a.cpp 内で MyClass というクラスを作り、b.cpp 内でも MyClass というクラスを作ります。MyClass はヘッダには出さないので、a.cpp の MyClass は a.cpp 内からしか見えないし、b.cpp 内の MyClass は b.cpp 内からしか見えないはずです。

ところが MyClass のメソッドを呼ぼうとしたとき、a.cpp の MyClass のメソッドを呼んだつもりが、b.cpp の MyClass のメソッドが呼ばれて、挙動がおかしくなったという事がありました。メンバ変数は a.cpp で定義した通りなのに、呼ばれたメソッドは b.cpp のものだった、というものです。

それ以来、たとえスコープ的に独立したものであっても同名のクラス名は付けないように気を付けていたのですが…。変な挙動をしたときは、同名のファイルやクラス、メソッドがないかどうか確認した方がよさそうです。

ところで、原因はともかく、今回の事故はこまめにクリーンビルドしていれば気づけたものでした。そうすれば、少なくとも何日も経過してからビルド出来ないことに気づくという事はなかったわけです。


結論:
「1日1回クリーンビルドしよう」

(あと、バージョン管理ツールは、こういう時にすごく役立つ)

以上です。

この記事が良かったらチップを贈って支援しましょう!

チップを贈るにはユーザー登録が必要です。チップについてはこちら

記事のタグから探す

月別アーカイブ

限定特典から探す

記事を検索