おい、最近配属された若手が「先輩、PL/Iの構造体を丸ごとファイルに出力したいんですけど、`WRITE`文でそのまま書けませんよね? 一個ずつフィールドを詰めるの面倒くさいです」って言ってきたんだ。
……お前な、甘いこと言うなよ。
C言語やJavaのノリでメインフレームの基幹系コードを触ると、足元をすくわれるどころか、夜間バッチを盛大に吹っ飛ばして夜食のカップ麺をすする羽目になるぞ。
PL/Iにはだな、「識別子(変数名)の命名規則の緩さ」と表裏一体の、強力かつ危険な奥義がある。それが今回解説する `STRING` ビルトイン関数だ。これを使えば、階層構造を持つ構造体全体を、あたかも単なる文字の塊(ストリング)であるかのように一括操作できる。
だがな、この機能、メモリの裏側の仕組みを知らずに使うと、パディング(隙間)の罠や文字コード変換の呪いにハマり、本番環境でデータ化けという名のホラーショーを引き起こすことになる。
今日は、長年メインフレームの修羅場をくぐり抜けてきた俺が、VSAMへのレコード入出力やONユニットでのエラー制御を交えながら、`STRING` 関数の真髄を叩き込んでやる。しっかりついてこい。
—
1. なぜPL/Iには「予約語」がないのか?と`STRING`の基本
まず大前提として、PL/Iという言語の恐ろしい(そして便利な)仕様についておさらいしておこう。
C言語やCOBOLとは違い、PL/Iには「言語予約語」というものが厳密には存在しない。`IF` や `READ` といったキーワードですら、文脈によっては「ただの変数名」として定義できてしまう。
この柔軟性は、識別子の命名規則にも表れており、最大31文字までの英数字とブレーク文字(アンダーバー `_` など)で自由に変数を作れる。
そして、この自由な世界において、構造体(STRUCTURE)をひとまとめにするのが `STRING` ビルトイン関数だ。
`STRING` 関数の正体
`STRING(構造体名)` は、指定した構造体(またはその一部)のメモリ領域を、データ型や境界調整の壁を無視して、ただの連続した文字ビット列(BITまたはCHARACTER)として直列化(フラット化)する機能だ。
例えば、電文の送受信や、VSAMのKSDS(KEY SEQUENCED DATA SET)へのレコード一括書き込みの際、個別のフィールドをちまちま動かさずに、一撃でワークエリアにロードしたり吐き出したりできる。
—
2. 構造体のメモリレイアウトと「パディング」の恐怖
ここで実務上の最大の罠について話そう。
「構造体を `STRING` で固めれば、宣言した通りのバイト数で文字列になる」と思ったら大間違いだ。メインフレームのハードウェア(IBM Zのアーキテクチャ)は、CPUがメモリにアクセスする効率を最大化するため、境界整列(Alignment)というルールを強制する。
- `FIXED BINARY(31)`(フルワード)は、4の倍数のメモリアドレスから始まるように配置される。
- そのため、奇数バイトの文字項目の直後に数値項目が来ると、コンパイラが勝手に「パディング(意味のない空白やヌルバイトの隙間)」を挿入する。
この状態で `STRING` 関数を使うと、人間が見えない「見えないゴミ」がデータの間に挟まったまま文字列化される。これを意識せずにVSAMファイルに書き込んだり、外部システムへJSONや固定長電文として投げたりすると、受信側で「項目がズレる」という大惨事になるわけだ。
—
3. 実践:`STRING` 関数を用いた構造体の一括操作とVSAM入出力コード
百聞は一見にしかずだ。実際のバッチプログラムを想定したコードを見てみよう。
ここでは、顧客マスタのレコード(構造体)を `STRING` でまとめて処理し、さらに万が一のデータ異常に備えて `ON` ユニットで割り込みを制御する例を示す。
1
/ /
/ 顧客マスタ一括処理プログラム /
/ /
CUST_BATCH: PROC OPTIONS(MAIN);
/ 境界調整によるパディングを防ぐためにUNALIGNED属性を明示 /
DCL 1 CUST_REC UNALIGNED,
5 CUST_ID CHAR(6), / 顧客ID /
5 CUST_NAME CHAR(20), / 顧客名 /
5 CUST_AMT FIXED DEC(9,25),/ 取引金額(パック十進数)/
5 CUST_STATUS CHAR(1); / ステータス /
/ STRING関数を適用するための受け渡し用キャラクター変数 /
DCL WORK_BUF CHAR(100) INIT((100)’ ‘);
/ ファイル定義(VSAM KSDSを想定) /
DCL CUST_FILE FILE RECORD SEQUENTIAL UPDATE
ENV(FB BLKSIZE(800));
/ エラー制御用のフラグ /
DCL IO_ERR_FLG BIT(1) INIT(‘0’B);
/ ONユニットによる入出力例外(ERROR/ENDFILE)の捕捉 /
ON ENDFILE(Cust_File) BEGIN;
PUT SKIP LIST(‘— 全レコードの処理が完了しました —‘);
IO_ERR_FLG = ‘1’B;
END;
ON ERROR BEGIN;
PUT SKIP LIST(‘【致命的エラー】入出力またはデータ変換異常が発生しました。’);
/ 異常終了コードをOSに返して終了 /
STOP;
END;
/ ファイルのオープン /
OPEN FILE(CUST_FILE);
/ メインループ /
DO WHILE(^IO_ERR_FLG);
/ 擬似的にファイルからレコードを読み込んだと仮定 /
/ READ FILE(CUST_FILE) INTO(WORK_BUF); /
/ —————————————————- /
/ 【核心】STRING関数による一括アンパッキング(逆直列化) /
/ —————————————————- /
/ 物理的な文字バッファの内容を、構造体のレイアウトに一発で流し込む /
STRING(CUST_REC) = WORK_BUF;
/ ビジネスロジックの適用例:ステータスチェック /
IF CUST_STATUS = ‘9’ THEN DO;
PUT SKIP EDIT (‘無効な顧客です ID:’, CUST_ID) (A, A);
/ 必要であればここで編集を加える /
END;
/ データを加工して、再びSTRINGで一括シリアライズ /
Cust_Status = ‘1’; / 処理済みに更新 /
/ 構造体を丸ごと文字バッファに戻す /
WORK_BUF = STRING(CUST_REC);
/ ファイルへ書き込み(実際はREWRITEなどを使う) /
/ WRITE FILE(CUST_FILE) FROM(WORK_BUF); /
IO_ERR_FLG = ‘1’B; / 動作確認のため1回で抜ける /
END;
/ ファイルのクローズ /
CLOSE FILE(CUST_FILE);
PUT SKIP LIST(‘正常終了しました。’);
END CUST_BATCH;
—
4. このコードの要点と、ベテランからのアドバイス
ここで、後輩のバグを防ぐために、上記のコードで最も重要なポイントを2点解説しておこう。
① `UNALIGNED` 属性の指定を忘れるな
構造体定義の先頭にある `UNALIGNED`。これが今回の肝だ。
これを付けないと、前述したパディング(アライメント調整)が勝手に行われ、`STRING(CUST_REC)` のバイト長が想定以上に膨れ上がる。
外部ファイルや他システムとI/Fする構造体には、必ず `UNALIGNED` を付けること。これを忘れると、文字位置がズレて夜間運用部隊から叩き起こされる電話が鳴ることになるぞ。
② 型変換(コンバージョン)の罠
`STRING(CUST_REC) = WORK_BUF` の代入時、PL/Iコンパイラは暗黙の型変換を行うことがある。特に `FIXED DEC`(パック十進数)や `FIXED BIN` が構造体内に混ざっている場合、文字データ(`CHAR`)からバイナリやパック形式への変換エラー(CONVERSIONエラー)が発生しやすい。
そのため、サンプルコードのように `ON ERROR` ユニットを適切に配置し、異常発生時の挙動をハンドリングできるようにしておくことが、メインフレームプログラマとしての最低限の保険だ。
—
5. まとめ
`STRING` ビルトイン関数は、構造体というリッチなデータ構造を、低水準のバイト列として自在に操るための強力なナイフだ。
正しく使えば、面倒なフィールドごとのムーブ処理(COBOLでいう `MOVE CORRESPONDING` のような泥臭い処理)を排除し、洗練された簡潔なコードを書くことができる。
しかし、メモリレイアウトの理解(特に `UNALIGNED` の有無)を誤ると、一発でデータ破損を引き起こす劇薬でもある。
「なんとなく動いたから良し」ではなく、メモリの裏側で何が起きているのかをイメージしながらコードを書く。これぞ、現代を生き抜くメインフレーム・エンジニアの矜持だ。
さて、理屈はここまでだ。自分の担当しているバッチの構造体定義、もう一度見直してみろよ。アライメントの漏れ、見つかるかもしれないぜ?
