Inhalt

3. XDR - eXternal Data Representation

3.1 Das Prinzip von XDR

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.

  1. Verschiedene Prozessoren verwenden verschiedene Zahlendarstellungen wie Big-, Little-Endian, 1- oder 2-Komplement, IEEE-Floats oder BCD (Binary Coded Decimal).
  2. Der gleiche Datentyp kann je nach Rechner verschiedene Anzahl von Bytes besetzen: z.B. 16- oder 32-Bit Integer.
  3. Verschiedene Programmiersprachen können abweichende Darstellungen der Datentypen haben, wie z.B. Strings mit 0 am Ende oder mit der Stringlänge im ersten Byte.
  4. Es gibt prozessor- oder compilercharakteristische Daten-Positionierung (Alignment). Sie führt zu Verschiebungen der Adressen oder zu den »Löchern« in struct-Gebilden.
  5. Programmdaten, die zwischen den Rechnern ausgetauscht werden sollen, können im Speicher komplizierte Gebilde mit Zeigern usw. bilden. Bei der Übertragung geht der topologische Bezug auf die Speicheradressen verloren. Die Daten müssen in einen Byte-Strom umgewandelt (»serialising«) und dann wieder zurückgewandelt (»deserialising«) 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:

  1. Funktionen, die Datenströme erzeugen und manipulieren (siehe Abschnitt Der RPC-Mechanismus)
  2. Konvertierungsfilter, die Eingangsdaten in einen Datenstrom und zurück umwandeln. (siehe Abschnitt Geschichte der RPC-Versionen)

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}).

3.2 XDR-Streams

Die Operationen der Serialisierung/Deserialisierung von Daten beziehen sich auf Datenströme, die realisiert werden entweder

  1. durch Standard-IO, d.h. mithilfe von FILEs aus <stdio.h>,
  2. direkt durch einen Puffer im Arbeitsspeicher (Memory Streams),
  3. oder allgemein durch einen Mechanismus, der noch genauer spezifiziert werden muß (Record Streams)

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.

x_op

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.

x_getlong, x_putlong

Schreiben und Lesen von longs von dem oder auf den Strom. Diese Funktionen nehmen die eigentliche Umwandlung vor (z.B. Big- nach Little-Endian).

x_getbytes, x_putbytes

Schreiben und Lesen von Bytesequenzen von dem oder auf den Strom. Sie geben je nach Ergebnis TRUE oder FALSE zurück.

x_getpostn, x_setpostn, x_destroy

Makros für Zugangsoperationen

x_inline

Nimmt als Parameter (XDR *) und die Anzahl der Bytes. Gibt die Adresse in dem strominternen Puffer zurück.

x_public

Daten des XDR-Benutzers. Sie sollen in der Stream-Implementation nicht benutzt werden.

x_private

Daten privat bezüglich der jeweiligen Stream-Implementierung

x_base

auch privat, wird für Informationen über die Position im Strom benutzt.

x_handy

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

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.

Memory Streams

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.

Record Streams

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.

XDR-Macros zur Strom-Manipulation.

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.

3.3 XDR - Filter

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.

Primitive Filter

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

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:

  1. strings,
  2. byte-arrays,
  3. opaque data,
  4. arrays,
  5. fixed size arrays
  6. discriminated unions
  7. pointer

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:

  1. discr, mit der Angabe der zur Übertragungszeit gültigen Komponente, die zuerst transformiert wird,
  2. unp, ein Pointer auf die eigentlichen Daten der Variante,
  3. arms, der Vektor von struct xdr_discrim, mit den Angaben für jede Komponente. Dieser Vektor muß mit NULL beendet sein.
  4. defaultarm kann ein NULL-Pointer sein. Sonst steht an dieser Stelle der Filter, der aufgerufen wird, wenn discr mit keinem value in dem arms-Vektor übereinstimmt.

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.

Custom Filter

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.

Filter-Beispiele

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.

3.4 Die XDR-Protokoll-Spezifikation

XDR-Datentypen

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
                                 }
                               }
      ---------------------------------------------------------------------

Die XDR-Syntax

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 "*/"


Inhalt