Bei der Übertragung der Prozedurparameter bzw. deren Ergebnissen mittels RPC muß der zahlenmässige Informationsgehalt und die Struktur von, unter Umständen komplizierten Datengebilden, bewahrt werden. Dabei müssen folgende Gesichtspunkte beachtet werden.
Das von SUN vorgeschlagene portable Datenformat eXternal Data Representation (XDR) kommt diesen Erfordernissen entgegen.
serialising deserialising
| |
V V
------------- ---------- ---------- -------------
| | ---> | XDR - | | XDR - | ---> | |
| | | Filter1| ---------------> | Filter1| | |
| | ---------- ---------- | |
| App1 z.B. | | App2 z.B. |
| | Datentransfer | |
| Client | | Server |
| | ---------- ---------- | |
| | | XDR - | <--------------- | XDR - | | |
| | <--- | Filter2| | Filter2| <--- | |
------------- ---------- ---------- -------------
^ ^
| |
deserialising serialising
Bild 4
Die XDR-Bibliothek liefert Funktionen, die diese Aufgaben größtenteils automatisch erledigen können. Sie teilen sich in zwei Gruppen:
XDR-Funktionen sind orientiert auf den Einsatz in C-Programmen, aber nicht unbedingt unter UNIX. Insbesondere wird das UNIX I/O-System nicht vorausgesetzt. Als Beispiel ermöglichen MS-DOS-Portierungen den Datenaustausch zwischen UNIX und DOS, bei dem der Zahleninhalt erhalten bleibt. Die Anwendung von XDR ist nicht auf RPC beschränkt. XDR bietet z.B. eine Möglichkeit, Teilergebnisse einer Datenverarbeitung zu speichern, die später anderen Programmen zur Verfügung gestellt werden sollen, die möglicherweise auf anderen Rechnern laufen.
Die lokale syntaktische Beschreibung der Daten wird bei uns die Sprache C sein. Mithilfe von XDR erfolgt die Umwandlung in eine XDR-Form. Die zugehörige Sprache ist die XDR-Sprache mit ihrer sog. Transfersyntax, bzw. deren abstrakten Beschreibung sog. »abstrakten Syntax«. Als Beispiel kann der Typ der abstrakten Syntax »boolean« dienen. Seine Darstellung in der Transfersyntax könnte aus den Werten 0 oder 1 bestehen und die Darstellung in der lokalen Syntax z.B. aus den Werten »Y« oder »N« in einem Rechner und »J«, oder »N« in einem zweiten. Ein Tupel (abstrakte Syntax, Transfersyntax) wird bei ISO/OSI auch als »Presentation Context« bezeichnet. In unserem Beispiel: (bool, {0,1}).
Die Operationen der Serialisierung/Deserialisierung von Daten beziehen sich auf Datenströme, die realisiert werden entweder
FILE
s aus
<stdio.h>
,
Außer diesen bereits vorhandenen Streams ist es auch möglich, eigene
Custom-Streams zu definieren, indem man zu den entsprechenden Funktionen aus
der XDR-Library selbst eigene dazuschreibt. Alle Angaben über den jeweils
eingesetzten Strom werden in einer Struktur XDR gehalten. Sie ist in
<rpc/xdr.h>
deklariert:
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)();
bool_t (*x_putlong)();
bool_t (*x_getbytes)();
bool_t (*x_putbytes)();
u_int (*x_getpostn)();
bool_t (*x_setpostn)();
caddr_t (*x_inline)();
void (*x_destrpy)();
} *x_ops;
caddr_t x_public;
caddr_t x_private;
caddr_t x_base;
int x_handy;
} XDR;
Diese Struktur bildet die einzige Schnittstelle zum Stream. Ein Zeiger auf sie wird an die meisten Funktionen aus der XDR-Bibliothek als Parameter weitergegeben. Alle Funktionen, deren Aufgaben unabhängig vom eingesetzten Datenstrom formuliert werden können, brauchen sich dann um stromspezifische Inhalte nicht zu kümmern.
Die Komponenten der XDR-Struktur werden nachfolgend erklärt. Sie sind jedoch für den Anwendungsprogrammierer meistens unerheblich sind und spielen nur bei der Definition neuer XDR-Ströme eine Rolle.
Die aktuelle Operation, die an dem Strom durchgeführt wird. Sie wird getrennt spezifiziert, weil die später beschriebenen XDR-Filter von diesem Parameter meistens unabhängig sind.
Schreiben und Lesen von long
s von dem oder auf
den Strom. Diese Funktionen nehmen die eigentliche
Umwandlung vor (z.B. Big- nach Little-Endian).
Schreiben und Lesen von Bytesequenzen von dem oder auf den Strom. Sie geben je nach Ergebnis TRUE oder FALSE zurück.
Makros für Zugangsoperationen
Nimmt als Parameter (XDR *)
und die Anzahl der Bytes.
Gibt die Adresse in dem strominternen Puffer zurück.
Daten des XDR-Benutzers. Sie sollen in der Stream-Implementation nicht benutzt werden.
Daten privat bezüglich der jeweiligen Stream-Implementierung
auch privat, wird für Informationen über die Position im Strom benutzt.
auch privat, zusätzlich
----------------------------------
| |
| --------- Umwandlung mittels | ----------
| | user | XDR-Filter | | output |
| | data | -----------> | UDP-Pakete --------->| data |
| | | | / | |
| | | X X X X / ----------
| --------- -----------------------
| / | -----------> Rückumwandlung
| / | XDR-Strom mit dem gleichen
| user prozess / ------------------------ XDR-Filter
| space / /
| / / |
| / / |
| / / |
| XDR- / |
| handle |
| |
| |
----------------------------------
Bild 5
Die gültigen Operationen, die XDR Struktur vermittelt, sind:
enum xdr_op {
XDR_ENCODE, /* Serialisierung */
XDR_DECODE, /* Deserialisierung */
XDR_FREE /* befreit den Speicherplatz */
};
Standard IO-Streams benutzen für die Datenübertragung den IO-Mechanismus,
wie er in <stdio.h>
definiert ist. Daten, die aus
ihrem lokalen Format ins XDR-Format umgewandelt, d.h. serialisiert werden,
werden auf ein FILE
geschrieben. Umgekehrt beinhaltet die
Rücktransformation das Lesen aus einem FILE
. Der Standard
IO-Stream wird mit folgendem Aufruf erzeugt:
void xdrstdio_create (handle, file, op)
XDR *handle;
FILE *file;
enum xdr_op op;
Vor diesem Aufruf muß der Speicherbereich »handle« im User Space, wie
im Bild 5 dargestellt, bereits existieren. Analog muß der Parameter »file«
auf ein bereits geöffnetes FILE
zeigen. Mit xdrstdio_create()
werden diese Komponenten so »zusammengebunden« und in der Struktur
»handle« abgelegt, daß dieses »handle« für weitere XDR-Operationen
bequem benutzt werden kann. Der Parameter »op« kann entsprechend
der Deklaration von enum xdr_op
als XDR_ENCODE
,
XDR_DECODE
oder XDR_FREE
gesetzt werden. Damit wird entschieden,
für welche Operation der in »handle« beschriebene IO-Stream benutzt wird,
wobei dieser Parameter in der XDR *handle
Struktur auch
nachträglich »per Hand« verändert werden kann. Im Falle von
XDR_FREE
wird der IO-Stream-Buffer mit fflush()
»gefluscht«, aber das FILE
wird nicht geschlossen. Die Funktion
fclose()
muß nachträglich explizit aufgerufen werden.
Damit hat man die Möglichkeit, ein FILE
nacheinander für
verschiedene IO-Streams zu benutzen und erst am Ende zu schließen.
Mit Standard IO-Streams kann man Daten im XDR-Format in einer Datei zwischenspeichern und später mit einem anderen Programm für die Weiterbehandlung wieder einlesen.
Im Gegensatz zu Standard IO-Streams bleiben Daten, die mit Memory Streams zwischengespeichert werden, im Hauptspeicher im Speicherbereich des jeweiligen Prozesses. Sie können anschließend vom gleichen Prozeß zurückgewandelt oder mit »shared memory« von einem anderen Prozeß abgeholt werden.
void xdrmem_create (handle, addr, len, op)
XDR *handle;
char *addr;
u_int len;
enum xdr_op op;
Die Aufruf-Parameter sind analog zu xdrstdio_create()
. Der
Parameter »addr« ist der Zeiger auf den betreffenden Speicherbereich,
»len« seine Länge in Bytes.
Die dritte Art von XDR-Streams ist allgemeiner. Standard IO-Streams und Memory-Streams können als seine Spezialfälle betrachtet werden.
void xdrrec_create (handle, sendsize, recvsize,
iohandle, readproc, writeproc)
XDR *handle;
u_int sendsize, recvsize;
char *iohandle;
int (*readproc)(), (*writeproc)();
Ähnlich wie Standard IO-Streams unterhalten Record-Streams eigene
Pufferzonen, deren Grössen für die Ausgabepuffer mit »sendsize« und für die
Eingabepuffer mit »recvsize« angegeben werden. Bei Nullwerten werden vom
System Voreinstellungen geliefert. Die zwei Parameter readproc
,
bzw. writeproc
sind Zeiger auf Funktionen, die die eigentliche
Ein/Ausgabe
durchführen. Sie müssen vom Anwender geliefert werden. Als Beispiel können
an dieser Stelle die Systemcalls read(2)
und write(2)
von clib.a stehen. Sie
können jedoch auch vom Anwender selbst geschrieben werden. Nach der
Erzeugung des Stromes werden sie von XDR-System für den eigentlichen
Datentransfer mit folgenden Parametern aufgerufen:
int readproc/writeproc (iohandle, buf, len)
char *iohandle,
*buf;
int len;
Der Parameter iohandle wird in xdrrec_create()
nicht benutzt.
Er wird an readproc oder writeproc weitergeleitet und kann dort beliebig
verwendet werden. Als Beispiel müßte im Falle von Systemcalls
read(2)
und write(2)
an dieser Stelle der Filedeskriptor
stehen. Ähnlich wie read(2)
und write(2)
geben
readproc()
und writeproc()
die Anzahl der transferierten
Bytes zurück.
--------------------------------------
| |
| ------------- Umwandlung mittels |
| | user data | XDR-Filter |
| | | -----------> | z.B. UDP-Pakete
| | | |
| | | X X X X
| ------------- --------------------------------
| / | writeproc() -->
| / |
| user prozess / --------------------------------
| space / / <-- readproc()
| / / |
| / / |
| / / | Transportmedium
| XDR- / | mit den XDR-Strömen
| handle |
| |
| |
--------------------------------------
Bild 6
Der Transportmechanismus wird mit dem XDR-Filter jeweils für einen Datensatz in Gang gesetzt, aber seine Richtung und alle sonstigen für den Transport verantwortlichen Variablen werden vom XDR handle gesteuert.
Im Gegensatz zu xdrstdio_create()
und xdrmem_create()
entfällt bei xdrrec_create()
der Parameter, der die
Kodierungsrichtung angibt. Sie kann nachträglich mit
handle->x_op = XDR_ENCODE oder XDR_DECODE;
explicit im handle hin und her geschaltet werden. Die Records (Sätze)
werden zusammen mit der Information über die Länge des jeweiligen
Fragmentes und der end_of_record Mark auf den Stream geschrieben:
-------------------------
| 0 | length 1 | data 1 | fragment 1
| 0 | length 2 | data 2 | fragment 2
| 0 | . | . | .
ein Record | 0 | . | . | .
| 0 | . | . | .
| 0 | . | . | .
| 0 | . | . | .
| 1 | . | . | .
-------------------------
^ ^
| |
| 31 Bits für die Länge des Datenfragmentes
|
1 Bit: 1 für das letzte Fragment, 0 sonst
Bild 7
Drei Zusatzfunktionen ermöglichen die explizite Aufteilung des Stromes in Records:
bool_t xdrrec_endofrecord (handle, flushnow)
XDR *handle;
bool_t flushnow;
bool_t xdrrec_skiprecord (handle)
XDR *handle;
bool_t xdrrec_eof (handle)
XDR *handle;
xdrrec_endofrecord beendet den Record (Satz), indem das laufende Fragment
als letztes markiert wird. Bei flushnow == TRUE wird sogleich writeproc
aufgerufen und bei flushnow == FALSE erst, wenn der Puffer gefüllt ist.
xdrrec_skiprecord wird beim Lesen aus dem Strom dazu benutzt, den laufenden Record zu überspringen.
Die Funktion xdrrec_eof()
gibt TRUE zurück, falls keine Daten
mehr im Strom darauf warten, abgeholt zu werden.
Wie beim Record-Strom gibt es, in <xdr.h> definierte Macros, die sich auf alle drei Strom-Typen beziehen:
u_int xdr_getpos (handle)
XDR *handle;
bool_t xdr_setpos (handle, pos)
XDR *handle;
u_int pos;
void xdr_destroy (handle)
XDR *handle;
Die zwei ersten ermöglichen es die Position im Strom zu erfragen, bzw.
zu setzen. Mit dem Returnwert von xdr_getpos()
kann die
tatsächliche Anzahl
von Bytes abgefragt werden, die bis jetzt für die Kodierung gebraucht
wurden.
Die dritte Funktion, xdr_destroy()
muß vom Anwender aufgerufen
werden um den mit xdr..._create()
erzeugten Strom zu schließen.
Damit wird auch
der beim Erzeugen möglicherweise allokierte Speicherplatz freigegeben.
Im vorigen Kapitel wurden Funktionen beschrieben, die den Mechanismus der XDR-Umwandlung initialisieren. Dieser, in der handle-Variable beschriebene, Mechanismus wird erst später mit den Filter-Funktionen in Gang gesetzt. Alle diese Filter werden, je nach Besetzung der Variablen handle->x_op, zum Kodieren oder zum Dekodieren benutzt. Diese Eigenschaft stellt sicher, daß das je nach Objekttyp kodierte Objekt auch richtig (mit der gleichen Funktion) dekodiert wird.
Es gibt in XDR-Librabry folgende sog. primitive Filter-Funktionen:
bool_t xdr_char (handle, cp)
XDR *handle;
char *cp;
bool_t xdr_u_char (handle, ucp)
XDR *handle;
unsigned char *ucp;
bool_t xdr_int (handle, ip)
XDR *handle;
int *ip;
bool_t xdr_u_int (handle, uip)
XDR *handle;
unsigned *uip;
bool_t xdr_long (handle, lp)
XDR *handle;
long *lp;
bool_t xdr_u_long (handle, ulp)
XDR *handle;
u_long *ulp;
bool_t xdr_short (handle, sp)
XDR *handle;
short *sp;
bool_t xdr_u_short (handle, usp)
XDR *handle;
u_short *usp;
bool_t xdr_float (handle, fp)
XDR *handle;
float *fp;
bool_t xdr_double (handle, dp)
XDR *handle;
double *dp;
bool_t xdr_enum (handle, ep)
XDR *handle;
enum_t *ep;
bool_t xdr_bool (handle, bp)
XDR *handle;
bool_t *bp;
Der erste Parameter ist der Pointer auf den bereits existierenden Strom. Der zweite gibt die Adresse an, von wo die Daten abgeholt bzw. wo sie abgelegt werden. Das ist notwendig, damit der Filter in beide Richtungen arbeiten kann: Beim Dekodieren müssen ja die Daten unter die angegebene Adresse geschrieben werden. Der Returnwert gibt an, ob eine erfolgreiche Umwandlung stattgefunden hat.
Der primitive Filter xdr_enum setzt voraus, daß enum sowohl beim Absender als auch beim Adressat mit Integer realisiert ist. Das ist jedoch nach Kernighan & Ritchi Standard C:
#define enum_t int;
xdr_bool ist der Spezialfall von xdr_enum für:
enum bool_t { TRUE = 1, FALSE = 0 };
Zusätzlich gibt es in der XDR-Library die Funktion:
bool_t xdr_void ();
die dann verwendet wird, wenn keine Daten umgewandelt werden. Sie wird z.B bei discriminated unions oder gebundenen Listen gebraucht.
Außer den beschriebenen Funktionen liefert die XDR-Library in-line Macros, die zwar den Funktionsaufruf sparen, jedoch keine Fehlerbehandlung ermöglichen:
long IXDR_GET_LONG (buf)
long *buf;
bool_t IXDR_GET_BOOL (buf)
long *buf;
»type« IXDR_GET_ENUM (buf, type)
long *buf;
»type« type;
u_long IXDR_GET_U_LONG (buf)
long *buf;
short IXDR_GET_SHORT (buf)
long *buf;
u_short IXDR_GET_U_SHORT (buf)
long *buf;
Entsprechend gibt es PUT-Macros. Um beide (GET oder PUT) anwenden zu können, muß vorher die Pufferadresse mit der Funktion
long *xdr_inline (handle, len)
XDR *handle;
int len;
geholt werden. Es folgt ein Beispielprogramm mit Macros, die standard IO-Streams verwenden.
#include <stdio.h>
#define N 10
...
FILE *fp, *fopen();
XDR handle;
long *buf;
int i, count;
short array[N];
fp = fopen (filename, "w");
xdrstdio_create (&handle, fp, XDR_ENCODE);
count = BYTES_PER_XDR_UNIT * N;
buf = inline (&handle, count);
for (i = 0; i < N; i++)
array[i] = IXDR_GET_SHORT (buf);
xdr_destroy (&handle);
fclose (fp);
Composite Filter werden zusätzlich zu den primitiven Filtern von der XDR-Library geliefert. Sie ermöglichen die Umwandlung von zusammengesetzten Datenstrukturen. Es gibt composite Filter für:
Sie können wie primitive Filter ebenfalls in custom-Filter benutzt werden.
bool_t xdr_string (handle, sp, maxlen)
XDR *handle;
char **sp;
u_int maxlen;
maxlen kann protokollspezifisch als eine feste Zahl, z.B. 256 gesetzt werden. Der zweite Parameter ist ein doppelter Pointer. Das ist notwendig, um den Fall der Dekodierung abzudecken. Weil dem Aufrufer die tatsächliche Länge des Strings, den er bekommt, nicht bekannt ist, kann einen genügend großen Puffer zur Verfügung stellen, oder der Puffer wird vom Filter selbst bereitgestellt. Der Aufrufer gibt nur die Adresse der Speicherzelle unter der xdr_string() die Adresse seines Puffers hineinschreibt. In beiden Fällen ist eine zusätzliche Zwischenvariable, die die Adresse des Stringpuffers enthält, notwendig.
char buf[MAXLEN], *p;
p = buf;
xdr_string (handle, &p, maxlen)
----------------
| |
| V
-----|------------------------------------------------- linearer Speicher
| p | | der String buf, dh. Stringdaten |
----- -----------------------------------
^
|
Adresse von p
Bild 8
Der Stringpuffer wird vom Filter xdr_string()
erst dann allokiert,
wenn *sp == NULL ist, dh. wenn die Zwischenvariable (p), deren Adresse an
xdr_string()
geleitet wird (sp = &p), keine gültige
Pufferadresse enthält
(p == NULL). Andernfalls ist der Anwender selbst dafür verantwortlich, daß
sein Puffer für den Filter nicht zu klein ist. Bei der XDR_FREE operation
findet, falls p != NULL ist, der Aufruf free(p)
statt.
Bemerkung: Der Adressoperator & darf in C nur vor einem real im Speicher existierenden Objekt stehen. Insbesondere hat die Konstruktion &&x keinen Sinn.
Die Methode mit der Übergabe des doppelten Pointers ist zwar bei Angabe von maxlen theoretisch nicht notwendig, sie bildet aber den einzigen Austauschmechanismus, der auch für Strings, die wesentlich kürzer als maxlen sind, effizient bleibt.
Ein »Wrapper« über xdr_string()
ist xdr_wrapstring()
:
Der Aufruf
xdr_wrapstring(hand, strp)
ist äquivalent zu
xdr_string(hand, strp, MAXUNSIGNED)
MAXUNSIGNED ist der maximale vorzeichenlose Integer. Es ist natürlich
schlecht, wenn er im System, wo die Kodierung stattfindet grösser ist als
im Addressat-System beim Dekodieren, aber das kann nur bei der Übertragung
von sehr langen Strings eine Rolle spielen. Eine praktische Grenze ist mit
der Paketgrösse des UDP-Protokolls erreicht, auf dem XDR aufbaut.
Bytearray unterscheidet sich von einem String dadurch, daß er nicht mit 0 beendet wird. Entsprechend können Nullen im Bytearray selbst vorkommen.
bool_t xdr_bytes (handle, sp, lp, maxlen)
XDR *handle;
char **sp;
u_int *lp;
u_int maxlen;
Der Filter xdr_bytes()
bekommt zusätzlich zu
xdr_string()
ein Argument
lp, das auf die Speicherstelle zeigt, wo die Länge des Arrays abgelegt
wird. Ist bei der »Deserialisierung« *sp == NULL, so wird wie bei
xdr_string()
, der Speicherplatz vom Filter selbst allokiert.
Falls die Anzahl der zu Übertragenden Bytes bekannt ist, kann an der
Stelle von xdr_bytes()
die schnellere Routine
xdr_opaque()
benutzt werden:
bool_t xdr_opaque (handle, sp, lp)
XDR *handle;
char *sp;
u_int lp;
xdr_bytes() kann als Spezialfall des Filters für Arrays betrachtet
werden:
bool_t xdr_array (handle, sp, lp, maxlen, esize, xdr_element)
XDR *handle;
char **sp;
u_int *lp;
u_int maxlen;
u_int esize;
bool_t (*xdr_element)();
esize ist die Größe eines Elementes in Bytes (z.B. mit sizeof()
berechnet) und (*xdr_element)()
ein Pointer auf den XDR-Filter
für das einzelne Element.
Ist die Anzahl der Bytes lp bekannt, so kann statt xdr_array()
die schnellere Routine xdr_vector()
verwendet werden (ähnlich wie
xdr_opaque()
statt xdr_bytes()
).
bool_t xdr_vector (handle, sp, lp, esize, xdr_element)
XDR *handle;
char *sp;
u_int lp;
u_int esize;
bool_t (*xdr_element)();
An der Stelle von Varianten (unions) gibt es in XDR discriminated
unions. Der Diskriminator, ein enum_t discr, der die Information darüber
trägt, welche Komponente der Variante konkret zum Einsatz kommt, muß vor
der Übertragung bekannt sein. Ebenfalls muß für jede Komponente eine
spezielle Prozedur angegeben werden, die diese Komponente transformiert.
Das kann auf folgende Weise geschehen: Jeder Komponente der Variante
ist ein Paar (value, proc) zugeordnet:
struct xdr_discrim {
enum value;
bool_t (*proc)();
};
Value ist der Wert, der sich bei einer Komponente mit discr decken
muß, und (*proc)()
die Prozedur, die diese Komponente
transformiert. Dann hat xdr_union:
bool_t xdr_union (handle, discr, unp, arms, defaultarm)
XDR *handle;
enum_t *discr;
char *unp;
struct xdr_discrim *arms;
bool_t (*defaultarm)();
folgende Parameter:
Wir geben ein Beispiel der Anwendung von xdr_union()
:
enum utype { INTEGER = 1, STRING = 2, MYTYPE = 3 };
struct dunion {
enum utype discr; /* der Diskriminator */
union {
int ival;
char *pval;
struct mytype mval;
} uval;
};
struct xdr_discrim dunion_arms[4] = {
{ INTEGER, xdr_int },
{ STRING, xdr_wrapstring },
{ MYTYPE, xdr_myfilter },
{ _dontcare_, NULL }
}
bool_t xdr_dunion (handel, utp)
XDR *handle;
struct dunion *utp;
{
return (xdr_union (handle, &utp->utype, &utp->uval,
dunion_arms, NULL));
}
xdr_myfilter()
für die Übertragung von MYTYPEs muß natürlich
noch geschrieben werden. Es gehört zum guten Programmierstil, in der
ersten Zeile des Beispiels die Angaben: INTEGER = 1, etc. explizit zu
machen, damit der Compiler des Empfängers hier keine andere Vorbesetzung
trifft.
Oft tritt der Fall auf, daß eine Datenstruktur nur mit der Angabe des
Pointers transformiert werden soll. Das ist insbesondere dann der Fall,
wenn der Pointer selbst ein Element einer anderen Struktur ist. Diese
Aufgabe kann mit xdr_reference()
erledigt werden.
bool_t xdr_reference (handle, pp, size, proc)
XDR *handle;
char **pp;
int *size;
bool_t (*proc)();
pp ist der Pointer auf die Struktur, size ihre Grösse (sizeof()
verwenden !), proc, der Filter für die Tranformation der Struktur. Wir
geben ein Beispiel:
struct pgn {
char *name;
struct mytype *mval;
};
#define MYSIZE sizeof(struct mytype)
bool_t xdr_pgn (handle, sp)
XDR *handle;
struct pgn *sp;
{
return (xdr_string (handle, &sp->name, NLEN) &&
xdr_reference (handle, &sp->mval, MYSIZE,
xdr_mytype));
}
xdr_reference ()
kann nur angewendet werden, wenn der betreffende
Pointer kein NULL-Pointer ist. Anderenfalls kommt xdr_pointer()
zum Einsatz. Die Synopsis ist die gleiche.
Die Tatsache, daß alle XDR-Filter als erstes Argument den Pointer auf den XDR-Handle haben und bool_t als Returnwert zurückgeben, kann dazu verwendet werden, eigene XDR-Filter zu bauen. Dabei können die primitiven Filter als Bausteine dienen. So kann z.B. die Umwandlung der Struktur
struct my_struct {
int i;
char c;
short s;
};
mit dem folgenden sog. »Custom-Filter« erreicht werden.
bool_t my_filter (handle, data)
XDR *handle;
struct my_struct *data;
{
return (xdr_int (handle, &data->i) &&
xdr_char (handle, &data->c) &&
xdr_short (handle, &data->s));
}
Der Custom-Filter wandelt die Struktur my_struct komponentenweise um. Gelingt eine Umwandlung nicht, so bricht er ab und gibt FALSE zurück. Anderenfalls wird TRUE zurückgegeben. Gerade aus diesem Grunde gilt bei XDR:
#define TRUE 1
#define FALSE 0
Die Eigenschaft der primitiven Filter, daß sie in beide Richtungen
(Kodieren, Dekodieren, aber auch XDR_FREE) arbeiten, überträgt sich
automatisch auf den Custom-Filter. Auch Alignment-Probleme bei Strukturen
führen zu keinem Fehler, falls die primitiven Filter richtig funktionieren.
Der zweite Parameter bei dem my_filter()
-Aufruf kann verschiedene
Bedeutung haben, wie später an einem Beispiel demonstriert wird.
Der Speicherplatz, der für die Zwischenpufferung der Daten in den
Filtern nötig ist, muß nicht immer vor der Übertragung bekannt sein. In
diesem Fall muß der Filter ihn zur Laufzeit allokieren. Folgende Filter
können je nach Situation Platz mit Hilfe von malloc()
allokieren:
xdr_array(),
xdr_bytes(),
xdr_pointer(),
xdr_reference(),
xdr_string(),
xdr_vector(),
xdr_wrapstring()
Um den Platz später zu befreien kann xdr_free()
benutzt werden:
void xdr_free (proc, obj)
xdr_proc_t proc;
char *obj;
Das erste Argument ist der Pointer auf den Filter, der den Platz allokierte,
z.B. xdr_string, das zweite die Adresse des Pointers auf das erzeugte
Objekt, z.B.:
char *ptr = NULL;
xdr_string (&handle, &ptr, MAXSIZE);
xdr_free (xdr_string, &ptr);
Naturgemäß überträgt sich die Eigenschaft Speicher zu allokieren
oder mit xdr_free()
freizugeben auf die Custom-Filter. Wird
malloc()
im
Custom-Filter explizit verwendet, so sollte in der gleichen Routine für den
Fall handle->x_op == XDR_FREE auch free()
aufgerufen werden.
Das folgende Beispiel zeigt die Anwendung eines XDR-Filters zum Speichern von numerischen Experimentdaten in einer rechnerunabhängigen Weise und das Einlesen von diesen Daten, das auf einem anderen Rechner stattfinden kann. Die Daten werden beim Aufruf mit -i eingelesen, und mit -o auf das File geschrieben.
#include <stdio.h>
#include <rpc/rpc.h>
#define MAXNAME 128
#define MAXX 1024
#define DATLEN 27
struct edata {
char *autor;
char date[DATLEN];
int n;
float *x;
};
bool_t filter (handle, d)
XDR *handle;
struct e_data *d;
{
register int i;
char *da = d->date;
if (!xdr_string (handle, &d->autor, MAXNAME) ||
!xdr_string (handle, &da, DATLEN) ||
!xdr_int (handle, &d->n) ||
d->n > MAXX)
return (FALSE);
for (i = 0; i < d->n; i++)
if (!xdr_float (handle, &d->x[i])
return (FALSE);
return (TRUE);
}
main (argc, argv)
int argc;
char *argv[];
{
struct e_data d;
float *malloc ();
XDR hand;
FILE *fp, *fopen();
int i;
char *ctime();
if (argc < 3) {
printf ("usage: %s -o | -i file\n", argv[0]);
exit (1);
}
d.x = malloc (MAXX * sizeof (float));
if (argv[1][1] == 'i') { /* input data */
fp = fopen (argv[2], "r");
xdrstdio_create (&hand, fp, XDR_DECODE);
d.autor = NULL;
}
else {
fp = fopen (argv[2], "w");
xdrstdio_create (&hand, fp, XDR_ENCODE);
strcpy (d.date, ctime (time (0)));
d.autor = "zbyszek";
d.n = MAXX;
for (i = 0; i < MAXX; i++)
d.x[i] = i / 3;
}
printf ("filter = %d\n", filter (&hand, &d));
}
Im »-o« Fall werden die Daten zuerst erzeugt (else-Block im main). Die eigentliche Lese- oder Schreiboperation geschieht im Filter-Aufruf (letzte Zeile). Beachten Sie bitte die unterschiedliche Handhabung von autor und date Variablen im Filter. Die Hilfsvariable:
char *da = d->date;
ist notwendig. Der xdr_string()
-Aufruf benötigt die Adresse
von der Adresse vom String.
In dem zweiten Beispiel wird ein Record-Stream verwendet. Dazu muß der create-Aufruf des ersten Bespiels ersetzt werden durch
xdrrec_create (&hand, 0, 0, fp, rdata, wdata);
Vorher muß die Operation im hand gesetzt werden:
hand.x_op = XDR_DECODE oder XDR_ENCODE;
Die zwei Funktionen rdata()
und wdata()
sind Wrapper
für fread()
und fwrite()
:
rdata (fp, buf, n)
FILE *fp;
char *buf;
int n;
{
if (n = fread (buf, 1, n, fp))
return (n);
else
return (-1);
}
wdata (fp, buf, n)
FILE *fp;
char *buf;
int n;
{
if (n = fwrite (buf, 1, n, fp))
return (n);
else
return (-1);
}
An dieser Stelle könnten zwei beliebige Funktionen stehen, die den Zugang zu einem beliebigen Medium darstellen, das vom vierten Parameter des create-Aufrufs beschrieben, und an diese Funktionen weitergegeben wird.
Zusätzlich sind am Ende des Filters die Zeilen:
if (hand->x_op == XDR_ENCODE) xdr_endofrecord (hand, TRUE);
if (hand->x_op == XDR_DECODE) xdr_skiprecord (hand);
sinnvoll.
Diese verkürzte Beschreibung entspricht RFC1014 vom ARPA Network Information Center. Wir geben hier die Byte-Reihenfolge der Übertragung einzelner Datentypen, sowie auch die Syntax der XDR-Sprache an. Die XDR-Sprache ähnelt in der Daten-Beschreibung der C-Sprache, deshalb werden hier hauptsächlich die Unterschiede angegeben.
Alle Daten sind Vielfache von 4 Bytes (32 Bits). Das sichert die korrekte Ausrichtung an der am meisten eingesetzten Hardware zu. (Ausnahme CRAY: 8 Byte Ausrichtung). Es wird immer am zu 4-Byte Grenze Ende mit Nullen aufgefüllt (insbesondere bei Opaque oder String). Reihenfolge der Übertragung ist Byte 0, Byte 1, ..., d.h. Big-Endian
Die Übertragung von einem Byte auf eine für das Medium spezifische Weise wird vorausgesetzt. XDR spezifiziert nicht wie ein Byte aus einzelnen Bits zusammengesetzt wird. Ob das höherwertige Bit eines Bytes auch tatsächlich als erstes durch die Leitung fließt, wird in den tiefer liegenden Schichten, meistens in Data Link Layer, spezifiziert.
Integer sind 4 Byte, 2-Komplement
(MSB) (LSB)
+------+------+------+------+
|Byte 0|Byte 1|Byte 2|Byte 3|
+------+------+------+------+
Unsigned Integer ist ein Integer ohne Vorzeichen. Enum wird mit Integer gebaut, wie in C. Boolean ist enum { FALSE = 0, TRUE = 1 }. Es wird als
bool identifier;
beschrieben. Hyper Integer und Unsigned Hyper Integer sind Erweiterungen von
Integer und Unsigned Integer auf 8 Byte:
(MSB) (LSB)
+------+------+------+------+------+------+------+------+
|Byte 0|Byte 1|Byte 2|Byte 3|Byte 4|Byte 5|Byte 6|Byte 7|
+------+------+------+------+------+------+------+------+
Float: ist ein 4-Byte lange IEEE single precision Float mit den Feldern:
S: Vorzeichenbit, 0 - positiv, 1 - negativ
E: Exponent mit Basis 2, 8 Bit lang
V: Verschiebung des Exponenten um 127
F: Mantisse mit Basis 2, 32 Bits
(-1)**S * 2**(E-V) * 1.F ** ist der Potenzoperator
+------+------+------+------+
|Byte 0|Byte 1|Byte 2|Byte 3|
S| E | F |
+------+------+------+------+
1|<- 8->|<------23 Bit----->|
Da, wie oben ausgeführt, die Übertragung eines Bytes von tieferen Protokollschichten erledigt wird, bezeichnet S in der oberen Zeichnung nur das höherwertige Bit des nullten Bytes und nicht das tatsächlich als erstes übertragene Bit.
Double entspricht dem 8 Byte langen Float:
+------+------+------+------+------+------+------+------+
|Byte 0|Byte 1|Byte 2|Byte 3|Byte 4|Byte 5|Byte 6|Byte 7|
S| E | F |
+------+------+------+------+------+------+------+------+
1|<--11-->|<-----------------52 Bit-------------------->|
Signed Zero, Signed Infinity (Overflow), oder Denormalized (underflow) entsprechen der IEEE-Konvention, NaN (not a number) wird nicht benutzt.
Fixed-length Opaque:
+------+------+ ... +------+ ... +------+ ... +------+
|Byte 0|Byte 1| ... |Byte n-1 0 | 0 | ... | 0 |
+------+------+ ... +------+ ... +------+ ... +------+
|<---------n Byte---------->| aufgefüllt mit Nullen |
Variable-length Opaque sind kodiert als unsigned Integer gefolgt vom fixed-length Opaque. Die Syntax ist:
opaque identifier<m>;
oder
opaque identifier<>;
m bezeichnet die maximale Länge, z.B, für UDP ist m=8192 wegen der
maximalen Paketgröße. Falls nicht anders spezifiziert, ist m = 2**32 - 1.
Strings sind analog wie opaque: unsigned Integer gefolgt vom String-Data in der aufsteigenden Byte-Reihenfolge: Byte0, Byte2, ... und aufgefüllt mit Nullen bis zur nächsten 4-Byte Grenze. Die Syntax ist:
string identifier<m>;
oder
string identifier<>;
Fixed-length Array:
+-------------+-------------+ ... +-------------+
| Element 0 | Element 1 | ... | Element n-1 |
+-------------+-------------+ ... +-------------+
Die Elemente sind Vielfache von 4 Byte, können aber ansonsten verschieden lang sein. Die letzte Eigenschaft ist notwendig für den Fall, daß der Typ einzelner Elemente eine variable Länge hat wie z.B. String.
Variable-length Array ist ein Integer gefolgt von Fixed-length Array. Die Syntax ist:
type-name identifier<m>;
oder
type-name identifier<>;
Structure ist analog zum Array eine Folge von Struct-Elementen in der
Reihenfolge, wie sie in der Definition vorkommen. Auch hier, wie überall in
RPC, müssen die Elemente Vielfache von 4 Bytes sein.
Unions (Varianten) verzweigen auf mehrere Äste, die verschiedene für die Union zulässige Typen beschreiben, von denen nur einer zur bestimmten Zeit gültig ist. Bei der Umwandlung in einen XDR-Stream muß der zur übertragungszeit gültige Typ durch eine Diskriminante (Kennung) angegeben werden. (Discriminated Union = Variante mit Kennung). XDR kennt keine Unions ohne Kennung. Der Diskriminator (Kennung) wird als unsigned Integer zuerst übertragen. Danach folgen alle für die Union zulässigen Äste nacheinander mit jeweils einer Typangabe (unsigned Integer) davor. Die Kennung der ganzen Union muß mit einem der angegebenen Typen übereinstimmen. Die Syntax ist:
union switch (discriminant-declaration) {
case discriminant-value-A;
arm-declaration-A;
case discriminant-value-B;
arm-declaration-B;
.
.
.
default :
default-declaration;
} identifier;
Der Default-Ast ist optional.
Der Typ void beinhaltet keine Daten. Konstante ist ein Integer:
const identifier = n;
»typedef« deklariert keine Daten, sondern dient wie in der Sprache C
dazu, neue Bezeichnungen für die folgenden Datendeklarationen zu schaffen.
Es gibt eine zu typedef äquivalente, jedoch bei XDR bevorzugte
Schreibweise: Statt z.B.:
typedef enum {
FALSE = 0,
TRUE = 1
} bool;
wird folgende gleichwertige Form empfohlen:
enum bool {
FALSE = 0,
TRUE = 1
};
In der XDR-Sprache haben Pointer wie:
type-name *identifier;
eine etwas andere Bedeutung als in C. Man nennt sie Optional-Data. Die obige
Deklaration ist äquivalent mit der folgenden:
union switch (bool opt) {
case TRUE:
type-name element;
case FALSE:
void;
} identifier;
Dies ist auch äquivalent zu:
type-name identifier<1>;
Hier wird die 4 Byte lange bool opt von der union-Deklaration als die
Länge des Arrays interpretiert (siehe Arrays). Bitfelder existieren im
jetzigen XDR nicht.
Die XDR-Datentypen zusammengefaßt:
---------------------------------------------------------------------
XDR-Datentyp C-Datentyp Decode/Encode-Filter
---------------------------------------------------------------------
---------------------------------------------------------------------
int int xdr_int
---------------------------------------------------------------------
unsigned int unsigned int, uint xdr_uint
---------------------------------------------------------------------
enum enum xdr_enum
---------------------------------------------------------------------
bool int, bool_t xdr_bool
---------------------------------------------------------------------
long long xdr_long
---------------------------------------------------------------------
unsigned long unsigned long, ulong xdr_u_long
---------------------------------------------------------------------
float float xdr_float
---------------------------------------------------------------------
double double xdr_double
---------------------------------------------------------------------
opaque identifier[n] char identifier[n] xdr_opaque
---------------------------------------------------------------------
opaque identifier<n> struct { xdr_bytes
opaque identifier<> uint identifier_len;
char *identifier_val;
} identifier;
---------------------------------------------------------------------
string identifier<m> char *identifier xdr_string
string identifier<>
---------------------------------------------------------------------
typename identifier[n] typename identifier[n] xdr_vector
---------------------------------------------------------------------
typename identifier<m> struct { xdr_array
typename identifier<> uint identifier_len;
typename *identifier_val;
} identifier;
---------------------------------------------------------------------
struct { same as XDR custom filter
component-delar-A;
component-delar-B;
...
} identifier;
---------------------------------------------------------------------
union switch (discr) { struct identifier { custom filter
case discr-value-A: int discr;
arm-A; union {
case discr-value-B: arm-A;
arm-B; arm-B
... }
default: };
default-decl;
} identifier;
---------------------------------------------------------------------
void void xdr_void
---------------------------------------------------------------------
constant int xdr_int
---------------------------------------------------------------------
typename *identifier; struct identifier { custom filter
int discr;
union {
typename element;
void
}
}
---------------------------------------------------------------------
Bei der Syntaxbeschreibung wird die Backus-Naur Notation (siehe Anhang) verwendet.
declaration:
type-specifier identifier
| type-specifier identifier "[" value "]"
| type-specifier identifier "<" value ">"
| "opaque" identifier "<" value ">"
| "string" identifier "<" value ">"
| type-specifier "*" identifier
| "void"
value:
constant
| identifier
type-specifier:
[ "unsigned" ] "int"
| [ "unsigned" ] "hyper"
| "float"
| "double"
| "bool"
| enum-type-spec
| struct-type-spec
| union-type-spec
| identifier
enum-type-spec:
"enum" enum-body
enum-body:
"{"
( identifier "=" value )
( "," identifier "=" value )*
"}"
struct-type-spec:
"struct" struct-body
struct-body:
"{"
( declaration ";" )
( declaration ";" )*
"}"
union-type-spec:
"union" union-body
union-body:
"switch" "(" declaration ")" "{"
( "case" value ":" declaration ";" )
( "case" value ":" declaration ";" )*
[ "default" ":" declaration ";" ]
"}"
constant-def:
"const" identifier "=" constant ";"
type-def:
"typedef" declaration ";"
| "enum" identifier enum-body ";"
| "struct" identifier struct-body ";"
| "union" identifier union-body ";"
definition:
type-def
| constant-def
specification:
defintion *
keyword:
"bool" | "case" | "const" |
"default" | "double" | "enum" |
"float | "hyper" | "opaque" |
"string" | "struct" | "switch" |
"typedef" | "union" | "unsigned" |
"void"
identifier:
letter
| identifier letter
| identifier digit
| identifier "_"
must not be a keyword
letter:
upper and lower case are same
comment:
"/*" comment-text "*/"