おい、最近オープン系との連携バッチや、Javaへのリライト案件が増えてきたからって、PL/Iの文字列処理を「ただの文字の入れ物だろ」となめてかかってないか?
メインフレームの現場で30年、数々の修羅場をくぐってきた私から言わせてもらうと、PL/Iの `CHARACTER(n)` と Java の `String` や `char[]` のメモリ管理の思想の違いを理解していない奴が多すぎる。ここを曖昧にしたままマイグレーションを進めると、本番稼働した途端に謎のメモリ破壊や、バッファオーバーラン、さらには文字化けの嵐に見舞われることになる。
今回は、PL/Iの固定長文字列の「本質」と、Javaへデータを渡す際やVSAMファイルを読み込む際に陥りがちな「死の罠」について、実務のコードを交えながら徹底的に叩き込んでやる。心して聞け。
—
1. なぜPL/Iの `CHARACTER(n)` と Java の `String` は水と油なのか?
まず、言語の成り立ちから頭を切り替える必要がある。
PL/Iの固定長文字列(`CHARACTER(n)`)の正体
PL/Iの `CHARACTER(n)` は、宣言された時点でメモリ上に厳密に $n$ バイトの領域が確保される。C言語やJavaのように「文字列の終わりを示す終端文字(NULL文字 `\0`)」なんて甘えたものは存在しない。
もし宣言より短いデータを代入すれば、容赦なく右側にスペース(EBCDICなら `X’40’`)がパディングされる。逆に、領域を超えるデータをぶち込めば、容赦なく切り捨て(トランケート)が発生する。
Javaの文字列(`String` / `char[]`)の正体
対してJavaの `String` はオブジェクトであり、内部的には可変長、あるいは文字コードに応じた可変バイト長で管理される。さらに、C言語由来のネイティブライブラリやJNI(Java Native Interface)経由でデータやり取りをする際、あるいはバイナリデータをストリームで受け渡す際、「NULL終端(`0x00`)」を前提とした処理が入り込む余地が大ありだ。
この「固定長・パディングあり・NULL終端なし」のPL/Iの世界から、「可変長・オブジェクト・NULL意識」のJavaの世界へデータを流し込むとき、悲劇の幕が上がる。
—
2. 実務で頻発する「バッファオーバーラン」と「謎のゴミデータ」の罠
例えば、メインフレーム側でVSAMファイルや順編成ファイルからレコードを読み込み、それを電文やJSON/XMLに加工してJava基盤へ飛ばすバッチを考えてみよう。
PL/Iの構造体(レコードレイアウト)では、以下のようにパディングスペースが詰められた状態でメモリ上に展開されている。
1
DCL 1 CUST_RECORD,
5 CUST_ID CHAR(5), キジュウの顧客ID
5 CUST_NAME CHAR(30), 顧客名(足りない分は右側スペース埋め)
5 CUST_STATUS CHAR(1); ステータスコード
これをそのままのノリでJava側にバイナリとして渡し、Java側で「C言語風のつもりで `byte[]` を文字列に変換しちゃえ」とやるとどうなるか。
Java側で `new String(bytes, “IBM-1039″等)` とやった場合、右側のパディングスペース(`X’40’`)がそのままStringの末尾にくっついてくる。さらに、もしJava側でC言語のネイティブライブラリを叩くようなアーキテクチャになっていて、PL/Iのバッファをそのまま `char` として扱わせようものなら、NULL終端がないためにメモリ上の隣の領域まで読み込み続け、バッファオーバーランを引き起こす。
逆に、Java側から送られてきた可変長文字列を、PL/I側の `CHAR(30)` に受け渡すときにも罠がある。Java側で末尾のスペースをトリムし忘れていたり、逆に想定より長い文字列が飛んできたとき、PL/Iのコンパイラは親切に(あるいは残酷に)右側を切り捨てる。
ここでONユニット(例外処理)を適切に組んでいないと、データがサイレントに欠損したまま後続の計算やDB更新が走り、翌朝の業務監査で青ざめることになるのだ。
—
3. 実践:安全なデータ制御を行うPL/Iコード例
百聞は一見にしかずだ。実際のバッチプログラムで、文字列長を厳密に制御し、組み込み関数(BUILTIN)を駆使して安全にデータをハンドリングするコードを見せておこう。
1
/ ———————————————— /
/ 顧客データ安全ハンドリング・バッチサンプル /
/ ———————————————— /
TEST_MIGRATION: PROC OPTIONS(MAIN);
/ 宣言部 /
DCL 1 IN_REC,
5 ID CHAR(5),
5 NAME CHAR(20);
DCL 1 OUT_RECORD,
5 O_ID CHAR(5),
5 O_NAME CHAR(20);
DCL
WK_NAME_LEN FIXED BIN(31,0),
IO_ERR_FLG BIT(1) INIT(‘0’B);
/ ONユニットによるファイル入出力エラーの捕捉 /
ON ENDFILE(SYSIN)
BEGIN;
IO_ERR_FLG = ‘1’B;
END;
/ メイン処理ループ /
DO WHILE(^IO_ERR_FLG);
GET FILE(SYSIN) DATA(IN_REC);
IF IO_ERR_FLG THEN LEAVE;
/ 【重要】PL/IのTRIMとLENGTHビルトイン関数を活用する /
/ CHARACTER(n)は末尾スペースを含むため、実効長を測るにはTRIMが必須 /
WK_NAME_LEN = LENGTH(TRIM(IN_REC.NAME));
/ デバッグ用:実効長がフィールド長を超えていないか、あるいは想定内かチェック /
IF WK_NAME_LEN > 20 THEN
CALL PLIRETV(-8); / 異常終了コードの設定例 /
/ 項目ごとの厳密な転記 /
O_ID = IN_REC.ID;
/ 履歴データやオープン系連携用に、右側スペースを明示的に整える /
O_NAME = TRIM(IN_REC.NAME);
/ 出力処理 /
PUT FILE(SYSPRINT) EDIT
(‘ID:’, O_ID, ‘ / NAME:’, O_NAME, ‘ (LEN:’, WK_NAME_LEN, ‘)’)
(A(4), A(5), A(8), A(20), A(6), F(3), A(1));
END;
RETURN;
END TEST_MIGRATION;
コードのポイント解説
1. `TRIM` ビルトイン関数の活用:
PL/Iの `CHAR(n)` 同士の代入であれば自動的にパディングされるが、Javaや他システムへ渡す前段階のデータ加工では、無駄なスペースを取り除くために `TRIM` を使うのが鉄則だ。
2. `LENGTH` との組み合わせ:
単に `LENGTH(IN_REC.NAME)` と書くと、文字の中身が何であれ宣言通りの `20` が返ってくる。実データの長さを知りたいときは必ず `LENGTH(TRIM(…))` とセットで使うこと。これがメインフレームプログラマの常識だ。
3. ONユニットによる堅牢なフロー制御:
ファイル読み込み時の異常やデータあふれを放置せず、`ON` ステートメントで処理フローを制御する。ここをサボると、ゴミデータが伝播してオープン系側のシステムを巻き込んだ大障害に発展する。
—
4. シニアアーキテクトからの現場の教訓
マイグレーションプロジェクトで最も恐ろしいのは、「動いているように見えるが、メモリの端っこでじわじわとデータを破壊しているバグ」だ。
- 「Javaだから自動でよしなにやってくれるだろう」という幻想を捨てること。
- PL/I側の固定長バッファのバイト数と、Java側のエンコーディング(UTF-8, Shift_JIS, IBM漢字など)ごとのバイト数の違いを設計書レベルで突き合わせること。
- NULL終端を想定する外部インターフェースとやり取りする場合は、必ずラッパー層でパディング文字の除去や終端の付与を厳密に行うこと。
この基本を忘れないでくれ。基幹システムを支えるのは、こうした泥臭くも正確なメモリとデータの管理の積み重ねなのだから。さて、次のタスクに取りかかろうか。
