ONC+ 開発ガイド

付録 A XDR テクニカルノート

付録 A は、Sun が XDR (External Data Representation: 外部データ表現) 標準規約に準拠して開発したライブラリルーチンセットについてのテクニカルノートです。XDR ライブラリルーチンを使用すると、C プログラマは任意のデータ構造をマシンに依存しない形式で記述できます。

XDR の概要

遠隔手続き呼び出しのときにデータは XDR 標準形式で伝送されますので、XDR は Sun の RPC パッケージの重要な基本概念となっています。複数の異なるタイプのマシンからアクセスされる (読み書きされる) データの転送には、XDR ライブラリルーチンを使用する必要があります。

XDR は異なる言語、異なるオペレーティングシステム、異なるマシンアーキテクチャの間で機能します。ほとんどのユーザー (特に RPC ユーザー) は、「整数フィルタ」、「浮動小数点フィルタ」、「列挙型フィルタ」の節を読むだけで十分でしょう。RPC と XDR を別のマシン上に実装したいプログラマは、このテクニカルノートやプロトコルの仕様に関心を持たれるかも知れません。

RPC 呼び出しを行わない場合でも、rpcgen を使用して XDR ルーチンを書くことができます。

XDR ルーチンを使用する C プログラムはファイル <rpc/xdr.h> をインクルードしなければなりません。<rpc/xdr.h> には、XDR システムとの必要なインタフェースがすべて入っています。XDR ルーチンはすべてライブラリ libnsl.a に入っていますので、コンパイルは次のコマンドで実行します。

example% cc program.c

移植性を保つためには、各環境でさまざまな基準を守ってプログラミングしなければなりません。プログラミング方法の微妙な違いを見つけるのも必ずしも簡単とはいえませんが、それらが広範囲に渡って関連し合っています。以下に示すサンプルプログラム 例 A-1例 A-2 (テキスト行の読み込みと書き出しのプログラム) で考えてみましょう。


例 A-1 writer サンプルプログラム (初期状態)

#include <stdio.h>
 
main()         	/* writer.c */
{ 	int i;
 
for (i = 0; i < 8; i++) {
 		if (fwrite((char *) &i, sizeof(i), 1, stdout) != 1) {
 			fprintf(stderr, "failed!¥n");
 			exit(1);
 		}
 	}
 	exit(0);
} 


例 A-2 reader サンプルプログラム (初期状態)

#include <stdio.h>
 
main()       /* reader.c */
{
 	int i, j;
 
	for (j = 0; j < 8; j++) {
 		if (fread((char *) &i, sizeof(i), 1, stdin) != 1) {
 			fprintf(stderr, "failed!¥n");
 			exit(1);
 		}
 		printf("%ld ", i);
 	}
 	printf("¥n");
 	exit(0);
} 

この 2 つのプログラムは移植可能に見えます。なぜならば、(a) lint チェックをパスし、(b) 任意のハードウェアアーキテクチャ上で、ローカルに実行しても同じように動作するからです。

writer プログラムの出力を reader プログラムにパイプした場合も、SPARC と Intel の両方で同じ結果が得られます。

sun% writer | reader
0 1 2 3 4 5 6 7
sun%
intel% writer | reader
0 1 2 3 4 5 6 7
intel%

ローカルエリアネットワークと 4.2BSD の出現に伴って、「ネットワークパイプ」の概念が導入されました。すなわち、あるマシン上のプロセスで生成したデータを、別のマシン上のプロセスが使用するという概念です。writerreader のプログラムでも、ネットワークパイプを使用できます。最初に SPARC 上でデータを生成し、そのデータを Intel 上で使用したときの実行結果を次に示します。

sun% writer | rsh intel reader
0 16777216 33554432 50331648 67108864 83886080 100663296
117440512
sun% 

writer を Intel で実行し、reader を SPARC で実行した場合も同じ結果になります。この原因は、Intel と SPARC の int 型整数は、ワードサイズは同じでもデータのバイト順序が異なるためです。


注 -

16777216 は 224 です。4 バイトを逆順にすると、24 番目のビットに 1 が置かれます。


複数の異なるタイプのマシンでデータを共有するときは、データの可搬性についての注意が必要です。read()write() 呼び出しを XDR ライブラリルーチンの呼び出しで xdr_int() に置き換えることにより、データが可搬なプログラムになります。xdr_int() は、int 型の整数の外部用の標準的な表現を扱うことができるフィルタです。writerプログラムの改良版を例 A-3に示します。


例 A-3 writer サンプルプログラム (XDR 修正バージョン)

#include <stdio.h>
#include <rpc/rpc.h> /* XDR は RPC のサブライブラリ */
 
main()       /* writer.c */
{
 	XDR xdrs;
 	int i;
 
xdrstdio_create(&xdrs, stdout, XDR_ENCODE);
 	for (i = 0; i < 8; i++) {
 		if (!xdr_int(&xdrs, &i)) {
 			fprintf(stderr, "failed!¥n");
 			exit(1);
 		}
 	}
 	exit(0);
} 

例 A-4 のサンプルプログラムは、reader プログラムを修正したものです。


例 A-4 reader サンプルプログラム (XDR 修正バージョン)

#include <stdio.h>
#include <rpc/rpc.h> /* xdr は rpc のサブライブラリ */
 
main()                /* reader.c */
{
 	XDR xdrs;
 	int i, j;
 
xdrstdio_create(&xdrs, stdin, XDR_DECODE);
 	for (j = 0; j < 8; j++) {
 		if (!xdr_int(&xdrs, &i)) {
 			fprintf(stderr, "failed!¥n");
 			exit(1);
 		}
 		printf("%ld ", i);
 	}
 	printf("¥n");
 	exit(0);
} 

変更したプログラムを SPARC 上で実行した結果、Intel 上で実行した結果、および SPARC からIntel にパイプした結果を次に示します。

sun% writer | reader
0 1 2 3 4 5 6 7
sun%
intel% writer | reader
0 1 2 3 4 5 6 7
intel%
sun%  writer | rsh intel reader
0 1 2 3 4 5 6 7
sun%
 

注 -

整数データの移植問題は、データの移植問題のごく簡単な一例にすぎません。どのデータ構造体にも移植性の問題があり、特にデータ境界とポインタが大きな問題になります。ワード境界の違いにより、マシンごとに構造体のサイズが異なる可能性があります。ポインタは使用するには便利ですが、ポインタが定義されたマシンの外部では何の意味も持ちません。


データの標準形式

XDR のアプローチは、データを 1 つの標準形式に統一するというものです。すなわち、XDR では、1 つのバイト順序、1 つの浮動小数点形式 (IEEE)、というように標準形式が定義されています。どのマシンで実行するプログラムでも XDR を使用すれば、データがローカルデータ形式から XDR 標準データ形式に変換されるので、移植可能なデータを作成できます。同様に、どのマシンで実行するプログラムでも XDR を使用すれば、データが XDR 標準データ形式からローカルデータ形式に変換されるので、移植可能なデータを読み込むことができます。1 つの標準形式を使用することにより、移植可能データを作成し送信するプログラムと、移植可能データを受信し使用するプログラムとが完全に切り離されます。新たなマシンや新たな言語が加わっても、移植データを作成し使用する既存のプログラムグループには何の影響もありません。新たなマシンは、標準データ形式と自身のローカルデータ形式との変換方法を「学ぶ」ことにより、既存のプログラムグループとの通信が可能になります。新たなマシンでは、他のマシンのローカルデータ形式を学ぶ必要がありません。これと同様に、他のマシン上で実行中の既存のプログラムグループでも、新たなマシンのローカルデータ形式を学ぶ必要がありません。新たなマシンが生成した移植可能なデータは、既存プログラムが既に理解している標準データ形式に従っているので、そのまま読み込むことができるからです。

XDR の標準化アプローチには、さまざまな先例があります。たとえば、TCP/IP、UDP/IP、XNS、Ethernet、および、ISO の OSI 参照モデルの第 5 層より下のプロトコルはすべて標準プロトコルです。標準化アプローチの利点は単純であることです。XDR の場合も、一連の変換ルーチンを一度書いてしまえばそれをずっと使用できます。標準化アプローチには欠点もありますが、実際に使用するデータ伝送アプリケーションでは大きな問題ではありません。たとえば、バイト順序が Intel の 2 つのマシン間で XDR 標準を使用して整数データを伝送するとします。送信側のマシンでは、整数データのバイト順を Intel から XDR のバイト順序に変換し、受信側のマシンではその反対の変換を行います。この 2 つのマシンのバイト順序は同じなので、本来このような変換は不要です。

標準データ形式とローカルデータ形式との変換に要する時間は重要ではありません。特に、分散型アプリケーションの場合は大した問題にはなりません。データ構造体を伝送する準備に要する時間のほとんどは、データ変換ではなく、データ構造体の各要素を取り出すのにかかります。たとえば、ツリー構造を伝送するには、葉の部分をすべてたどって、リーフレコード内の各要素をバッファにコピーして境界を合わせます。葉の部分を格納した記憶領域はその後解放しなければなりません。同様にツリー構造を受信するには、それぞれの葉の部分に対して記憶領域を割り当て、データをバッファからその記憶領域に移動して正しく境界を合わせ、葉と葉をリンクするポインタを設定します。どのマシンでも、標準データ形式との変換のあるなしに関係なく、データ構造体の走査とコピーにコストがかかります。分散型アプリケーションではこのような通信オーバヘッド、すなわち、送信側のプロトコル層を下ってネットワークを通り受信側のプロトコル層を上るのに時間がかかるため、標準データ形式とのデータ変換のオーバヘッドは相対的に小さくなります。

XDR ライブラリ

XDR ライブラリを使用すると、移植の問題が解決するばかりでなく、C の任意のデータ構造を一貫した、文書化された明確な方法で入出力することができます。したがって、ネットワーク上の複数のマシンでデータを共有する場合以外でもXDR ライブラリを使用する意味があります。

XDR ライブラリには、いくつか例を挙げてみるだけでも、文字列 (NULL で終わるバイト配列)、構造体、共用体、配列に対するフィルタルーチンがあります。ユーザーはさまざまなより基本的なルーチンを使用して、独自のXDR ルーチンを作成し、任意のデータ構造体 (配列の要素、共用体のアーム、他の構造体からポイントされるオブジェクト) を記述できます。構造体自体にも、任意の要素の配列や他の構造体へのポインタを持たせることができます。

2 つのサンプルプログラムをより詳しく見てください。XDR ストリーム作成ルーチンファミリがあります。ファミリの各メンバーはビットストリームの扱いが異なります。この例では、標準入出力ルーチンを使用してデータを操作しているので、xdrstdio_create() を使用します。XDR ストリーム作成ルーチンに渡す引数は、ルーチンの機能によって異なります。サンプルプログラムでは、初期化するXDR 構造体へのポインタ、入出力を行う FILE へのポインタ、処理内容の 3 つを引数として渡しています。処理内容は、writer プログラムではシリアライズするので XDR_ENCODEreader プログラムではデシリアライズするので XDR_DECODE を指定します。


注 -

RPC ユーザーは XDR ストリームを作成する必要はありません。RPC システムでストリームが作成され、ユーザーに引き渡されます。


xdr_int() プリミティブは、ほとんどの XDR ライブラリプリミティブと、すべてのクライアント作成の XDR ルーチンに共通の特徴を持っています。第 1 に、このルーチンはエラーが起こると FALSE(0) を返し、正常終了すると TRUE(1) を返します。第 2 に、各データ型 xxx ごとに対応する XDR ルーチンがあります。XDR ルーチンの形式を次に示します。

xdr_xxx(xdrs, xp)
   XDR *xdrs;
   xxx *xp;
{
}

このサンプルプログラムの場合は、xxx は int ですので、対応する XDR プリミティブは xdr_int() になります。クライアントも同様にして、任意の構造体 xxxを定義し、対応するルーチン xdr_int() を作成して、個々のフィールドごとにデータ型に一致する XDR ルーチンを呼び出します。どのデータ型の場合も、最初のパラメータ xdrs は隠されたハンドルとして、そのままプリミティブに渡します。

XDR ルーチンは両方向の変換を行います。すなわち、データをシリアライズするときもデシリアライズするときも同じルーチンを呼び出します。この機能は、移植可能データのソフトウェアエンジニアリングには不可欠な機能です。どちらの変換操作にも同一ルーチンを呼び出すという概念は、シリアライズしたデータはデシリアライズが可能であることをほぼ保証するものです。ネットワークデータを生成するときも使用するときも同じルーチンが使用できます。これは、ルーチンでは必ず、オブジェクト自体ではなくオブジェクトへのポインタを渡すことにより実現されます。デシリアライズの場合だけは、オブジェクト自体が変更されます。この機能は、簡単なサンプルプログラムではわかりませんが、より複雑なデータ構造体をマシン間で引き渡すときに、この機能の価値が明らかになります。必要な場合は、XDR の処理内容を取り出すこともできます。詳細は、「処理内容」の節を参照してください。

もう少し複雑な例を以下に示します。ある人の総資産と負債のデータをプロセス間で交換するとします。このデータは、次のような独自のデータ型が必要なほど重要であるとします。

struct gnumbers {
   int g_assets;
   int g_liabilities;
};

このデータ型に対応する XDR ルーチンでは、この構造体を次のように記述します。

bool_t                      /* 正常終了では TRUE、異常終了では FALSE */
xdr_gnumbers(xdrs, gp)
   XDR *xdrs;
   struct gnumbers *gp;
{
   if (xdr_int(xdrs, &gp->g_assets) &&
         xdr_int(xdrs, &gp->g_liabilities))
      return(TRUE);
   return(FALSE);
}

引数 xdrs は値を調べたり変更したりすることなく、そのままサブコンポーネントルーチンに渡していることに注意してください。XDR ルーチンを呼び出すたびにその戻り値を調べ、異常終了している場合はすぐに FALSE を返して終了しなければなりません。

この例では、bool_t 型が TRUE (1)FALSE (0) のどちらかの値だけをとる整数として宣言されていることがわかります。このマニュアルでは次の定義を使用します。

#define bool_t int
#define TRUE 1
#define FALSE 0

これらの取り決めを使用すると、xdr_gnumbers() は次のように書き換えられます。

xdr_gnumbers(xdrs, gp)
   XDR *xdrs;
   struct gnumbers *gp;
{
   return(xdr_int(xdrs, &gp->g_assets) &&
            xdr_int(xdrs, &gp->g_liabilities));
} 

このマニュアルでは、両方のコーディング形式を使用します。

XDR ライブラリのプリミティブ

この節では、XDR プリミティブの概要を述べます。始めにメモリー割り当てと基本データ型を説明し、次に合成データ型を説明します。最後に、XDR ユーティリティについて説明します。XDR のプリミティブとユーティリティのインタフェースはインクルードファイルで定義されています。<rpc/xdr.h><rpc/rpc.h> から自動的にインクルードされます。

XDR ルーチンで必要なメモリー

XDR ルーチンを使用するときは、前もってメモリーを割り当てておかなければならない (または、必要なメモリーサイズを決定しておかなければならない) 場合があります。XDR 変換ルーチンで使用するメモリーの割り当てや割り当て解除が必要なときのために、xdr_sizeof() というルーチンが提供されています。xdr_sizeof() は、XDR フィルタルーチン (func()) がデータ (data()) の符号化や復号化で使用するバイト数を返します。xdr_sizeof() が返す値には、RPC ヘッダーやレコードマークは含まれていませんので、必要なメモリーサイズを正確に求めるには、それらのバイト数も加えなければなりません。エラーが起こった場合、xdr_sizeof() はゼロを返します。

xdr_sizeof(xdrproc_t func, void *data)

xdr_sizeof() は、RPC 環境以外で XDR を使用するアプリケーションでメモリーを割り当てるとき、トランスポートプロトコルを選択するとき、下位レベルの RPC を使用してクライアント作成関数やサーバー作成関数を実行するときに特に便利です。

例 A-5例 A-6で、xdr_sizeof()の 2 通りの使用方法を説明します。


例 A-5 xdr_sizeof() の使用例 1

#include <rpc/rpc.h>
 
/*
 *	この関数への入力引数は、CLIENT ハンドル、XDR 関数、XDR 変換を行う
 *	データへのポインタです。XDR 変換を行うデータが、CLIENT ハンドルに
 *	結合しているトランスポートで送信可能な大きさの場合は TRUE、
 *	大き過ぎて送信不可能な場合は FALSE を返します。
 */
bool_t
cansend(cl, xdrfunc, xdrdata)
	CLIENT *cl;
	xdrproc_t xdrfunc;
 void   *xdrdata;
{
	int fd;
 struct t_info tinfo;
 
	if (clnt_control(cl, CLGET_FD, &fd) == -1) {
		/* ハンドルの clnt_control() エラー */
 	return (FALSE);
	}
 
	if (t_getinfo(fd, &tinfo) == -1) {
		/* ハンドルの t_getinfo() エラー */
 	return (FALSE);
	} else {
		if (tinfo.servtype == T_CLTS) {
			/*
			 * 現在使用しているのは非接続型トランスポートです。
 		 * xdr_sizeof() を使用して、メモリー要求がこのトランスポートでは 
			 * 大き過ぎないか調べます。
 		 */
			switch(tinfo.tsdu) {
				case 0:                      /* TSDU の概念なし */
				case -2:                       /* 通常データの送信不可能 */
					return (FALSE);
 				break;
				case -1:                        /* TSDU サイズの制限なし */
					return (TRUE);
					break;
 			default:
					if (tinfo.tsdu < xdr_sizeof(xdrfunc, xdrdata))
 					return (FALSE);
					else
						return (TRUE);
			}
		} else
			return (TRUE);
	}
}

例 A-6 は、xdr_sizeof() の使用例 2 になります。


例 A-6 xdr_sizeof() の使用例 2

#include <sys/statvfs.h>
#include <sys/sysmacros.h>
 
/*
 *	この関数への入力引数は、ファイル名、XDR 関数、XDR 変換を行うデータへの
 *	ポインタです。この関数は、ファイルが置かれているファイルシステムに、
 *	データを XDR 変換するのに必要な空間が残っていれば TRUE を返します。
 *	ファイルシステムに関して statvfs(2) で得られる情報はブロック数単位
 *	なので、xdr_sizeof() の戻り値もバイト数からディスクブロック数に
 *	変換しなければならないことに注意してください。
 */
bool_t
canwrite(file, xdrfunc, xdrdata)
	char	     *file;
	xdrproc_t xdrfunc;
	void     *xdrdata;
{
 struct statvfs s;
 
	if (statvfs(file, &s) == -1) {
		/* ハンドルの statvfs() エラー */
		return (FALSE);
	}
 
	if (s.f_bavail >= btod(xdr_sizeof(xdrfunc, xdrdata)))
		return (TRUE);
 else
		return (FALSE);
}
 

整数フィルタ

XDR ライブラリでは、整数を対応する外部表現に変換するプリミティブが提供されます。プリミティブが変換の対象とする整数は、次の組み合わせで表されます。

[signed, unsigned] * [short, int, long]

具体的には次の 8 つのプリミティブが提供されています。

bool_t xdr_char(xdrs, op)
   XDR *xdrs;
   char *cp;
bool_t xdr_u_char(xdrs, ucp)
   XDR *xdrs;
  	unsigned char *ucp;
bool_t xdr_int(xdrs, ip)
  	XDR *xdrs;
  	int *ip;
bool_t xdr_u_int(xdrs, up)
  	XDR *xdrs;
  	unsigned *up;
bool_t xdr_long(xdrs, lip)
  	XDR *xdrs;
  	long *lip;
bool_t xdr_u_long(xdrs, lup)
  	XDR *xdrs;
  	u_long *lup;
bool_t xdr_short(xdrs, sip)
  	XDR *xdrs;
  	short *sip;
bool_t xdr_u_short(xdrs, sup)
  	XDR *xdrs;
  	u_short *sup;

最初の引数 xdrs は、XDR のストリームハンドルです。第 2 引数は、ストリームへ渡すデータのアドレス、または、ストリームからデータを受け取るアドレスです。どのルーチンも、変換に成功すれば TRUE、失敗すれば FALSE を返します。

浮動小数点フィルタ

XDR ライブラリでは、C の浮動小数点型データのプリミティブも提供されます。

bool_t xdr_float(xdrs, fp)
  	XDR *xdrs;
  	float *fp;
bool_t xdr_double(xdrs, dp)
   XDR *xdrs;
  	double *dp;
 

最初の引数 xdrs は、XDR のストリームハンドルです。第 2 引数は、ストリームへ渡す浮動小数点データのアドレスまたはストリームから浮動小数点データを受け取るアドレスです。どちらのルーチンも、変換に成功すれば TRUE、失敗すれば FALSE を返します。


注 -

数値の表現形式は、浮動小数点に関する IEEE 標準規約に従っているため、IEEE 準拠の表現形式からマシン固有の表現形式に復号化したり、暗号化したりすると、エラーが起こる場合があります。


列挙型フィルタ

XDR ライブラリでは、一般の列挙型に対するプリミティブを提供しています。このプリミティブでは、C の enum 型のマシン内部表現が C の整数と同じであるとみなしています。ブール型は enum 型の重要な一例です。ブール値の外部表現は常に TRUE (1)FALSE (0) です。

#define bool_t int
#define FALSE  0
#define TRUE   1
#define enum_t int
bool_t xdr_enum(xdrs, ep)
   XDR *xdrs;
   enum_t *ep;
bool_t xdr_bool(xdrs, bp)
   XDR *xdrs;
   bool_t *bp; 

第 2 引数 epbp は、ストリーム xdrs へ渡すデータのアドレス、または、ストリーム xdrs からデータを受け取るアドレスです。

データなしルーチン

ときには、データが一切渡されず要求されていなくても、XDR ルーチンを RPC システムに提供しなければならない場合があります。ライブラリでは、そのためのルーチンを提供しています。

bool_t xdr_void(); /* 常に TRUE を返す */ 

合成データ型フィルタ

合成データ型、または、複合データ型を変換するプリミティブは、これまでに説明したプリミティブより多くの引数を必要とし、より複雑な機能を実行します。この節では、文字列、配列、共用体、構造体へのポインタに対するプリミティブを説明します。

合成データ型のプリミティブでは、メモリー管理を使用する場合があります。多くの場合、XDR_DECODEを指定してデータをデシリアライズすると、メモリーが割り当てられます。そのため、XDR パッケージではメモリー割り当てを解除する方法を提供しなければなりません。メモリー割り当ての解除は、XDR_FREE という XDR 処理で行います。XDRの 処理内容には、XDR_ENCODEXDR_DECODEXDR_FREE の 3 つがあります。

文字列

C 言語では、文字列が、NULL コードで終わるバイトシーケンスと定義されています。NULL コードは、文字列の長さを求めるときは計算に入れません。ところが、文字列を引き渡したり操作したりするときは、文字へのポインタが使用されます。そのため、XDRライブラリでは文字列を文字シーケンスではなく、char * と定義しています。文字列の外部表現は、内部表現とは大きく異なります。

文字列は内部では文字へのポインタで表現されますが、外部では ASCII 文字シーケンスで表現されます。この 2 つの表現形式の間の変換は、ルーチン xdr_string() で実行します。

bool_t xdr_string(xdrs, sp, maxlength)
   XDR *xdrs;
   char **sp;
   u_int maxlength;

最初の引数 xdrs は、XDR のストリームハンドルです。第 2 引数 sp は文字列へのポインタ (データ型は char **) です。第 3 引数 maxlength は、符号化または復号化の対象とする最大バイト数です。通常、この値はプロトコルで決まります。たとえば、あるプロトコル仕様では、ファイル名は最大 255 文字までとされています。文字数が maxlength の値を超えていれば FALSE、超えていなければ TRUE が返されます。

xdr_string() の機能は、この節でこれまでに説明した他の変換ルーチンと同様です。処理内容が XDR_ENCODE の場合は最も簡単です。引数 sp はある長さの文字列を指しています。この文字列の長さが maxlength を超えていなければ、この文字列がシリアライズされます。

デシリアライズの場合はもう少し複雑です。最初に、ストリームから取り込む文字列の長さを決定します。文字列の長さは maxlength を超えることはできません。次に sp をデレファレンスします。その値が NULL の場合は、適切なサイズの文字列を割り当てて、*sp がその文字列を指すように設定します。*sp の元々の値が NULL でない場合は、デシリアライズしたデータを入れるターゲットエリアが既に割り当てられており、maxlength 以下の長さの文字列をそこに格納できるものとみなします。どちらの場合も、文字列が復号化されてターゲットエリアに保存されます。次に、文字列の最後に NULL コードが付加されます。

XDR_FREE 処理の場合は、sp をデレファレンスして文字列を取り出します。その文字列が NULL 文字列でなければ、領域を解放して *spNULL に設定します。この処理を実行するときは、xdr_string() は引数 maxlength を無視します。

空の文字列 ("") を XDR 変換することはできますが、NULL 文字列を XDR 変換はできません。

バイト配列

文字列よりも可変長バイト配列を使用する方が便利な場合があります。バイト配列は、次の 3 つの点で文字列と異なっています。(1) 配列の長さ(バイトカウント) を符号なし整数として明示的に保持している。(2) バイトシーケンスが NULL コードで終了しない。(3) データの外部表現と内部表現が一致する。バイト配列の内部表現と外部表現との変換には、プリミティブ xdr_bytes() を使用します。

bool_t xdr_bytes(xdrs, bpp, lp, maxlength)
   XDR *xdrs;
   char **bpp;
   u_int *lp;
   u_int maxlength;

このルーチンの第 1、第 2、第 4 引数はそれぞれ、xdr_string() の第 1、第 2、第 3 引数と同じです。シリアライズの場合は、lp をデレファレンスしてバイトシーケンスの長さを得ます。デシリアライズの場合は、*lp にバイトシーケンスの長さが設定されます。

配列

XDR ライブラリパッケージでは、任意の要素で構成される配列を処理するプリミティブが提供されています。xdr_bytes() ルーチンは一般の配列のサブセットを処理します。すなわち、xdr_bytes() ルーチンでは、配列要素のサイズは 1 に決まっており、各要素の外部記述も組み込まれています。一般の配列に対するプリミティブ xdr_array() の引数は、xdr_bytes() の引数より 2 つ多く、配列要素のサイズと、各要素を変換する XDR ルーチンとが渡されます。渡されたXDR ルーチンは、配列の各要素の符号化または復号化のときに呼び出されます。

bool_t
xdr_array(xdrs, ap, lp, maxlength, elementsize, xdr_element)
   XDR *xdrs;
   char **ap;
   u_int *lp;
   u_int maxlength;
   u_int elementsize;
   bool_t (*xdr_element)();

引数 ap は、配列へのポインタのアドレスです。配列をデシリアライズするときに *apNULL の場合は、適切なサイズの配列が割り当てられ、*ap はその配列を指すように設定されます。配列をシリアライズするときは、配列の要素数を *lp から取り出します。配列をデシリアライズするときは、*lp には配列の長さが設定されます。引数 maxlength は、配列に入れることができる最大要素数です。elementsiz は、配列の各要素のサイズ (バイト数) です。C の sizeof() 関数を使用してこの値を調べることができます。xdr_element() ルーチンは、配列の各要素のシリアライズ、デシリアライズ、解放を行うときに呼び出されます。

このほかの合成データ型の説明の前に、3 つのサンプルプログラムを説明します。

配列変換サンプルプログラム 1

ネットワークに接続したマシンのユーザーは、次の 3 つの項目によって識別できます。(a) マシン名。たとえば、krypton。(b) ユーザーの UID。これについては、geteuid(2) のマニュアルページを参照してください。(c) ユーザーが所属するグループ番号。これについては、getgroups(2) のマニュアルページを参照してください。これらの識別情報を持つ構造体と、それに対する XDR ルーチンは例 A-7 のようにコーディングできます。


例 A-7 配列変換のサンプルプログラム 1

struct netuser {
 	char  *nu_machinename;
 	int   nu_uid;
 	u_int nu_glen;
 	int   *nu_gids;
 };
#define NLEN 255       /* マシン名は 255 文字以下 */
#define NGRPS 20       /* ユーザーが所属するグループ数は 20 以下 */
 
bool_t
xdr_netuser(xdrs, nup)
 	XDR *xdrs;
 	struct netuser *nup;
{
 	return(xdr_string(xdrs, &nup->nu_machinename, NLEN) &&
 		    xdr_int(xdrs, &nup->nu_uid) &&
 		    xdr_array(xdrs, &nup->nu_gids, &nup->nu_glen, NGRPS,
		               sizeof (int), xdr_int));
}

配列変換サンプルプログラム 2

ネットワークユーザーのグループは、netuser 構造体の配列で表すことができます。構造体の宣言と、それに対する XDR ルーチンは 例 A-8 のようになります。


例 A-8 配列変換のサンプルプログラム 2

struct party {
 
 	u_int p_len;
 	struct netuser *p_nusers;
};
#define PLEN 500 /* グループに所属するユーザー数の上限 */
bool_t
xdr_party(xdrs, pp)
 	XDR *xdrs;
 	struct party *pp;
{
 	return(xdr_array(xdrs, &pp->p_nusers, &pp->p_len, PLEN,
 	 sizeof (struct netuser), xdr_netuser));
}

配列変換サンプルプログラム 3

main に対するよく知られた引数 argcargv を持つ構造体を作成し、その構造体の配列にコマンドヒストリを保存することができます。構造体の宣言と、その XDR ルーチンは 例 A-9 のようになります。


例 A-9 配列変換のサンプルプログラム 3

struct cmd {
 	u_int c_argc;
 	char **c_argv;
};
#define ALEN 1000           /* argc は 1000 以下 */
 #define NARGC 100          /* 各コマンドの args は 100 以下 */
 
struct history {
 	u_int h_len;
 	struct cmd *h_cmds;
};
#define NCMDS 75            /* ヒストリは 75 コマンドまで */
bool_t
xdr_wrapstring(xdrs, sp)
 
 	XDR *xdrs;
 	char **sp;
{
 	return(xdr_string(xdrs, sp, ALEN));
 }
 
bool_t
xdr_cmd(xdrs, cp)
 
 	XDR *xdrs;
 	struct cmd *cp;
{
 	return(xdr_array(xdrs, &cp->c_argv, &cp->c_argc, NARGC,
 	        sizeof (char *), xdr_wrapstring));
}
bool_t
xdr_history(xdrs, hp)
 
 	XDR *xdrs;
 	struct history *hp;
{
 	return(xdr_array(xdrs, &hp->h_cmds, &hp->h_len, NCMDS,
 	        sizeof (struct cmd), xdr_cmd));
}

このプログラムで最もむずかしいのは、xdr_string() を呼び出すためのルーチン xdr_wrapstring() が必要な点です。xdr_array() が配列要素記述ルーチンを呼び出すときは引数が 2 つしか渡されないため、xdr_string() の第 3 引数を提供するルーチン xdr_wrapstring() が必要になります。

これまでの説明で XDR ライブラリの再帰的性質が明らかになりました。その他の合成データ型の変換も見てみましょう。

隠されたデータ

プロトコルによっては、サーバーからクライアントにハンドルが渡され、クライアントは後からハンドルをサーバーに送り返します。クライアントではハンドルの内容を調べることはなく、受け取ったものをそのまま送り返します。すなわち、ハンドルは隠されたデータ (内容が隠されたデータ) です。固定長の隠されたデータを記述するには、xdr_opaque() プリミティブを使用します。

bool_t
xdr_opaque(xdrs, p, len)
   XDR *xdrs;
   char *p;
   u_int len;

引数 p はデータのアドレスです。len は隠れたオブジェクトに入っているバイト数です。隠されたデータの定義からすると、実際に隠れたオブジェクトに入っているデータはマシン間で移植不可能です。

SunOS/SVR4 には、隠されたデータの操作用にもう 1 つのルーチンが提供されています。そのルーチン xdr_netobj()xdr_opaque() と同様にカウント付きの隠されたデータを送信します。 例 A-10 に、xdr_netobj() の構文を示します。


例 A-10 xdr_netobj() のサンプルプログラム

struct netobj {
	u_int   n_len;
 char    *n_bytes;
};
typedef struct netobj netobj;
 
bool_t
xdr_netobj(xdrs, np)
	XDR *xdrs;
	struct netobj *np;

xdr_netobj() はフィルタプリミティブで、可変長の隠されたデータとその外部表現との変換を行います。引数 npnetobj() 構造体のアドレスです。netobj() 構造体には、隠されたデータの長さと隠されたデータへのポインタが入っています。長さは、MAX_NETOBJ_SZ バイトを超えることはできません。変換に成功すれば TRUE、失敗すれば FALSE が返されます。

固定長配列

xdr_vector() ライブラリでは、例 A-11サンプルプログラムで示すように、固定長配列に対するプリミティブ xdr_vector()が提供されています。


例 A-11 xdr_vector() のサンプルプログラム

#define NLEN 255	/* マシン名は 255 文字以下 */
#define NGRPS 20	/* ユーザーは正確に 20 のグループに所属 */
 
struct netuser {
 	char *nu_machinename;
 	int nu_uid;
 	int nu_gids[NGRPS];
};
 
bool_t
xdr_netuser(xdrs, nup)
 	XDR *xdrs;
 	struct netuser *nup;
{
 	int i;
 
	if (!xdr_string(xdrs, &nup->nu_machinename, NLEN))
 		return(FALSE);
 	if (!xdr_int(xdrs, &nup->nu_uid))
 		return(FALSE);
 	if (!xdr_vector(xdrs, nup->nu_gids, NGRPS, sizeof(int),
 	     xdr_int))
 		return(FALSE);
 	return(TRUE);
}
 

識別型の共用体

XDR ライブラリは識別型の共用体もサポートしています。識別型の共用体は、C の共用体に、共用体の「アーム」を選択する enum_t 型の値が付加されたものです。

struct xdr_discrim {
  	enum_t value;
  	bool_t (*proc)();
};
 
bool_t
 xdr_union(xdrs, dscmp, unp, arms, defaultarm)
   XDR *xdrs;
   enum_t *dscmp;
   char *unp;
   struct xdr_discrim *arms;
  	bool_t (*defaultarm)(); /* may equal NULL */
  

このルーチンでは、最初に *dscmp にある要素識別子を変換します。要素識別子は常に enum_t 型です。次に、*unp にある共用体が変換されます。引数 armsxdr_discrim 構造体配列へのポインタです。各構造体には、[value,proc] のペアが入っています。共用体の要素識別子が対応する value と一致すれば、それに対する proc() が呼び出されて共用体が変換されます。xdr_discrim 構造体配列の終わりは、ルーチンの値が NULL(0) なので判別できます。arms 配列の中に要素識別子に一致するものがない場合は、defaultarm() 手続きが NULL でなければそれが呼び出されます。NULL の場合は FALSE を返します。

識別型の共用体サンプルプログラム

共用体のデータ型は、整数、文字へのポインタ (文字列)、gnumbers 構造体のどれかで、共用体と現在の型は構造体で宣言されています。宣言は次のようになります。

enum utype {INTEGER=1, STRING=2, GNUMBERS=3};
struct u_tag {
   enum utype utype;	/* 共用体の要素識別子 */
   union {
      int ival;
      char *pval;
      struct gnumbers gn;
   } uval;
};
  

例 A-12 では、合成データと XDR 手続きで、識別型共用体のシリアライズ (またはデシリアライズ) を行います。


例 A-12 XDR 識別型の共用体サンプルプログラム

struct xdr_discrim u_tag_arms[4] = {
 	{INTEGER, xdr_int},
 	{GNUMBERS, xdr_gnumbers}
 	{STRING, xdr_wrapstring},
 	{__dontcare__, NULL}
 	/* アームの最後は常に NULL の xdr_proc */
 }
 
bool_t
xdr_u_tag(xdrs, utp)
 	XDR *xdrs;
 	struct u_tag *utp;
{
 	return(xdr_union(xdrs, &utp->utype, &utp->uval,
	       u_tag_arms, NULL));
}

xdr_gnumbers() については「XDR ライブラリ」の節で説明し、xdr_wrapstring() はサンプルプログラム C に示しました。この例では、xdr_union()のデフォルトの arm 引数 (最後の引数) には NULL を渡しています。したがって、共用体の要素識別子の値は、u_tag_arms 配列に表示された値のどれかになります。また、この例から、arm 配列の要素はソートされていなくてもよいことがわかります。

要素識別子は、この例では連続した値を取っていますが、連続していなくてもかまいません。要素識別子の型の各要素に、明示的に整数値を割り当てておくと、要素識別子の外部表現として明記でき、異なる C コンパイラでも要素識別子が同じ値で出力されることが保証されます。

練習

この節の他のプリミティブを使用して xdr_union() を作成してください。

ポインタ

C では、構造体の中に、別の構造体へのポインタを入れることがよくあります。プリミティブ xdr_reference() を使用すると、そのように参照される構造体を簡単にシリアライズ (またはデシリアライズ) できます。

bool_t
xdr_reference(xdrs, pp, size, proc)
   XDR *xdrs;
   char **pp;
   u_int ssize;
   bool_t (*proc)();

引数 pp は構造体へのポインタのアドレスです。引数 ssize は構造体のサイズ (バイト数) です。C の sizeof() 関数を使用してこの値を調べることができます。引数 proc() は構造体を記述する XDR ルーチンです。データを復号化するとき、*ppNULL の場合は記憶領域が割り当てられます。

プリミティブ xdr_struct() では、構造体内の構造体を記述する必要はありません。ポインタだけで十分です。

練習

xdr_array() を使用して xdr_reference() を作成してください。


注意 - 注意 -

xdr_reference()xdr_array() のデータ外部表現は互換ではありません。


ポインタのサンプルプログラム

人の名前、および、その人の総資産と負債の入った gnumbers 構造体へのポインタとで構成される次のような構造体があるとします。

struct pgn {
   char *name;
   struct gnumbers *gnp;
};

これに対する XDR ルーチンは次のようになります。

bool_t
xdr_pgn(xdrs, pp)
   XDR *xdrs;
   struct pgn *pp;
{
   return(xdr_string(xdrs, &pp->name, NLEN) &&
      xdr_reference(xdrs, &pp->gnp, sizeof(struct gnumbers),
                    xdr_gnumbers));
}

ポインタセマンティクス

C のプログラマは多くのアプリケーションで、ポインタの値に 2 つの意味を持たせています。一番多い例としては、ポインタ値が NULL (ゼロ) の場合はデータが不要なことを意味する方法ですが、アプリケーションごとにさまざまな解釈ができます。実際に C プログラマは、ポインタ値の解釈をオーバロードすることにより、効率よく識別型共用体を実現しています。たとえば、サンプルプログラム E では、gnp のポインタ値を NULL にすれば、その人の総資産と負債が未知であると解釈できます。すなわち、ポインタ値には 2 つの意味があります。データがあるかどうかを示し、もしあるとしたらメモリーのどこにあるかを示します。リンクリストは、アプリケーション固有のポインタ解釈の極端な例です。

プリミティブ xdr_reference() はシリアライズの際に、値が NULL のポインタに特別な意味を持たせることができません。そのため、データをシリアライズするときに、xdr_reference() に値が NULL のポインタのアドレスを渡すと、多くの場合メモリーフォルトを引き起こし、UNIX システムではコアダンプが起こります。

xdr_pointer() では NULL ポインタも正しく処理できます。

フィルタ以外のプリミティブ

XDR ストリームの操作は、次に示すプリミティブで行います。

u_int xdr_getpos(xdrs)
   XDR *xdrs;
 
bool_t xdr_setpos(xdrs, pos)
   XDR *xdrs;
  	u_int pos;
 
xdr_destroy(xdrs)
  	XDR *xdrs;

ルーチン xdr_getpos()はデータストリーム内の現在位置を符号なし整数で返します。

警告 : XDR ストリームの中には、xdr_getpos()の返す値が意味を持たないものがあります。その場合は、-1 が返されます (ただし、-1 も正当な値です)。

ルーチンxdr_setpos() はストリーム位置を posに設定します。

警告 : XDR ストリームの中には、xdr_setpos() で位置設定ができないものもあります。その場合は、FALSE が返されます。指定した位置が適用範囲外の場合もエラーとなります。設定位置の適用範囲はストリームごとに異なります。

XDR ストリームを廃棄するには、プリミティブ xdr_destroy() を使用します。このルーチンを呼び出した後で、ストリームを使用した場合の動作は未定です。

処理内容

処理内容 (XDR_ENCODEXDR_DECODEXDR_FREE のどれか) を利用して XDR ルーチンを最適化したい場合があります。XDR の処理内容は、常に xdrs->x_op に入っています。「リンクリスト」の節には、xdrs->x_op フィールドを活用するサンプルプログラムが示されています。

ストリームへのアクセス

XDR ストリームは、適切な作成ルーチンを呼び出すことで得られます。作成ルーチンへの引数でストリームのさまざまな特性が決まります。現在、データのシリアライズとデシリアライズに使用できるストリームには、標準入出力 FILE ストリーム、レコードストリーム、UNIX ファイル、メモリーがあります。

標準入出力ストリーム

XDR ストリームは、xdrstdio_create() を使用して標準入出力とのインタフェースをとることができます。

#include <stdio.h>
#include <rpc/rpc.h> /* XDR は RPC のサブセット */
 
void
xdrstdio_create(xdrs, fp, xdr_op)
   XDR *xdrs;
  	FILE *fp;
   enum xdr_op x_op;

xdrstdio_create() は、xdrs が指す XDR ストリームを初期化します。XDR ストリームは、標準入出力ライブラリとのインタフェースが可能です。引数 fp はオープンしているファイル、x_op は XDR の処理内容です。

メモリーストリーム

メモリーストリームを作成すると、メモリーの特定領域とのデータストリームが使用できます。

#include <rpc/rpc.h>
 
void
xdrmem_create(xdrs, addr, len, x_op)
   XDR *xdrs;
  	char *addr;
  	u_int len;
  	enum xdr_op x_op;

xdrmem_create() ルーチンは、ローカルメモリー内の XDR ストリームを初期化します。引数 addr は使用するメモリーを指します。引数 len はメモリーの大きさ (バイト数) です。引数 xdrsx_op は、xdrstdio_create() の同名の引数と同じです。現在、RPC ではデータグラムの実現に xdrmem_create() を使用しています。TLI ルーチン t_sndndata() を呼び出す前に、完全な呼び出しメッセージ (または応答メッセージ) がメモリーに構築されます。

レコード (TCP/IP) ストリーム

レコードストリームは、レコードマーク標準の上に構築される XDR ストリームです。レコードマーク標準は、UNIX ファイル、または、4.2 BSD 接続インタフェースの上に構築されます。

#include <rpc/rpc.h>      /* XDR は RPC のサブセット */
 
xdrrec_create(xdrs, sendsize, recvsize, iohandle, readproc,
              writeproc)
   XDR *xdrs;
   u_int sendsize, recvsize;
  	char *iohandle;
  	int (*readproc)(), (*writeproc)();
 

xdrrec_create() は、任意の長さの双方向レコードシーケンスが可能な XDR ストリームインタフェースを提供します。レコードの内容は、XDR 形式のデータと考えられます。レコードストリームは、主に RPC と TCP 接続とのインタフェースとして使用されますが、通常の UNIX ファイルとのデータ入出力にも使用できます。

引数 xdrs は、これまでに説明した同名の引数と同じです。レコードストリームでは、標準入出力ストリームと同様のデータバッファリングを行います。引数 sendsizerecvsize には、それぞれ送信バッファと受信バッファのサイズ (バイト数) を指定します。この値がゼロ (0) の場合は、あらかじめ指定されたデフォルトのバッファサイズが使用されます。バッファにデータを読み込んだり、データをフラッシュしたりするときは、ルーチン readproc()または writeproc() を呼び出します。この 2 つのルーチンの使用方法と機能は、UNIXのシステムコール read()write() に似ていますが、第 1 引数には隠されたデータ iohandle を渡します。その後の 2 つの引数 (および nbytes) と、戻り値 (バイトカウント) はシステムルーチンと同じです。次の xxx()readproc() または writeproc() とすると、その形式は次のようになります。

/* 実際に伝送したバイト数を返す。エラーのときの戻り値は -1 */
int
xxx(iohandle, buf, len)
  	char *iohandle;
  	char *buf;
  	int nbytes;

XDR ストリームには、バイトストリームのレコードを区切る方法が提供されています。XDR ストリームを作成するのに必要な抽象データ型については、「XDR ストリームの作成」を参照してください。XDRストリームレコードを区切るのに使用する RPC プロトコルについては、「レコードマーク標準」を参照してください。

レコードストリームに特有なプリミティブは以下のとおりです。

bool_t
xdrrec_endofrecord(xdrs, flushnow)
   XDR *xdrs;
   bool_t flushnow;
 
bool_t
xdrrec_skiprecord(xdrs)
   XDR *xdrs;
 
bool_t
xdrrec_eof(xdrs)
   XDR *xdrs;

xdrrec_endofrecord() ルーチンを呼び出すと、現在出力しているデータにレコードマークが付けられます。引数 flushnowTRUE を指定すると、ストリームの writeproc() が呼び出されます。TRUE を指定しないと、出力バッファがいっぱいになったときに writeproc() が呼び出されます。

xdrrec_skiprecord() ルーチンを呼び出すと、入力ストリーム内の現在位置が、現在レコードの境界まで移動し、ストリーム内の次のレコードの始めに位置します。

ストリームの入力バッファにデータがなくなると、xdrrec_eof() ルーチンから TRUE が返されます。ストリームの元のファイル記述子にもデータが残っていないという意味ではありません。

XDR ストリームの作成

この節では、新たな XDR ストリームインスタンスの作成に必要な抽象データ型を示します。

XDR オブジェクト

例 A-13 の構造体は、XDR ストリームとのインタフェースを定義します。


例 A-13 XDR ストリームインタフェースの例

enum xdr_op {XDR_ENCODE=0, XDR_DECODE=1, XDR_FREE=2};
 
typedef struct {
	enum xdr_op x_op;
	struct xdr_ops {
 		bool_t (*x_getlong)();       /* ストリームから long 型データを入力 */
 		bool_t (*x_putlong)();       /* ストリームに long 型データを出力 */
 		bool_t (*x_getbytes)();      /* ストリームから複数バイトを入力 */
 		bool_t (*x_putbytes)();      /* ストリームに複数バイトを出力 */
 		u_int (*x_getpostn)();       /* ストリームのオフセットを返す */
 		bool_t (*x_setpostn)();      /* オフセットの再設定 */
 		caddr_t (*x_inline)();       /* バッファリングされたデータへのポインタ */
 		VOID (*x_destroy)();         /* プライベートエリアの解放 */
		bool_t (*x_control)();				/* クライアント情報の検索、変更 */
		bool_t (*x_getint32)();       /* ストリームから int を取得 */
 		bool_t (*x_putint32)();       /* ストリームに int を出力 */
 	} *x_ops;
 	caddr_t x_public;                /* ユーザーデータ */
 	caddr_t x_private;               /* プライベートデータへのポインタa */
 	caddr_t x_base;                  /* 位置情報のためのプライベートデータ */
 	int		 x_handy;                 /* その他のプライベートワード */
 } XDR; 

x_op フィールドは、現在ストリームに対して実行している処理内容を示します。このフィールドは XDR プリミティブにとっては重要なフィールドですが、ストリームの実現に対しては影響ありません。すなわち、ストリームは、この値に依存した方法で作成しないでください。x_privatex_basex_handy の各フィールドは、特定のストリームを実現するためのプライベートデータです。x_public は XDR クライアントのためのフィールドでなので、XDR ストリームを実現するために使用したり、XDR プリミティブで使用したりできません。x_getpostn()x_setpostn()x_destroy() はストリームへのアクセス操作に使用するマクロです。x_inline() ルーチンには、XDR * と符号なし整数 (バイトカウント) の 2 つの引数を渡します。このルーチンからは、ストリームの内部バッファセグメントへのポインタが返されます。呼び出し側ではそのバッファセグメントを自由に使用できます。ストリーム側からみると、そのバッファセグメントに入っているデータは失われます。このルーチンは、要求されたサイズのバッファセグメントを返せない場合は NULL を返します。x_inline() ルーチンは、バッファを反復して使用するためのもので、このバッファの使用は移植可能ではありません。この機能の使用はお勧めできません。

ストリームに対するバイトシーケンスの単純な入出力には、x_getbytes()x_putbytes() を使用します。入出力に成功すれば TRUE、失敗すれば FALSE が返されます。この 2 つのルーチンの引数は同じです。xxx を関数名に合わせて置き換えます。

bool_t
xxxbytes(xdrs, buf, bytecount)
   XDR *xdrs;
   char *buf;
   u_int bytecount; 

x_getint32()x_putint32() のオペレーションにより、データストリームから int 数値を受け取ったり、データストリームへ数値を出力したりできます。この 2 つのルーチンは、数値のマシン固有表現から標準外部表現への変換を担当します。変換には、UNIX のプリミティブ htonl()ntohl() を利用できます。上位レベルの XDR 対応ソフトウェアでは、符号付きと符号なしの整数のビット数は同じで、負にならない整数のビットパターンは符号なしの整数のビットパターンと同じであるとみなします。入出力に成功すれば TRUE 、失敗すれば FALSE が返されます。

関数 x_getint()x_putint() では、これらのオペレーションを使用します。この 2 つのルーチンの引数は同じです。

bool_t
xxxint(xdrs, ip)
   XDR *xdrs;
   int32_t *ip;

これらのオペレーションの long バージョン (x_getlong()x_putlong()) でも、 x_getint32()x_putint32() を呼び出します。この場合、プログラムがどのようなマシンで実行されていても、処理される量は必ず 4 バイトになります。

XDR ストリームを新規開発するときは、作成ルーチンを使用して、新たな操作ルーチンを持つ XDR 構造体を作成し、クライアントに提供しなければなりません。

高度な機能

この節では、データ構造体を引き渡す方法を説明します。そのような構造体の 1 つに、リンクリスト (長さは任意) があります。これまでの簡単なサンプルプログラムとは違い、ここでは XDR C ライブラリルーチンと、XDR データ記述言語の両方を使用します。XDR データ記述言語の詳細は、付録 C 「XDR プロトコル仕様」 を参照してください。

リンクリスト

「ポインタのサンプルプログラム」の節では、各個人の総資産と負債に関する C のデータ構造体とそれに対する XDR ルーチンのサンプルプログラムを示しました。例 A-14 では、リンクリストを使用して、ポインタのサンプルプログラムと同じものを作成します。


例 A-14 リンクリストのサンプルプログラム

struct gnumbers {
 	int g_assets;
 	int g_liabilities;
 };
 
bool_t
xdr_gnumbers(xdrs, gp)
 	XDR *xdrs;
 	struct gnumbers *gp;
{
 	return(xdr_int(xdrs, &(gp->g_assets) &&
 		     xdr_int(xdrs, &(gp->g_liabilities)));
}

同じ情報を持つリンクリストを作成します。構造体は次のようになります。

struct gnumbers_node {
   struct gnumbers gn_numbers;
  	struct gnumbers_node *gn_next;
};
typedef struct gnumbers_node *gnumbers_list;

リンクリストのヘッドは単に構造体の短縮形というわけではなく、データオブジェクトと考えられます。同様に、gn_next フィールドも、オブジェクトの終わりかどうかを示すのに使用されます。残念ながら、オブジェクトが次につながる場合も、gn_next フィールドに続きのアドレスが入ります。オブジェクトをシリアライズするときは、リンクアドレスの情報は役に立ちません。

このリンクリストを XDR データ記述言語で示すと、gnumbers_list の再帰宣言となります。

struct gnumbers {
  	int g_assets;
  	int g_liabilities;
};
struct gnumbers_node {
  	gnumbers gn_numbers;
  	gnumbers_node *gn_next;
}; 

この記述では、次のデータがあるかどうかがブール値でわかります。ブール値が FALSE の場合は、それが構造体の最終データフィールドとなります。ブール値が TRUE の場合は、その後に gnumbers 構造体と gnumbers_list とが (再帰的に) 続きます。C の宣言では明示的に宣言されたブール値がなく (ただし、gn_next フィールドが暗黙にその情報を持ちます)、XDR データ記述では明示的に宣言されたポインタがないことに注意してください。

gnumbers_list に対する XDR ルーチンを作成するためのヒントは、上の XDR についての記述から得られます。上の XDR 共用体を実現するためのプリミティブ xdr_pointer()の使用方法を参照してください。


例 A-15 xdr_pointer() のサンプルプログラム

bool_t
xdr_gnumbers_node(xdrs, gn)
 	XDR *xdrs;
 	gnumbers_node *gn;
{
 return(xdr_gnumbers(xdrs, &gn->gn_numbers) &&
         xdr_gnumbers_list(xdrs, &gn->gn_next));
}
 
bool_t
xdr_gnumbers_list(xdrs, gnp)
 	XDR *xdrs;
 	gnumbers_list *gnp;
{
 return(xdr_pointer(xdrs, gnp, sizeof(struct gnumbers_node),
                      xdr_gnumbers_node));
 xdr_pointer}
 

リンクリストをこれらのルーチンで XDR を使用すると、C のスタックがリスト内のノードの数だけ増大するという問題が起こります。これは再帰呼び出しが原因です。例 A-16 のルーチンは、互いに再帰呼び出しを行う上の 2 つのルーチンを 1 つにまとめて、再帰呼び出しが起こらないようにしたものです。


例 A-16 再帰呼び出しを行わない XDR 変換

bool_t
xdr_gnumbers_list(xdrs, gnp)
 	XDR *xdrs;
 	gnumbers_list *gnp;
{
 	bool_t more_data;
 	gnumbers_list *nextp;
 
	for(;;) {
 		more_data = (*gnp != NULL);
 		if (!xdr_bool(xdrs, &more_data))
 			return(FALSE);
 		if (! more_data)
 			break;
 		if (xdrs->x_op == XDR_FREE)
 			nextp = &(*gnp)->gn_next;
 		if (!xdr_reference(xdrs, gnp,
			sizeof(struct gnumbers_node), xdr_gnumbers))
 		return(FALSE);
 		gnp = (xdrs->x_op == XDR_FREE) ? nextp : &(*gnp)->gn_next;
 	}
 	*gnp = NULL;
 	return(TRUE);
}

最初に行うのは、次に続くデータがあるかどうかを調べ、このブール値をシリアライズできるようにします。XDR_DECODE の場合はこの文は不要なことに注意してください。なぜなら、次の文で more_data をデシリアライズするまでブール値はわからないからです。

次の文で XDR 共用体の more_data フィールドに XDR を作成します。本当に次のデータがないことがわかれば、最終ポインタを NULL に設定してリストの終了を示し、処理は終了したので TRUE を返します。XDR_DECODE の場合だけ、ポインタを NULL に設定する意味があることに注意してください。XDR_ENCODEXDR_FREE の場合は既に NULL になっているからです。

処理内容が XDR_FREE の場合は次に、リストの次のポインタアドレスを nextp に保存します。このアドレスはここで保存しておかなければなりません。なぜならば、次の文で gnp をデレファレンスして、リスト内の次の項目のアドレスを取り出しますが、その文を実行すると、gnp が指している記憶領域は解放されてその中のデータが失われるためです。XDR_DECODE の場合は、次の文を実行するまで gnp に値が設定されないため、処理内容によってアドレス保存を行うかどうかを決めています。

次にプリミティブ xdr_reference() を使用して、このノードのデータを XDR 変換します。xdr_reference() はこれまでに使用した xdr_pointer() に似ていますが、次のデータがあるかどうかを示すブール値を送信しない点が異なります。ここでは、その情報を既にXDR 変換してしまっているので、xdr_pointer() は使用しません。ここで、リストの要素と異なる型の XDR ルーチンを渡していることに注意してください。ルーチン xdr_gnumbers() が渡されますが、実際にリストに入っている各要素の型は gnumbers_node() です。xdr_gnumbers_node() を渡さないのは、このルーチンが再帰呼び出しを行うからです。代わりに渡された xdr_gnumbers() は、非再帰部分をすべて XDR 変換します。このような方法がうまくいくのは、各要素の最初の項目が gn_numbers()フィールドの場合だけです。最初のフィールドなので xdr_reference() に渡される両者のアドレスが同じだからです。

最後に gnp の値を更新して、リスト内の次の項目を指すようにします。処理内容が XDR_FREE の場合は保存しておいた値を設定し、それ以外の場合は gnp をデレファレンスして正しい値を取り出します。再帰呼び出しを使用する方法よりむずかしくなりますが、この方が数多くの手続き呼び出しを使用する場合のオーバヘッドがなくなってパフォーマンスも向上します。もっとも、ほとんどのリストはそれほど大きくはないので (項目数がせいぜい 100 以下)、その場合は再帰呼び出しを行なっても問題ありません。