【PL/I学習|初心者向け】ON-UnitからのGOTOにご用心!スタック巻き戻しと安全な後処理の鉄則

メインフレームエンジニアの皆さん、こんにちは!今回は、PL/Iの強力な例外処理機能であるON-Unitと、その中で注意すべき「スタックのクリーンアップ」規則について、初心者の方にも分かりやすく解説していきます。

1. 導入: ON-UnitのGOTOがなぜ重要なのか?

PL/IにおけるON-Unitは、プログラム実行中に予期せぬエラー(ゼロ除算やI/Oエラーなど)が発生した際に、特定の処理を実行するための重要な仕組みです。しかし、このON-Unit内でさらにサブルーチンを呼び出し、そのサブルーチンからプログラムの別の場所へ GOTO 文でジャンプした場合、思わぬ落とし穴にはまることがあります。

それは、GOTOによって「呼び出しスタック」が一気に巻き戻され(クリーンアップされ)、本来実行されるはずだった後処理(例えば、開いたファイルのクローズや確保したメモリの解放など)がスキップされてしまう可能性がある、という問題です。これにより、リソースリークやデータ不整合といった重大なバグにつながることもあります。

本記事では、このスタックの巻き戻しメカニズムを理解し、いかにして安全な後処理を実現するかを学びます。

2. 基礎知識: ON-Unit、スタック、そして巻き戻し

まずは、このテーマを理解するために必要な基本的な用語を整理しましょう。

  • ON-Unit: PL/Iの例外処理ブロックです。特定の条件(ON条件)が発生したときに自動的に実行されるコードの集まりで、エラーからの回復やログ記録などに使われます。例えば、ON ERROR BEGIN; ... END; のように記述します。
  • スタック(呼び出しスタック): プログラムがサブルーチンや関数を呼び出す際に、そのサブルーチンに戻るためのアドレスやローカル変数などの情報を一時的に記憶しておくメモリ領域です。LIFO(後入れ先出し)の構造になっており、サブルーチンが呼び出されるたびに情報が積まれ、サブルーチンから戻るたびに情報が取り除かれます。
  • スタックの巻き戻し(Unwind): プログラムが異常終了したり、GOTO文などで複数の呼び出し階層を一気に飛び越えたりするときに、スタックに積まれた情報を順に破棄していく処理のことです。PL/Iのランタイム(LE: Language Environment)がこの処理を行い、メモリポインタを安全な状態に復元します。これは、現代のプログラミング言語における例外(Exception)の「投げ上げ」によるスタック展開と本質的に同じ概念です。
  • GOTO文: プログラムの実行フローを、指定されたラベルの位置へ直接ジャンプさせる命令です。非常に強力ですが、多用するとプログラムの制御が複雑になり、可読性や保守性が低下しやすいとされています。

3. 実装/解決策: GOTOによるスタック巻き戻しへの対応

参考情報にもある通り、ON-Unitから呼び出したサブルーチン内でGOTOを使い、プロシージャ外へ脱出すると、PL/Iのランタイムがスタックを一気に巻き戻します。このとき、GOTOで飛び越えた呼び出し階層にあった「本来実行されるべき後処理」は実行されません。

この問題を回避し、確実に後処理を実行するための考え方は以下の通りです。

  • 後処理の一元化: ファイルのクローズやリソースの解放など、プログラムの正常終了時にも異常終了時にも必ず行うべき処理は、GOTOの影響を受けにくい場所に一元化するか、ON-Unit内で確実に実行されるように設計します。
  • GOTOの使用を最小限に: ON-Unit内、特にそこから呼び出されたサブルーチン内でのGOTOは、スタックの巻き戻しを意識して慎重に利用すべきです。可能であれば、GOTOではなく、ON-Unit内でエラー処理を完結させ、通常の戻り値やフラグでエラー状態を上位に伝える設計を検討します。
  • 現代言語の「finally」ブロックの概念を導入: Javaなどの言語には、例外が発生しても必ず実行される finally ブロックがあります。PL/Iには直接的な finally はありませんが、ON-Unit内で必ず実行したい処理を記述したり、プログラム終了時に必ず呼び出される共通のエラー終了ルーチンを作成したりすることで、これに近い挙動を実現できます。

4. サンプルプログラム

以下のPL/Iプログラムは、ON-Unitから呼び出されたサブルーチン内でエラーを発生させ、さらにGOTOでプログラムの終了地点へジャンプした場合に、どのようにスタックが巻き戻され、後処理がスキップされるかを示しています。


MAIN_PROC: PROCEDURE OPTIONS(MAIN);
    DCL FILE_HANDLE FILE;
    DCL (I, J) FIXED BIN(31);

    / --- 1. エラー発生時に実行されるON-Unitを定義 --- /
    ON ERROR BEGIN;
        PUT SKIP LIST(' ON ERROR ON-Unitに入りました ');
        CALL CLEANUP_AND_EXIT; / エラー処理用のサブルーチンを呼び出す /
        / 注意: ここからGOTOでMAIN_PROCの外へ脱出すると、
                 MAIN_PROCの後続処理はスキップされます。
                 このON-Unitブロックの終了時に、暗黙的に元のエラー発生点へ戻ろうとしますが、
                 CLEANUP_AND_EXIT内でGOTOするため、戻りません。
        /
    END;

    / --- 2. ファイルを開く処理 (本来はクローズが必要なリソース) --- /
    OPEN FILE(FILE_HANDLE) OUTPUT;
    PUT SKIP LIST('>> ファイルを開きました。');

    / --- 3. メインのデータ処理サブルーチンを呼び出す --- /
    CALL PROCESS_DATA;

    / --- 4. 正常終了時の処理 (GOTOでスキップされる可能性あり) --- /
    PUT SKIP LIST('>> メイン処理が完了しました。');
    CLOSE FILE(FILE_HANDLE); / 正常終了時のファイルクローズ /
    PUT SKIP LIST('>> ファイルを閉じました。(メインプロシージャ)');

    RETURN; / メインプロシージャの終了 /

/ --- データ処理サブルーチン --- /
PROCESS_DATA: PROCEDURE;
    PUT SKIP LIST('>> PROCESS_DATA 開始');
    I = 0;
    J = 10;
    / --- 意図的にゼロ除算エラーを発生させる --- /
    J = J / I; / ここでSIZEまたはZERODIVIDE条件が発生し、ON ERROR ON-Unitへ制御が移る /
    PUT SKIP LIST('>> PROCESS_DATA 終了 (この行は実行されません)');
END PROCESS_DATA;

/ --- エラー発生時のクリーンアップと終了処理サブルーチン --- /
CLEANUP_AND_EXIT: PROCEDURE;
    PUT SKIP LIST('>> CLEANUP_AND_EXIT 開始 (ON-Unitから呼び出された)');

    / --- 5. ファイルが開いていればクローズする --- /
    IF FILE_HANDLE.OPEN THEN DO; / ファイルが開いているか確認 /
        CLOSE FILE(FILE_HANDLE);
        PUT SKIP LIST('>> ファイルを閉じました。(CLEANUP_AND_EXIT内)');
    END;

    / --- 6. プログラムの最終的な終了地点へGOTOでジャンプ --- /
    GO TO PROGRAM_END; / ここでスタックが巻き戻され、MAIN_PROCの後続処理はスキップされる /
    PUT SKIP LIST('>> CLEANUP_AND_EXIT 終了 (この行は実行されません)');
END CLEANUP_AND_EXIT;

/ --- プログラムの最終終了地点 --- /
PROGRAM_END: ENTRY;
    PUT SKIP LIST('>>> プログラムが終了します。<<<');
END PROGRAM_END;

END MAIN_PROC;

上記のプログラムを実行すると、以下の出力が期待されます。


>> ファイルを開きました。
>> PROCESS_DATA 開始
 ON ERROR ON-Unitに入りました 
>> CLEANUP_AND_EXIT 開始 (ON-Unitから呼び出された)
>> ファイルを閉じました。(CLEANUP_AND_EXIT内)
>>> プログラムが終了します。<<<

この出力から分かるように、MAIN_PROC 内の「>> メイン処理が完了しました。」と「>> ファイルを閉じました。(メインプロシージャ)」の行は実行されていません。これは、CLEANUP_AND_EXIT サブルーチン内での GO TO PROGRAM_END; によってスタックが巻き戻され、PROCESS_DATA から MAIN_PROC への通常の戻り処理がスキップされたためです。

5. 応用・注意点: 現場で役立つ補足情報

  • リソース管理の徹底: ファイル、データベース接続、動的に確保したメモリなどのリソースは、ON-UnitでのGOTOによるスタック巻き戻しが発生しても確実に解放されるよう、設計段階で強く意識してください。クリーンアップ処理を専用のサブルーチンにまとめ、そこから直接プログラムを終了させるようなロジックは有効ですが、そのサブルーチン内で開かれたリソースは責任を持ってクローズする必要があります。
  • GOTOの代替案: PL/Iでは、GOTOを使わずにエラー処理を上位プロシージャに伝える方法として、戻り値やグローバルなフラグ変数を利用する手法があります。これにより、呼び出しスタックを破壊することなく、エラー状態を伝播させることが可能です。
  • レガシーコードの改修・移行時: 既存のPL/Iプログラムを現代化する際や、他言語への移行を検討する際には、ON-UnitとGOTOの組み合わせによるスタック巻き戻しが隠れたバグの原因となっていないか、特に注意してコードレビューを行ってください。多くの場合、`finally` ブロックに相当する処理への再配置が求められます。
  • シンプルさを追求: 複雑なGOTOジャンプは、コードの可読

コメント

タイトルとURLをコピーしました