例外処理を再整備する

条件コンパイル定義式の中にはASSERTIONマクロが含まれる.ASSERTIONは通常(つねにとは限らない)関数の入口に置かれて,引数の整合性などをチェックする一種の関門だ.ゼルコバの木ではASSERT_NEVERとDEBUG_NEVERという2つのマクロを使っている.マイクロソフトのASSERTIONは肯定型の言明で「かならず~であること」を確認するようになっているが,我々の場合には「決して~ではない」ということを検証する※.この違いは欧米的な意識と日本的意識の相違なのではないかという気がする.欧米感覚では「かくあるべき」という規範意識が強いような気がするが,日本的感覚だと,「こういうことは許されない」がそれ以外に関しては言及しない(なんでもよい).

※ASSERTIONとASSERT_NEVERは相互に転換可能だ.ASSERT(X) という言明は ASSERT NEVER EVER(~X)と等価である.

ASSERT_NEVERもDEBUG_NEVERもどちらも決して起きてはならないが,論理的にはまず起こらないと推定されるものだ.しかし,プログラムでは何が起こるか分からないので一次防禦線として張られている.DEBUG_NEVERはデバッグ時にのみ実行される内部検査で,仮に起きたとしても軽微なエラーとしてそのまま続行可能,ASSERT_NEVERは続行不能な致命的障害を意味する.ASSERT_NEVERはリリース版でもアクティブになっているので,事後処理つまり,例外処理が不可欠だ.例外処理はどうしても手抜きになってしまうところだが,いずれ整備しなくてはならない.旧版では「バグレポートの提出要請」という形で完全な例外処理が実装されていた.現行でも一部まだその機構は残っているが,これを再整備する必要がある.

昨日の終業間近に出ていたメモリアクセス違反を見ておこう.MARGBOX::setGenerationで例外をスローした後,新規ファイル生成不可→アプリ終了しようとしてGP例外が場外で発生する.ASSERT_NEVERが2回スローされているが,これは後で見ることにして… いや,2回どころか数回発生している.最後はCARDLINK:CleanSansyoで発出されたもので,例外はここで起きているものと見られる.ビルドはReleaseで,FORMALVERSION.Debugモードで再現できるかどうかを見てみよう.⇒GP例外は発生しない.その代わり,

Debug Error!
Program: D:\ZELKOVA\debug\ZelkovaTree2021.exe
abort() has been called

のような表示が出ている.「Debug Error!」という語句はゼルコバの木のものではないので,VSのデバッガが出しているのだろう.これでは手がかりにならないので,cleansansyo.cppの238行目からトレースしてみる.⇒~COUPLING→ ~FAMILYTREE→ ~CARDTABLE→ ~ARRAY→ CleenSlot→ ~CARLINK→ CleanSlot→ clearslot→ CleanSansyo のように進んだところで,RETURNIFCLEAN というマクロによってアボートしている.

#define RETURNIFCLEAN \
     ASSERT_NEVER (object == this && !(checkcid(‘n’) && ((NAMEBOX*)object)->getghostbits(SYMMETRICMARG|REALGHOST) == (SYMMETRICMARG|REALGHOST))) \
     if (!object || !object->Refcount()) return 0;

このマクロは各クラスのCleanSansyoの冒頭で実行されている.このマクロの趣旨は「objectが空かobjectの被参照カウントがゼロならゼロ復帰する」という妥当なものだが,その前に実行しているマクロが曲者だ.これは実行順序が逆だと思う.⇒修正したが,解決しない.⇒ASSERT_NEVERが成立して例外をスローすることでアボートしている.おそらく,VBが「プログラムを終了します」を出した時点でアプリ終了になっているはずだ.しかし,DLLには特に通知を送っていないので,それを検知することはできない.

いや,認識しているのではないか?PHASEはENDOFAPPLICATIONという状態になっている.~COUPLINGが実行されているのがその証拠だ.⇒ENDOFAPPLICATIONフェーズでは例外をスローしないとしておこう.確かに,それしか方法がないが,ではENDOFAPPLICATIONのときにはどうしたらよいのか?そのまま続行できないとすれば,リターンする以外ないが戻り値は各関数によってまちまちだ.⇒return 0; としてみたところ,void型の関数から一斉にクレームがあがった.

_ASSERT_NEVERというのがあるが,これはどういう用途で作られたものだろう?⇒デバッグモードではSTOPするか,_STOPかの違いだ.つまり,停止するか?警告パネルを出すかの違いだ.非公式版ではどちらも警告パネルを出すようになっている._ASSERT_NEVERを使っている事例を見ると主要なコンポーネントが存在することを確認するような使い方になっているので,これらを区別する必要性はあまりないような気がする.⇒_ASSERT_NEVERをvoid型で使うマクロとして再定義するというのでよいのではないか?やってみよう.

_ASSERT_NEVERが141個あった.⇒「コンストラクタは値を返せません」というのまで出てきた.確かにコンストラクタの中でASSERTIONするというのは自己矛盾であるような気もする.「オレは誰だ~」と言っているようなものだ.コンストラクタはそれ自身で完結しなくてはならない.⇒DEBUG_NEVERなら許されるだろう.⇒整数値以外を返す関数もある→3個.⇒付け替え完了し,ノーマルに終了できる._ASSERT_NEVERは定義を含めて367箇所,ASSERT_NEVERは2114箇所ある.Releaseビルドしたら漏れが見つかって,_ASSERT_NEVERが406個,ASSERT_NEVERが2075個に変わった.合計2481.

ReleaseビルドのFORMALVERSIONを起動して冒頭で例外が発生した TRASHCAN:MakeTrashCanの中で起きているように思われる

Debugビルドでは問題なく立ち上がってくるので,ASSERTIONの修正が関係している可能性が高い.「_DEBUGマクロを未定義にする」でどうなるか見てみよう.原因は分かった.コンストラクタの中で実行しているASSERT_NEVERをすべてDEBUG_NEVERに変えてしまったからだ.これらの中には単なる検査ではなく,

new (this, COUPLsKAKEIZU) KAKEIZU

のように実動作を伴うものがある.このような場合にはDEBUG_NEVERではなく,CONFIRM_NEVERを使わなくてはならない.TITLELINKでも同様のことが起きている.しまった.どのクラスのコンストラクタを修正したかメモっていない.以下のようなエラーが出ているので,これを片付けないと前に進めない.

image

ゼルコバの木で使っているクラスは49種ある.それを一つづつチェックするよりは,むしろ,DEBUG_NEVERを検索してしまった方が早い.DEBUG_NEVERは372個あるが,マクロの引数をチェックすれば修正が必要か否かはすぐに分かる.というか,このような誤りは別の場所にも存在する可能性があるので,全点検査はいずれにしても必要だ.DEBUG_NEVER (CheckFreeDirectSubTribe(funcname))のように検査関数を(検査モードで)呼び出すだけのものは見逃してもよい.しかし,これはかなりまずいのではないか?

DEBUG_NEVER (getkukan(primenod, realnode, true).Width())

getkukanの戻り値はBRectだ.いや,違う.この条件式はその矩形の幅がゼロか否かを見ている.⇒全点検査した.問題ないように思われる.⇒動作するようになった.上の不良はランニング中にソースコードを編集して続行したためではないかと思われる.⇒アプリを終了しようとしているのに,FAMILYTREEでInitializeFamilyTreeを実行しようとしているのはなぜか?このとき,

(topology->PDB->getmaxrecn() || topology->MDB->getmaxrecn())

が起きてASSERT_NEVERが再発動する.InitializeFamilyTreeの実行はアプリ終了後ではない.起動→新規ファイル→サンプルオープン→例外発生→検定失敗→新規ファイルオープンで実行される.しかし,起動時に新規ファイルが開けているのに,障害発生後にそれができなくなるというのもおかしい.ファイルのクローズを実行していないのではないか?⇒いや,実行されている.しかし,テーブルは空になっていない.

COUPLING::EraseFamilyTreeではフェーズがINITIALSTATE以下では入口で復帰している.これを廃止すればテーブルは空になるが,別のところでエラーが発生するようになる.Coupling::OpenFamilyBaseでpartialmap不在が発生する.COUPLING::OpenFamilyBaseではpartialmapが存在するものと仮定している.PARTIALNAMEはどこで誰が生成しているのか?FAMITREEの所有で,FAMILYTREE:InitializeFamilyTreeで生成され,FAMILYTREE:Disposeで削除される.TOPOLOGYも同様だ.CARDTABLEとMARGTABLEはLINKTABLEのコンストラクタで生成されている.

それではどこでPARTIALNAMEは消されているのだろう?⇒COUPLING:EraseFamilyTreeでFAMILYTREEのすべてのスロットをCleanSlotしている.COUPLINGがPARTIALNAMEを必要としているのは,ファイルの読み込み時に使っているからだ.部分図データはファイルのヘッダ部に保存され,PARTIALNAMEに直接ロードされる.それだけではない.TOPOLOGYもUNDOSYSTEMも,EraseFamilyTreeで消去しているほとんどすべてのオブジェクトが必要だ.ということは実質,EraseFamilyTreeは終了時以外は実行されていないということになる.呼び出しているフリはしているが,

if (PHASE <= INITIALSTATE) return;

でほぼ常時無動作でリターンしている.不要なときに空動作で戻るのはよいとしても,必要なときに動作しないのではまずい.見た限りではPARTIALMAPやTOPOLOGYは初回生成された場所以外では再生成されないので,EraseFamilyTreeで消去するというのは不当であるように思われる.これらのオブジェクトを消去しないという仮修正を入れて動作を見てみることにする.⇒かなりの修正が必要になったが,最後にTITLEBOXが存在しないという問題が残った.TITLEBOXはNAMEBOXやMARGBOXと同格の描画オブジェクトだが,かなり特殊な扱いになっていて,TREEVIEW直下のTITLELINKというのが管理している.

EraseFamilyTreeでは一旦すべての描画オブジェクトをパージしてしまうので,その中にTITLEBOXが含まれているため,TITLEBOX不在が至るところで発生することになる.⇒この件に関してはむしろ,まずタイトル枠が存在しない状態でも動作するように仕様変更した方がよいと思う.タイトル枠の仕様はまだ,今後も変化する可能性があるのでそのオブジェクトが一つ欠けているだけでシステムが停止してしまうというのはあまりよい仕様ではない.⇒一応エラーなしに動作するようになったが,画面が真っ白のままだ.メニューなどは操作できるので,システム的には動作しているようだが,画面上に何も描画されていないことは拡張選択しても何も選択できないことからも間違いない.

原因は分かった.COUPLING:SetTitleBox→ TITLEBOX:SetDispParmを呼び出していない.この中にはTREEVIEWの計算が入っている.TREEVIEWに関わる部分を切り出して別途実行する必要がある.系図外枠の計算を行っているTREEVIEW::GetTitlePositionの修正も必要だ.⇒一通り修正を入れたつもりだが,画面が出ない…タイトル枠を表示しないというモードがあるくらいなのだから,タイトル枠がなければ描画できないということはないはずだ.

!描画できた!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA