【実務・中級編】DEFINED属性によるメモリ領域のオーバーレイと別名定義 – PL/Iの基本構文とデータ制御実践ガイド

メインフレームの現場から:予約語なき世界と危険な果実「DEFINED属性」

おい、最近入った若手が「先輩、PL/Iって予約語がないって本当ですか? じゃあ変数名に `READ` とか `WRITE` って書いても怒られないんです文?」って聞いてきやがった。

「その通りだ」と私は答えた。COBOLやJavaみたいな近代言語の感覚でいると、PL/Iの懐の深さ(あるいは大雑把さ)には度肝を抜かれる。PL/Iには、いわゆる厳密な「予約語」が存在しない。コンテキスト(文脈)によって、それがキーワードなのかユーザー定義の識別子なのかをコンパイラが判断する仕組みになっている。だから、`IF IF = THEN THEN = ELSE;` なんていう悪夢のようなコードすら、文法上はコンパイルを通せてしまう(絶対に書くなよ!)。

この「何でもあり」な言語特性を最も象徴するのが、今回取り上げる `DEFINED` 属性 だ。別名(Alias)定義とも呼ばれ、同一のメモリ領域を異なるデータ型や構造で無理やり共有させるための強力な機能である。

C言語の `union` や、COBOLの `REDEFINES` 句に近いといえばピンと来るだろうか。しかし、PL/Iの `DEFINED` はそれ以上に自由度が高く、それゆえに現場の保守エンジニアを絶望のどん底に突き落とすポテンシャルを秘めている。

今日は、この `DEFINED` 属性の仕組みと、VSAMファイル入出力やレコード処理における実務での活用法、そして一歩間違えば夜間バッチを即座に異次元へ飛ばす「保守性低下の罠」について、現場のノウハウを交えてみっちり叩き込んでやろう。

1. `DEFINED` 属性の基本とメモリ・オーバーレイのメカニズム

`DEFINED` 属性(短縮形の `DEF` でも書けるが、可読性を考えて基本はフルスペルで書こうな)は、新しく定義する変数を、すでに存在する別の変数のメモリ領域に割り当てる機能だ。

例えば、物理的なレコードレイアウトが上流工程の仕様変更で「ある時は数値(固定小数点)であり、ある時は文字データ(キャラクタ)の塊として扱いたい」という、メインフレームのレガシーな現場ではよくある理不尽な要求に対して、この `DEFINED` が火を吹く。

データの見方を強制的に変える恐ろしさ

COBOLの `REDEFINES` は同じレコード構造体内での再定義に限られることが多かったが、PL/Iの `DEFINED` は、異なる構造体間や独立した変数間であっても、メモリのオフセットを合わせて無理やり重ね合わせることができる。

まずは、百聞は一見にしかず。実際のコードを見てみよう。

1
/ ================================================================= /
/ プログラム名: OVLSP01 – DEFINED属性によるメモリオーバーレイ実例 /
/ ================================================================= /
OVLSP01: PROC OPTIONS(MAIN);

/ 80バイトの未加工(RAW)レコード領域 /
DCL RAW_BUFFER CHARACTER(80);

/ RAW_BUFFERの先頭をヘッダ情報として別名定義 /
DCL 1 HEADER_AREA DEFINED(RAW_BUFFER),
5 H_TYPE CHARACTER(2), / レコード種別 /
5 H_SEQ_NO FIXED BIN(31), / シーケンス番号 /
5 H_FILLER CHARACTER(70); / 予備領域 /

/ 同じRAW_BUFFERの先頭をトランザクションデータとして別名定義 /
DCL 1 TRAN_AREA DEFINED(RAW_BUFFER),
5 T_CODE CHARACTER(2), / トランザクションコード /
5 T_AMOUNT FIXED DEC(11,2),/ 金額データ /
5 T_DETAILS CHARACTER(63); / 明細データ /

/ サンプルとしてRAW_BUFFERにスペースを埋める /
RAW_BUFFER = ‘ ‘;

/ ヘッダとして値を設定してみる /
H_TYPE = ‘HD’;
H_SEQ_NO = 12345;

/ ここで何が起きているか? /
/ 最初の物理バッファ(RAW_BUFFER)を書き換えただけであり、 /
/ 同一メモリを指している TRAN_AREA の中身も強制的に変化する。 /

DISPLAY(‘RAW_BUFFER : ‘ || RAW_BUFFER);
DISPLAY(‘T_CODE : ‘ || T_CODE);
/ T_AMOUNT は FIXED DEC(11,2) なので、H_SEQ_NO のバイナリ領域を /
/ 無理やりパック十進数として解釈するため、ゴミデータ(あるいは例外)になる /
DISPLAY(‘T_AMOUNT : ‘ || T_AMOUNT);

END OVLSP01;

このコードのミソは、`RAW_BUFFER` というひとつの80バイトのキャラクタ変数を、`HEADER_AREA` と `TRAN_AREA` という全く異なる構造体で「ハッキング」している点だ。

2. VSAMアクセスとレコード入出力における実践的活用

実際の基幹システム、例えば勘定系や物流系のオンライン・バッチ処理では、VSAM(KSDSやESDS)から読み込んだ可変長あるいは不特定フォーマットのレコードを解析する際に、この手法が使われてきた。

VSAMのレコード域(エリア)をそのまま `UNSPEC` や `DEFINED` で受けることで、レコード種別ごとの構造体キャストをノーコスト(物理的なデータコピーなし)で行うわけだ。

ここで、ONユニット(例外処理)と組み合わせて、安全に(あるいはスリリングに)レコードを処理する実用的なパターンを見てみよう。

1
/ ================================================================= /
/ プログラム名: VSAMOVL – VSAMレコードのDEFINEDによる動的解釈 /
/ ================================================================= /
VSAMOVL: PROC OPTIONSMAN;

DCL VSAM_FILE FILE RECORD SEQUENTIAL INPUT
ENVIRONMENT(BUFH(2));

/ 生のレコード受信用エリア(最大200バイト) /
DCL MASTER_REC CHARACTER(200);

/ 種別共通のワーキングエリア /
DCL 1 COMMON_HDR DEFINED(MASTER_REC),
5 REC_ID CHARACTER(1), / ‘1’:顧客, ‘2’:口座 /
5 REC_KEY CHARACTER(9); / 共通キー /

/ 顧客レコード用ビュー /
DCL 1 CUST_REC DEFINED(MASTER_REC),
5 C_ID CHARACTER(1),
5 C_KEY CHARACTER(9),
5 C_NAME CHARACTER(40), / 顧客名 /
5 C_BIRTH CHARACTER(8); / 生年月日 (YYYYMMDD) /

/ 口座レコード用ビュー /
DCL 1 ACCT_REC DEFINED(MASTER_REC),
5 A_ID CHARACTER(1),
5 A_KEY CHARACTER(9),
5 A_ACC_NO CHARACTER(12), / 口座番号 /
5 A_BALANCE FIXED DEC(11,2);/ 残高 /

DCL END_OF_FILE BIT(1) INIT(‘0’B);

/ 入出力異常時のONユニット定義 /
ON ENDFILE(VSAM_FILE) END_OF_FILE = ‘1’B;

OPEN FILE(VSAM_FILE);

DO WHILE (^END_OF_FILE);
READ FILE(VSAM_FILE) INTO(MASTER_REC);

IF END_OF_FILE THEN LEAVE;

/ レコードIDによる分岐処理 /
SELECT(REC_ID);
WHEN(‘1’)
CALL PROCESS_CUSTOMER;
WHEN(‘2’)
CALL PROCESS_ACCOUNT;
OTHERWISE
DISPLAY(‘UNKNOWN RECORD ID: ‘ || REC_ID);
END;
END;

CLOSE FILE(VSAM_FILE);
RETURN;

PROCESS_CUSTOMER: PROC;
/ CUST_REC のビューを通してフィールドにアクセス /
DISPLAY(‘CUSTOMER -> NAME: ‘ || C_NAME || ‘ BIRTH: ‘ || C_BIRTH);
END PROCESS_CUSTOMER;

PROCESS_ACCOUNT: PROC;
/ ACCT_REC のビューを通してフィールドにアクセス /
DISPLAY(‘ACCOUNT -> ACC#: ‘ || A_ACC_NO || ‘ BALANCE: ‘ || A_BALANCE);
END PROCESS_ACCOUNT;

END VSAMOVL;

この実装は、データ移動命令(`MOVE` や代入)を発生させずにポインタの概念(物理アドレスの共用)で型キャストを行っているため、処理性能の観点からは非常に効率が良い。大型汎用機の限られたCPUサイクルを極限まで絞り取る必要があった時代には、まさに黄金のテクニックだった。

3. 保守性の低下リスクと「絶対にやってはいけない」アンチパターン

さて、ここからがベテランからの本当の忠告だ。
「動くからといって、無闇に `DEFINED` を使うな」

現代のメインフレーム開発において、`DEFINED` 属性は「諸刃の剣」どころか、下手をすると保守担当者の首を刎ねるギロチンと化す。なぜか? 理由は以下の3点に集約される。

① データ依存関係のブラックボックス化

ある変数を修正したつもりが、メモリ上で重なっている全く別の変数の値が意図せず書き換わる(サイレント・コラプション)。コンパイラや静的解析ツールでも、この意図しないメモリ共有によるバグは追いにくい。特に、構造体のパディング(境界調整による空白バイト)を考慮せずに `DEFINED` を組むと、コンパイラや最適化オプション(`OPT` レベル)の変更によって、予期せぬオフセットずれを起こす。

② マイグレーション時の大惨事

将来的に、このシステムをオープン系(JavaやC#、あるいはクラウド上のモダンな言語)にマイグレーションする際、この「メモリの強制オーバーレイ」に依存したロジックは、自動変換ツールの解析を完全にフリーズさせる。直訳が不可能で、全手書きでのリライトが必須になるため、移行コストが跳ね上がる。

③ デバッグの難易度(スナップ・ダンプの解析地獄)

夜間バッチが `S0C4`(ストレージ保護例外)や `S0C7`(データ例外)でアブノーマルエンド(異常終了)した際、シックス・ダンプ(Core Dump)を覗いても、`MASTER_REC` の中身が顧客なのか口座なのか、あるいはゴミなのかの判別が極めて困難になる。`DEFINED` はデータ構造の型安全性を完全に破壊するからだ。

悪い例:やってはいけないネストした複雑な `DEFINED`

1
/ こんなコードを書いた奴は、次の日の朝礼後に私から呼び出しだ /
DCL A CHARACTER(10) INIT(‘1234567890’);
DCL B FIXED BIN(31) DEFINED(A);
DCL C CHARACTER(4) DEFINED(B); / 泥沼の多重定義 /

このようなコードは、コンパイル時に怒られなくとも、メンテする人間が全員鬱病になる。

4. アーキテクトからの提言:現代における住み分け

もし君が今、新規にPL/Iプログラムを書く、あるいは既存のレガシープログラムを大規模改修する立場にあるなら、以下の原則を胸に刻んでほしい。

1. 基本は UNION 構造(あるいは通常の代入)を使う
近代的なPL/I(Enterprise PL/Iなど)では、`UNION` 属性がサポートされている。`DEFINED` よりもスコープやデータ構造の整合性がコンパイラによって担保されやすいため、メモリを共有したい場合は可能な限り `UNION` を検討せよ。
2. どうしても `DEFINED` を使う場合のドキュメント化
物理レイアウト図(何バイト目から何バイト目までがどの変数に割り当てられているか)を、ソースコードのヘッダコメントにアスキーアートなどで必ず残せ。後任のプログラマへの最低限の慈悲だ。
3. 型安全性の意識を持つ
「バッチの処理時間を0.1秒削るために可読性を投げ捨てる」という昭和の悪しきマインドセットはもう捨てろ。今のメインフレームのハードウェア(z16など)は、素直に構造体をコピーしてもビクともしないほど高速だ。コードの安全性を最優先せよ。

PL/Iの自由度の高さは美徳であり、同時に呪いでもある。
「予約語がない」からこそ、プログラマの品格と技量がコードの端々に現れる。

先輩風を吹かせるわけではないが、次に君が `DEFINED` をコードにタイピングしようと思ったときは、一度手を止めて自問してほしい。「本当にこのオーバーレイは必要か? 普通の構造体代入で代替できないか?」と。

その一手間が、将来の夜間障害を防ぎ、君自身の平和な睡眠時間を守ることになるのだから。

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