PL/IのADDRとENTRY、似ているようで全く違う!基幹システム改修で迷わないための徹底解説
おい、諸君。今日はPL/Iのちょっとややこしい、でも知っておかないと後で痛い目を見るかもしれない「ADDR」と「ENTRY」ビルトイン関数について、じっくり話を聞いてほしい。特に、日々バッチ改修やマイグレーション調査に汗を流している諸君には、ぜひともこの違いを腹に落としてもらいたい。
メインフレームのPL/Iプログラム、特に基幹システムとなると、その規模や複雑さは想像以上だ。レコード入出力やVSAMアクセス、ONユニットでのエラーハンドリング、そして複雑に絡み合ったプロシージャ呼び出し…これらを理解し、安全に改修していくためには、言語の細部にまで目を光らせる必要がある。ADDRとENTRYも、一見すると「何かのポインタを取ってくる関数」という点で似ているように見えるかもしれないが、その目的と使いどころは全く異なる。これを混同すると、思わぬバグを生み出したり、デバッグで泥沼にはまったりする原因になりかねないんだ。
ADDR関数:変数の「場所」を教えてくれる頼れる相棒
まず、ADDR関数から話を始めよう。ADDR関数は、その名の通り、引数として指定したデータ変数のメモリアドレスを取得するための関数だ。PL/Iでは、変数というのはメモリ上のどこかに配置されている。ADDR関数は、その「どこ」を教えてくれる、いわば「住所」を教えてくれる関数なんだ。
例えば、こんなコードを考えてみてくれ。
PLI
DCL MY_VAR CHAR(50) INIT(‘Hello, Mainframe!’);
DCL MY_ADDR POINTER;
MY_ADDR = ADDR(MY_VAR);
/ MY_ADDRには、MY_VARが格納されているメモリ上のアドレスが入る /
/ このアドレスを使って、直接メモリを操作することも可能だが、 /
/ 基本的にはポインタ変数経由でのアクセスに留めるのが安全だ /
この例では、`MY_VAR`という文字変数を用意し、`ADDR`関数を使ってそのアドレスを`MY_ADDR`というポインタ変数に格納している。`MY_ADDR`は、これ以降`MY_VAR`と同じメモリ領域を指し示すことになる。
ADDR関数の使いどころ:ポインタ演算や動的メモリ確保の基礎
ADDR関数は、主に以下のような場面で活躍する。
- ポインタ演算: ポインタ変数を使って、メモリ上の特定のオフセットにあるデータを参照したい場合。例えば、構造体や配列の要素をポインタで走査するような場面で、基準となるアドレスを取得するために使われることがある。
- 動的メモリ確保: PL/Iには`ALLOCC`や`FREE`といった関数(あるいはそれに類する機能)で、実行時にメモリを動的に確保・解放する機能がある。これらの機能と連携し、確保したメモリ領域のアドレスを取得するためにADDR関数が使われることがある。
- 低レベルなメモリ操作: 非常に限定的だが、特定のハードウェアインタフェースや、既存のC言語ライブラリなどをPL/Iから呼び出す際に、メモリ領域のアドレスを渡す必要がある場合がある。
ただし、ADDR関数で取得したアドレスを直接操作するのは、非常に高度な技術と深い理解が必要だ。誤った操作は、システム全体のクラッシュにつながる可能性もある。通常は、取得したアドレスをポインタ変数に格納し、そのポインタ変数を通じて変数を参照・更新するのが安全な方法だ。
ENTRY関数:プロシージャの「入り口」を教えてくれる案内人
一方、ENTRY関数は全く別の目的を持っている。ENTRY関数は、指定したプロシージャ(サブルーチン)のエントリポイントのアドレスを取得するための関数だ。
「エントリポイント」というのは、プログラムの実行がそのプロシージャに入っていく「入り口」となる場所のことだ。ENTRY関数は、この「入り口」そのもののアドレスを、ADDR関数が変数のアドレスを返すのと同じように、ポインタ変数として返してくれる。
例えば、こんなコードを見てみよう。
PLI
/ 別のファイルや、同じソースの別ブロックにあるプロシージャ /
MY_SUBROUTINE: PROCEDURE(P1, P2);
DCL P1 FIXED BINARY;
DCL P2 CHAR();
/ … プロシージャ本体 … /
END MY_SUBROUTINE;
/ メインプログラムまたは別のプロシージャ /
DCL SUB_ENTRY ENTRY RETURNS(POINTER); / プロシージャのエントリポイントはポインタ /
DCL ENTRY_PTR POINTER;
ENTRY_PTR = ENTRY(MY_SUBROUTINE);
/ ENTRY_PTRには、MY_SUBROUTINEプロシージャの開始アドレスが入る /
/ このポインタを使って、間接的にプロシージャを呼び出すことも可能 /
/ CALL MY_SUBROUTINE(123, ‘ABC’); と同等 /
/ CALL ENTRY_PTR(123, ‘ABC’); / このような直接的な呼び出しはできない /
/ CALLを介して間接的に呼び出す例 /
CALL ENTRY_PTR'(‘123’, ”ABC”); / CALL BY POINTER 構文 /
この例では、`MY_SUBROUTINE`というプロシージャを定義し、そのエントリポイントのアドレスを`ENTRY`関数で取得し、`ENTRY_PTR`というポインタ変数に格納している。
ENTRY関数の使いどころ:動的なプロシージャ呼び出しやコールバック
ENTRY関数は、ADDR関数よりもさらに限定的な状況で使われることが多いが、その威力は大きい。
- 動的なプロシージャ呼び出し: 実行時の条件によって、どのプロシージャを呼び出すかを変えたい場合。例えば、設定ファイルから呼び出すべきプロシージャ名を読み込み、その名前からENTRY関数でエントリポイントを取得し、間接的に呼び出すといったことが可能になる。これは、プラグイン機構のようなものを実装する際の基礎になる。
- コールバック関数: ある処理が終わった後、呼び出し元に処理を委ねたい場合。呼び出し元で定義されたプロシージャのエントリポイントをENTRY関数で取得し、それを引数として渡すことで、相手のプロシージャを呼び出すことができる。これは、イベント駆動型のプログラミングや、ライブラリ関数からユーザー定義関数を呼び出すような場面で使われる。
- プロシージャポインタ: PL/Iには、プロシージャ自体を変数として扱う「プロシージャポインタ」という概念がある。ENTRY関数は、このプロシージャポインタにアドレスを格納するために使われる。
注意点として、ENTRY関数で取得したポインタは、直接 `CALL ENTRY_PTR(arg1, arg2);` のように呼び出すことはできない。PL/Iの標準的な呼び出し構文ではないからだ。間接呼び出しを行う場合は、`CALL`ステートメントの後にプロシージャポインタを指定する特殊な構文(`CALL pointer_variable(arg1, arg2);` のような形)を使うか、あるいは、ENTRY関数で取得したポインタを一時的に`ENTRY`型で宣言した変数に代入してから呼び出すなどの工夫が必要になる。
ADDRとENTRY、混同しないための「心の壁」
さて、ADDRとENTRYの違い、なんとなく掴めただろうか?
- ADDR: データ変数のメモリアドレス
- ENTRY: プロシージャのエントリポイントのアドレス
この二つは、どちらも「アドレス」という点では共通しているが、指し示している対象が全く違う。ADDRは「モノ(データ)」の場所、ENTRYは「コト(処理)」の場所を教えてくれる、と考えると分かりやすいかもしれない。
実践!レコード入出力とONユニットでの応用例
では、実際の基幹システムでよく見かけるレコード入出力やONユニットの制御フローの中で、これらの関数がどう絡んでくるか、少し想像を膨らませてみよう。
例えば、あるバッチ処理で、ファイルからレコードを読み込み、そのレコードの内容に応じて異なるサブルーチンを呼び分ける、というような処理があったとする。
PLI
/ ====================================================================== /
/ メイン処理ブロック /
/ ====================================================================== /
MAIN_PROC: PROCEDURE OPTIONS(MAIN);
/ — 変数宣言 — /
DCL INPUT_RECORD PIC X(100); / 入力レコード用バッファ /
DCL RECORD_TYPE PIC 9; / レコード種別(1桁の数字) /
DCL PROC_TO_CALL POINTER; / 呼び出すプロシージャのポインタ /
/ — プロシージャのエントリポイントを宣言 — /
/ RECORD_TYPEが1ならPROCESS_TYPE_Aを、2ならPROCESS_TYPE_Bを呼び出す /
DCL PROC_A_ENTRY ENTRY RETURNS(POINTER);
DCL PROC_B_ENTRY ENTRY RETURNS(POINTER);
/ — ファイル定義 — /
DCL INPUT_FILE FILE RECORD SEQUENTIAL INPUT ENV(F(100));
/ — ONユニット定義 — /
ON ENDFILE(INPUT_FILE) BEGIN
PUT SKIP LIST(‘ファイル終了’);
GO TO END_JOB;
END;
ON ERROR BEGIN
PUT SKIP LIST(‘エラー発生!’);
/ エラー発生時の処理をここに記述 /
/ 例: エラーコードを取得し、ログ出力 /
/ SYSDATA を使ってエラー情報を取得する例 /
/ DCL SYSDATA BUILTIN; /
/ PUT SKIP LIST(‘エラーコード: ‘, SYSDATA); /
GO TO END_JOB;
END;
/ — ファイルオープン — /
OPEN FILE(INPUT_FILE);
/ — メインループ — /
DO WHILE (1);
READ FILE(INPUT_FILE) INTO(INPUT_RECORD);
RECORD_TYPE = SUBSTR(INPUT_RECORD, 1, 1); / レコードの最初の1文字を種別とする /
/ レコード種別に応じて、呼び出すプロシージャを決定 /
IF RECORD_TYPE = ‘1’ THEN
PROC_TO_CALL = ENTRY(PROCESS_TYPE_A); / タイプAのプロシージャアドレスを取得 /
ELSE IF RECORD_TYPE = ‘2’ THEN
PROC_TO_CALL = ENTRY(PROCESS_TYPE_B); / タイプBのプロシージャアドレスを取得 /
ELSE DO;
PUT SKIP LIST(‘不明なレコード種別: ‘, RECORD_TYPE);
/ 不明な種別の場合は、エラー処理へ /
SIGNAL ERROR; / ON ERROR ユニットを発火させる /
END;
/ 決定したプロシージャを間接的に呼び出す /
/ 実際には、プロシージャの引数に合わせて調整が必要 /
/ ここでは簡略化のため、引数なしで呼び出す形を想定 /
CALL PROC_TO_CALL(); / プロシージャポインタ経由の呼び出し /
END; / DO WHILE /
END_JOB:
/ — クリーンアップ処理 — /
CLOSE FILE(INPUT_FILE);
PUT SKIP LIST(‘処理終了’);
RETURN; / メインプロシージャの終了 /
END MAIN_PROC;
/ ====================================================================== /
/ サブルーチン定義 (別ブロックまたは別ソースファイル) /
/ ====================================================================== /
PROCESS_TYPE_A: PROCEDURE;
/ タイプAのレコードに対する処理 /
PUT SKIP LIST(‘プロセスAを実行中…’);
/ … 実際の処理 … /
END PROCESS_TYPE_A;
PROCESS_TYPE_B: PROCEDURE;
/ タイプBのレコードに対する処理 /
PUT SKIP LIST(‘プロセスBを実行中…’);
/ … 実際の処理 … /
END PROCESS_TYPE_B;
この例では、`ENTRY`関数を使って、`PROCESS_TYPE_A`や`PROCESS_TYPE_B`といったサブルーチンのエントリポイントを取得し、`PROC_TO_CALL`というポインタ変数に格納しています。そして、`DO WHILE`ループの中で、読み込んだレコードの種別に応じて、この`PROC_TO_CALL`変数が指すプロシージャを間接的に呼び出しています。
`ADDR`関数は、ここでは直接使っていませんが、もし`PROCESS_TYPE_A`や`PROCESS_TYPE_B`の中で、特定の構造体変数のアドレスを取得して、それを別の関数に渡す、といった処理があれば、そこで`ADDR`関数が使われることになるでしょう。
ONユニットでの注意点
ONユニットは、エラー発生時などにプログラムの実行フローを制御するために非常に重要です。`ON ENDFILE`や`ON ERROR`といったユニット内で、`ADDR`や`ENTRY`関数を使うことも理論上は可能です。
例えば、エラー発生時に、エラー箇所のメモリダンプを取得したい場合、`ADDR`関数を使って変数のアドレスを取得し、それをファイルに書き出す、といった処理が考えられます。しかし、ONユニット内での複雑な処理は、デバッグをさらに困難にする可能性があります。通常は、ONユニットでは最低限のエラーハンドリングと、処理の継続または終了に留め、詳細なデバッグ情報は別途ログファイルなどに記録するのが定石です。
まとめ:両者の違いを明確に、安全なコーディングを
ADDRとENTRY、この二つのビルトイン関数は、PL/Iでメモリを扱う上で非常に強力なツールですが、その使い分けを誤ると、予期せぬバグやプログラムの暴走を招く可能性があります。
- ADDR は、データの「住所」を知りたいときに使う。
- ENTRY は、処理(プロシージャ)の「入り口」を知りたいときに使う。
この原則をしっかりと心に刻み、改修や新規開発の際には、どちらの関数が適切かを慎重に判断してください。特に、レガシーコードの調査においては、これらの関数がどのように使われているかを見抜くことが、バグの温床を見つけ出す鍵となります。
もし、これらの関数を使ったコードに出くわして、どうしても理解できない場合は、迷わず先輩や同僚に相談すること。そして、自分でコードを書く際には、必ずコメントをしっかりと残し、後から自分が見ても理解できるように心がけましょう。それが、基幹システムを長きにわたって支えるエンジニアとしての責務ですからな。
今日の話はここまでだ。しっかり復習して、明日の業務に活かしてくれることを期待しているぞ!
