こんにちは!IBMメインフレームの世界へようこそ。
普段はJavaやCOBOL、あるいは今風のモダンな言語をバリバリ書いている方にとって、レガシーの代名詞とも言える「PL/I(ピーエルワン)」のソースコードを開いた瞬間、「うわっ、なんだこれ…」と冷や汗が出てしまった方も多いのではないでしょうか。
特に、識別子のルールが独特だったり、見慣れないキーワードが並んでいたりすると、それだけで「触りたくないなあ」と及び腰になってしまいますよね。でも、大丈夫です。怖がる必要は全くありません。基本のキから一つずつ紐解いていけば、PL/Iほど懐が深く、ロジカルで面白い言語はありませんよ。
今回は、そんなPL/Iの数ある重要トピックの中から、「ENTRY属性と引数渡し(BYADDR / BYVALUE)の規約」を徹底的に解説します。JavaやCOBOLの感覚でいると見事にハマる「メインフレームの裏側の世界」を、一緒に覗いてみましょう!
—
1. 他言語とはココが違う?PL/Iの引数渡しの基本思想
JavaやC言語、COBOLなどでサブルーチン(関数や手続き)を呼び出すとき、「データをどうやって相手に渡すか」を意識したことはありますか?
近代的な言語の多くは、値そのものをコピーして渡したり(値渡し)、オブジェクトの参照先を渡したりしますよね。しかし、IBMメインフレームの土俵で長年培われてきたPL/Iには、「デフォルトですべての引数をアドレス(場所)で渡す」という強烈なこだわりがあります。
これをPL/Iの専門用語で `BYADDR`(バイ・アドレス:アドレス渡し) と呼びます。
🏠 例え話:合鍵を渡すか、コピーを渡すか
- BYVALUE(値渡し): 自宅の合板のミニチュア(値のコピー)を相手に手渡すイメージです。相手がそれをどういじろうと、あなたの本物の家には一切影響しません。
- BYADDR(アドレス渡し / PL/Iのデフォルト): あなたの「自宅の合鍵」を丸ごと相手に渡すイメージです。相手はあなたの家に入って、冷蔵庫の中身を勝手に書き換えること(副作用)ができてしまいます。
PL/Iはデフォルトで「合鍵(アドレス)」を渡すため、サブルーチン側で引数を書き換えると、呼び出し元の変数まで変わってしまうという特徴があります。これがバグの温床になることもありますが、巨大なデータを高速に処理するメインフレームならではの「メモリを無駄にコピーしない」ための知恵でもあるのです。
—
2. ENTRY属性と引数の「お約束」をコードで見てみよう
PL/Iで外部のサブプログラム(あるいは同じソース内の別プロシージャ)を呼び出すとき、コンパイラに対して「このプログラムはこういう引数を受け取りますよ」と宣言する必要があります。それが `ENTRY`属性 です。
百聞は一見にしかず。実際のコード例を見てみましょう。
1
/ ========================================================== /
/ 呼び出し元メインプログラム (MAIN_PG) /
/ ========================================================== /
MAIN_PG: PROC OPTIONS(MAIN);
DCL WS_DATA CHAR(10) INIT(‘IBM_MF’);
DCL WS_AMOUNT FIXED BIN(31,0) INIT(1000);
/ 外部サブルーチン SUB_CALC のENTRY属性宣言 /
/ BYADDR や BYVALUE の規約をここで明示します /
DCL SUB_CALC EXT ENTRY (
CHAR(10) BYADDR, / 1番目の引数:アドレス渡し /
FIXED BIN(31,0) BYVALUE / 2番目の引数:値渡し /
);
/ サブルーチンの呼び出し /
CALL SUB_CALC(WS_DATA, WS_AMOUNT);
PUT SKIP LIST(‘呼び出し終了後のデータ: ‘ || WS_DATA);
RETURN;
END MAIN_PG;
ここで注目してほしいのが、`ENTRY` のカッコの中にある `BYADDR` と `BYVALUE` の指定です。
- 1番目の `WS_DATA` は `BYADDR`(省略時のデフォルトもこれ)。変数の場所(アドレス)をそのまま渡します。
- 2番目の `WS_AMOUNT` はあえて `BYVALUE` を指定しています。数値の「1000」という実体を値としてコピーして渡します。
—
3. サブルーチン側の受け皿(PROCEDURE)の書き方
では、呼び出される側のサブルーチン(`SUB_CALC`)はどのように書くのでしょうか。ここが他言語経験者が最も戸惑うポイントです。呼び出し元と受け取り側で規約が一致していないと、メインフレームが異常終了(ABEND:S0C4など、おそるべきメモリアクセス違反)を起こします。
1
/ ========================================================== /
/ 呼び出し先サブプログラム (SUB_CALC) /
/ ========================================================== /
SUB_CALC: PROC(P_DATA, P_AMOUNT)
OPTIONS(COBOL); / 例としてCOBOL規約やOS標準規約を意識 /
/ 引数のデータ属性と渡し方を受け取り側でも正確に宣言する /
DCL P_DATA CHAR(10) BYADDR;
DCL P_AMOUNT FIXED BIN(31,0) BYVALUE;
/ 処理ロジック /
PUT SKIP LIST(‘受け取ったテキスト: ‘ || P_DATA);
PUT SKIP LIST(‘受け取った数値: ‘ || P_AMOUNT);
/ P_DATAはBYADDR(アドレス渡し)なので、ここで書き換えると /
/ 呼び出し元の WS_DATA の中身も書き換わってしまいます! /
P_DATA = ‘REWRITTEN’;
RETURN;
END SUB_CALC;
—
4. 他言語(COBOLやC、Java)連携時の痛い罠と注意点
レガシーマイグレーションの現場では、既存のCOBOL資産とPL/I資産が混在していたり、C言語で作られたユーティリティをPL/Iから呼び出したりすることがよくあります。その際、この引数渡しの規約を知らないと、原因不明のバグに何日も悩まされることになります。
① COBOLとの連携における罠
COBOLは基本的にすべてのデータを「参照(アドレス)」で受け渡します。そのため、PL/I側からCOBOLを呼び出す、あるいはその逆の場合、お互いのデータ定義と `BYADDR` の認識がズレていると、メモリが破壊されます。
特に、PL/I側でうっかり `BYVALUE` を指定して数値や文字を渡してしまうと、COBOL側は「アドレスが渡ってきた」と勘違いしてその値をメモリ番地として解釈し、S0C4 ABEND(保護例外)を引き起こして即死します。
② C言語やJava(JNI経由など)連携における罠
C言語やJavaの世界は、デフォルトが「値渡し(BYVALUE)」です。
もしPL/IからC言語の関数を呼び出す場合、PL/I側で明示的に `BYVALUE` を指定してあげないと、C言語側には「変数の値ではなく、変数が置いてあるメモリのアドレス値」が届いてしまい、プログラムが盛大に暴走します。
> 💡 アーキテクトからのアドバイス
> 他言語と連携するラッパープログラムを書くときは、必ず「呼び出し先が何を期待しているか(アドレスか、値か)」を確認し、PL/Iの `ENTRY` 宣言に `BYADDR` / `BYVALUE` を絶対に省略せずに明記するのが、デバッグ地獄を回避する唯一にして最高の防衛策です。
—
まとめ
いかがでしたでしょうか?
PL/Iの `ENTRY` 属性と `BYADDR / BYVALUE` の規約は、一見すると古臭く面倒くさいルールに見えるかもしれませんが、裏を返せば「コンピュータのメモリ構造をダイレクトに制御できる、非常にパワフルで精密な仕組み」です。
「合鍵を渡しているのか、コピーを渡しているのか」
このイメージさえ頭の中に持っておけば、レガシーなPL/Iコードを読むのも、新しいモダナイゼーションの設計をする también(おっと、つい言葉が)怖くなくなりますよ。
基幹システムの海原へ漕ぎ出すあなたの背中を、少しでも押せたなら幸いです。それでは、また次回のレガシー・ラボでお会いしましょう!
