Chapter 6: Developing distributed applications using ONC RPC and XDR

Table of contents

Chapter 6

Developing distributed applications using ONC RPC and XDR

ONC(TM) RPC (Remote Procedure Call) was developed by Sun Microsystems to facilitate the development of distributed client/server applications. Using RPC, developers can write distributed applications in nearly the same way they write non-distributed applications today.

In RPC, the caller process causes the server process to execute a procedure call, much as if the calling code were executing the procedure call locally (in its own address space). Using RPC, the calling code and the called procedure run as two separate processes, so they do not have to execute on the same physical machine.

The RPC mechanism is implemented as a library of procedures, plus a specification for portable data transmission known as XDR (external data representation). Both RPC and XDR are portable, providing a standard I/O library for interprocess communication, either on one machine or across a network.

The rpcgen(NC) utility automatically generates header files and stubs linked into the client and server code to transparently perform the operations required to implement RPC and XDR.

Compiling RPC code

When you write an application that uses the the RPC protocol to call a program on a remote machine you use a C-like language called Remote Procedure Call Language (RPCL). Once the code is written in RPCL, use the rpcgen(NC) command to generate actual C language code that implements the remote procedure call in accordance with the RPC protocol.

The RPCL input may contain C-style comments and normal C language preprocessor directives. Comments are simply ignored, and the directives are copied uninterpreted into the output header file.

When creating XDR routines, you can customize them by leaving some of the data types undefined. When rpcgen encounters these undefined data types, it assumes the existence of a corresponding routine named xdr_type_name , where type_name is the name of the undefined data type.

Using the various options, you can compile XDR routines, compile C data-definitions (a header file), specify the name of the output file, or compile a server using the given transport.

See the rpcgen(NC) manual page for for more information.

Using remote procedure calls

Programs that communicate over a network need a paradigm for communication. The method used by the NFS is the Remote Procedure Call (RPC) paradigm, in which a client communicates with a server. In this process, the client first calls a procedure to send a request to the server. When the packet containing the request arrives, the server calls a dispatch routine, performs the service requested, sends back the reply, and the procedure call returns to the client.

You can think of the RPC interface as divided into three layers.

As a general rule, you should use the highest layer you need, and avoid the lower layers unless you must access some functionality they provide.

Although this document discusses only the interface to C, remote procedure calls can be made from any language. Moreover, although this document discusses RPC when used to communicate between processes running on different machines, it works just as well for communication between processes running on the same machine.


NOTE: The examples shown on the following pages illustrate the capabilities of RPC, but are not meant for actual use on your SCO system. This is because the rnusers routine that the examples rely on is not included with the SCO UNIX operating system.

Using the highest layer

Suppose you are writing a program that needs to know how many users are logged into a remote machine. You might do this by calling a library routine named rnusers, as illustrated here:

   #include <stdio.h>
   

main(argc, argv) int argc; char **argv; { unsigned num;

if (argc < 2) { fprintf(stderr, "usage: rnusers hostname\n"); exit(1); } if ((num = rnusers(argv[1])) < 0) { fprintf(stderr, "error: rnusers\n"); exit(-1); } printf("%d users on %s\n", num, argv[1]); exit(0); }

The rnusers routine is not included with this release of the software, but is shown as an example only.

The program above could be compiled with:

cc program.c -lrpcsvc -lsocket

Using the intermediate layer

The simplest interface, which explicitly makes RPC calls, uses the functions callrpc and registerrpc. Here is another way to get the number of remote users:

   #include <stdio.h>
   #include <utmp.h>
   #include <rpc/types.h>
   #include <rpc/xdr.h>
   #include <rpcsvc/rusers.h>
   

main(argc, argv) int argc; char **argv; { unsigned long nusers;

if (argc < 2) { fprintf(stderr, "usage: nusers hostname\n"); exit(-1); } if (callrpc(argv[1], RUSERSPROG, RUSERSVERS, RUSERSPROC_NUM, xdr_void, 0, xdr_u_long, &nusers) != 0) { fprintf(stderr, "error: callrpc\n"); exit(1); } printf("number of users on %s is %d\n", argv[1], nusers); exit(0); }

A program number, version number, and procedure number define each RPC procedure. The program number defines a group of related remote procedures, each of which has a different procedure number. Each program also has a version number, so when a minor change is made to a remote service (adding a new procedure, for example) a new program number does not have to be assigned.

When you call a procedure to find the number of remote users, the appropriate program, version and procedure numbers are looked up in a manual, in a similar manner to looking up the name of the memory allocator when memory is to be allocated.

The simplest routine in the RPC library used to make remote procedure calls is callrpc. It has eight parameters:

If callrpc completes successfully, it returns zero; nonzero otherwise. The exact meaning of each return code is found in <rpc/clnt.h>, and each return code is in fact an enum clnt_stat cast into an integer.

Because data types may be represented differently on different machines, callrpc needs both the type of the RPC argument and a pointer to the argument itself, and needs similar information for the result.

For RUSERSPROC_NUM, the return value is an unsigned long. This means that callrpc has xdr_u_long as its first return parameter, which says that the result is of type unsigned long , and has &nusers as its second return parameter, which is a pointer to where the long result will be placed. Because RUSERSPROC_NUM takes no argument, the argument type parameter of callrpc is xdr_void and the pointer to the argument parameter variable is NULL.

The callrpc procedure uses the User Datagram Protocol (UDP) to send a message over the network and wait for a response. If UDP receives no response, it again sends the message and waits for a response. After trying several times to deliver a message and receiving no response, callrpc returns with an error code. Methods for adjusting the number of retries or for using a different protocol require the use of the lower layer of the RPC library.

The remote server procedure corresponding to callrpc might look like this:

   char *
   nuser(indata)
           char *indata;
   {
           static int nusers;
   

/* * code here to compute the number of users * and place result in variable nusers */ return ((char *)&nusers); }

The procedure takes one argument, which is a pointer to the input of the remote procedure call (ignored in the above example) and it returns a pointer to the result. In the current version of C, character pointers are the generic pointers, so both the input argument and the return value are cast to char \(*. Normally, a server registers all of the RPC calls it plans to handle, and then goes into an infinite loop waiting to service requests. In this example, there is only a single procedure to register, so the main body of the server would look like this:

   #include <stdio.h>
   #include <rpcsvc/rusers.h>
   

char *nuser();

main() { registerrpc(RUSERSPROG, RUSERSVERS, RUSERSPROC_NUM, nuser, xdr_void, xdr_u_long); svc_run(); /* never returns */ fprintf(stderr, "Error: svc_run returned!\n"); exit(1); }

The registerrpc routine establishes which C procedure corresponds to each RPC procedure number. The parameters are:

Only the UDP transport mechanism can use registerrpc; thus, it is always safe in conjunction with calls generated by callrpc.


NOTE: The UDP transport mechanism can deal only with arguments and results less than 8K bytes in length.



Assigning program numbers

Program numbers are assigned in groups of 0x20000000 (536870912) according to the following chart:

0 - 1fffffff defined by Sun Microsystems
20000000 - 3fffffff defined by user
40000000 - 5fffffff transient
60000000 - 7fffffff reserved
80000000 - 9fffffff reserved
a0000000 - bfffffff reserved
c0000000 - dfffffff reserved
e0000000 - ffffffff reserved



Using XDR to pass arbitrary data types

In the previous example, the RPC call passes a single unsigned long. RPC can handle arbitrary data structures, regardless of the byte order or structure layout conventions of different machine architectures. It does this by always converting the data to a network standard, eXternal Data Representation (XDR), before sending the data over the wire. The process of converting from a particular machine representation to XDR format is called serializing, and the reverse process is called deserializing. The type field parameters of callrpc and registerrpc can be a built-in procedure like xdr_u_long in the previous example, or a user-supplied one. XDR has these built-in type routines: 

xdr_int() xdr_u_int() xdr_enum()
xdr_long() xdr_u_long() xdr_bool()
xdr_short() xdr_u_short() xdr_string()

As an example of a user-defined type routine, assume that you want to send the following structure:

   struct simple {
           int a;
           short b;
   } simple;
Then, callrpc should be called as
   callrpc(hostname, PROGNUM, VERSNUM, PROCNUM, xdr_simple, &simple ...);
where xdr_simple is written as:
   #include <rpc/rpc.h>
   

xdr_simple(xdrsp, simplep) XDR *xdrsp; struct simple *simplep; { if (!xdr_int(xdrsp, &simplep->a)) return (0); if (!xdr_short(xdrsp, &simplep->b)) return (0); return (1); }

An XDR routine returns nonzero (TRUE in the sense of C) if it completes successfully, and zero otherwise. A complete description of XDR is in the section ``Using the XDR protocol'', so this section only gives a few examples of XDR implementation.

In addition to the built-in primitives, there are also the prefabricated building blocks:

xdr_array() xdr_bytes()
xdr_reference() xdr_union()

To send a variable array of integers, you can package them in a structure like this:

   struct varintarr {
           int *data;
           int arrlnth;
   } arr;
and make an RPC call such as:
   callrpc(hostname, PROGNUM, VERSNUM, PROCNUM, xdr_varintarr, &arr...);
with xdr_varintarr defined as:
   xdr_varintarr(xdrsp, varintarr)
           XDR *xdrsp;
           struct varintarr *arrp;
   {
           return (xdr_array(xdrsp, &arrp->data, &arrp->arrlnth, MAXLEN,
                   sizeof(int), xdr_int));
   }
This routine takes as parameters the XDR handle, a pointer to the array, a pointer to the size of the array, the maximum allowable array size, the size of each array element, and an XDR routine for handling each array element. If the size of the array is known in advance, then the following could also be used to send out an array of length SIZE:
   int intarr[SIZE];
   

xdr_intarr(xdrsp, intarr) XDR *xdrsp; int intarr[]; { int i;

for (i = 0; i < SIZE; i++) { if (!xdr_int(xdrsp, &intarr[i])) return (0); } return (1); }

XDR always converts quantities to 4-byte multiples when deserializing. Thus, if either of the examples above involved characters instead of integers, each character would occupy 32 bits. That is the reason for the XDR routine xdr_bytes, which is like xdr_array except that it packs characters. It has four parameters which are the same as the first four parameters of xdr_array. For null-terminated strings, there is also the xdr_string routine, which is the same as xdr_bytes without the length parameter. On serializing, it gets the string length from strlen; on deserializing, it creates a null-terminated string.

Here is a final example that calls the previously written xdr_simple as well as the built-in functions xdr_string and xdr_reference, which chases pointers:

   struct finalexample {
           char *string;
           struct simple *simplep;
   } finalexample;
   xdr_finalexample(xdrsp, finalp)
           XDR *xdrsp;
           struct finalexample *finalp;
   {
           int i;
   

if (!xdr_string(xdrsp, &finalp->string, MAXSTRLEN)) return (0); if (!xdr_reference(xdrsp, &finalp->simplep, sizeof(struct simple), xdr_simple)) return (0); return (1); }

Using the lower layers

In the examples given so far, RPC takes care of many details automatically. This section shows how to change the defaults by using lower layers of the RPC library. It is assumed that the reader is familiar with sockets and the system calls for dealing with them.

In general, you should avoid using the lower layers of RPC. If you want to perform any of the following tasks, however, you must use the lower layers:



Sample server program

A number of assumptions are built into registerrpc: 

The server for the nusers program shown on the next page is written using a lower layer of the RPC package, which does not make these assumptions.
   #include <stdio.h>
   #include <rpc/rpc.h>
   #include <rpcsvc/rusers.h>
   

int nuser();

main() { SVCXPRT *transp; transp = svcudp_create(RPC_ANYSOCK); if (transp == NULL){ fprintf(stderr, "could not create an RPC server\n"); exit(1); } pmap_unset(RUSERSPROG, RUSERSVERS); if (!svc_register(transp, RUSERSPROG, RUSERSVERS, nuser, IPPROTO_UDP)) { fprintf(stderr, "could not register RUSER service\n"); exit(1); } svc_run(); /* never returns */ fprintf(stderr, "should never reach this point\n"); } nuser(rqstp, transp) struct svc_req *rqstp; SVCXPRT *transp; { unsigned long nusers; switch (rqstp->rq_proc) { case NULLPROC: if (!svc_sendreply(transp, xdr_void, 0)) { fprintf(stderr, "could not reply to RPC call\n"); exit(1); } return; case RUSERSPROC_NUM: /* * code here to compute the number of users * and put in variable nusers */ if (!svc_sendreply(transp, xdr_u_long, &nusers) { fprintf(stderr, "could not reply to RPC call\n"); exit(1); } return; default: svcerr_noproc(transp); return; } }

First, the server gets a transport handle, which is used for sending out RPC messages. The procedure registerrpc() uses svcudp_create to get a UDP handle. If you require a reliable protocol, call svctcp_create instead. If the argument to svcudp_create is RPC_ANYSOCK, the RPC library creates a socket on which to send out RPC calls. Otherwise, svcudp_create expects its argument to be a valid socket number. If you specify your own socket, it can be bound or unbound. If it is bound to a port by the user, the port numbers of svcudp_create and clntudp_create (the low-level client routine) must match.

When the user specifies RPC_ANYSOCK for a socket or gives an unbound socket, the system determines port numbers in the following way: when a server starts up, it advertises to a port mapper demon on its local machine, which picks a port number for the RPC procedure if the socket specified to svcudp_create is not already bound. When the clntudp_create call is made with an unbound socket, the system queries the port mapper on the machine to which the call is being made and gets the appropriate port number. If the port mapper is not running or has no port corresponding to the RPC call, the RPC call fails. Users can make RPC calls to the port mapper themselves. The appropriate procedure numbers are in the include file <rpc/pmap_prot.h>.

After creating an SVCXPRT, the next step is to call pmap_unset so that if the nusers server crashed earlier, any previous trace of it is erased before restarting. More precisely, pmap_unset erases the entry for RUSERS from the port mapper's tables.

Finally, the program number for nusers is associated with the procedure nuser. The final argument to svc_register is normally the protocol being used, which in this case is IPPROTO_UDP. Unlike registerrpc, there are no XDR routines involved in the registration process. Also, registration is done on the program level, rather than the procedure level.

The user routine nuser must call and dispatch the appropriate XDR routines, based on the procedure number. Two things are handled by nuser that are handled automatically by registerrpc:


The user service routine serializes the results and returns them to the RPC caller via svc_sendreply. Its first parameter is the SVCXPRT handle, the second is the XDR routine, and the third is a pointer to the data to be returned. Not illustrated above is how a server handles an RPC program that passes data. As an example, you can add the following procedure named RUSERSPROC_BOOL, which has an argument nusers, and returns TRUE or FALSE depending on whether there are nusers logged on:
   case RUSERSPROC_BOOL: {
           int bool;
           unsigned nuserquery;
   

if (!svc_getargs(transp, xdr_u_int, &nuserquery)) { svcerr_decode(transp); return; } /* * code to set nusers = number of users */ if (nuserquery == nusers) bool = TRUE; else bool = FALSE; if (!svc_sendreply(transp, xdr_bool, &bool)){ fprintf(stderr, "could not reply to RPC call\n"); exit(1); } return; }

The relevant routine is svc_getargs, which takes as arguments an SVCXPRT handle, the XDR routine, and a pointer to where the input is to be placed.


Using XDR to allocate memory

XDR routines do memory allocation in addition to doing input and output. This is why the second parameter of xdr_array is a pointer to an array, rather than the array itself. If it is NULL, then xdr_array allocates space for the array and returns a pointer to it, putting the size of the array in the third argument. As an example, consider the following XDR routine xdr_chararr1, which deals with a fixed array of bytes with length SIZE:

   xdr_chararr1(xdrsp, chararr)
           XDR *xdrsp;
           char chararr[];
   {
           char *p;
           int len;
   

p = chararr; len = SIZE; return (xdr_bytes(xdrsp, &p, &len, SIZE)); }

You can call it from a server like this:
   char chararr[SIZE];
   

svc_getargs(transp, xdr_chararr1, chararr);

where chararr has already allocated space.

If you want XDR to do the allocation, you would have to rewrite this routine in the following way:

   xdr_chararr2(xdrsp, chararrp)
           XDR *xdrsp;
           char **chararrp;
   {
           int len;
   

len = SIZE; return (xdr_bytes(xdrsp, chararrp, &len, SIZE)); }

The RPC call might then look like this:
   char *arrptr;
   

arrptr = NULL; svc_getargs(transp, xdr_chararr2, &arrptr); /* * use the result here */ svc_freeargs(xdrsp, xdr_chararr2, &arrptr);

After using the character array, it can be freed with svc_freeargs. In the routine xdr_finalexample given earlier, if finalp->string was NULL in the call
   svc_getargs(transp, xdr_finalexample, &finalp);
then
   svc_freeargs(xdrsp, xdr_finalexample, &finalp);
frees the array allocated to hold finalp->string; otherwise, it frees nothing. The same is true for finalp->simplep.

To summarize, each XDR routine is responsible for serializing, deserializing, and allocating memory. When an XDR routine is called from callrpc, the serializing part is used. When called from svc_getargs, the deserializer is used. When called from svc_freeargs, the memory deallocator is used. When building simple examples like those in this section, a user does not have to worry about the three modes. 

Sample client program

When you use callrpc, you have no control over the RPC delivery mechanism or the socket used to transport the data. To illustrate the layer of RPC that allows adjustment of these parameters, consider the following code to call the nusers service:

   #include <stdio.h>
   #include <rpc/rpc.h>
   #include <rpcsvc/rusers.h>
   #include <sys/socket.h>
   #include <sys/fs/nfs/time.h>
   #include <netdb.h>
   

main(argc, argv) int argc; char **argv; { struct hostent *hp; struct timeval pertry_timeout, total_timeout; struct sockaddr_in server_addr; int addrlen, sock = RPC_ANYSOCK; register CLIENT *client; enum clnt_stat clnt_stat; unsigned long nusers;

if (argc < 2) { fprintf(stderr, "usage: nusers hostname\n"); exit(-1); } if ((hp = gethostbyname(argv[1])) == NULL) { fprintf(stderr, "cannot get addr for '%s'\n", argv[1]); exit(-1); }

           pertry_timeout.tv_sec = 3;
           pertry_timeout.tv_usec = 0;
           addrlen = sizeof(struct sockaddr_in);
           bcopy(hp->h_addr, (caddr_t)&server_addr.sin_addr, hp->h_length);
           server_addr.sin_family = AF_INET;
           server_addr.sin_port =  0;
           if ((client = clntudp_create(&server_addr, RUSERSPROG,
               RUSERSVERS, pertry_timeout, &sock)) == NULL) {
                   perror("clntudp_create");
                   exit(-1);
           }
           total_timeout.tv_sec = 20;
           total_timeout.tv_usec = 0;
           clnt_stat = clnt_call(client, RUSERSPROC_NUM, xdr_void, 0,
               xdr_u_long, &nusers, total_timeout);
           if (clnt_stat != RPC_SUCCESS) {
                   clnt_perror(client, "rpc");
                   exit(-1);
           }
           clnt_destroy(client);
   }
The low-level version of callrpc is clnt_call, which takes a CLIENT pointer rather than a host name. The parameters to clnt_call are as follows: The CLIENT pointer is encoded with the transport mechanism. The procedure callrpc uses UDP; thus it calls clntudp_create to get a CLIENT pointer. To get TCP (Transmission Control Protocol), use clnttcp_create.

The parameters to clntudp_create are as follows:

The final argument to clnt_call is the total time to wait for a response. Thus, the number of tries is the clnt_call timeout divided by the clntudp_create timeout.

Note that the clnt_destroy call deallocates any space associated with the CLIENT handle, but it does not close the socket associated with it, which was passed as an argument to clntudp_create. The reason is that if there are multiple client handles using the same socket, then it is possible to close one handle without destroying the socket that other handles are using.

To make a stream connection, the call to clntudp_create is replaced with a call to clnttcp_create.

   clnttcp_create(&server_addr, prognum, versnum, &socket,
   	inputsize, outputsize);
There is no timeout argument; instead, the receive and send buffer sizes must be specified. When the clnttcp_create call is made, a TCP connection is established. All RPC calls using that CLIENT handle would use this connection. The server side of an RPC call using TCP has svcudp_create replaced by svctcp_create.

Using RPC/XDR for other tasks

This section discusses some other aspects of RPC. 

Using select on the server

Suppose a process is handling RPC requests while performing some other activity. If the other activity involves periodically updating a data structure, then the process can set an alarm signal before calling svc_run If, however, the other activity involves waiting for a file descriptor, the svc_run call will not work. The code for svc_run is:

   void
   svc_run()
   {
           int readfds;
   

for (;;) { readfds = svc_fds; switch (select(32, &readfds, NULL, NULL, NULL)) {

case -1: if (errno == EINTR) continue; perror("rstat: select"); return; case 0: break; default: svc_getreq(readfds); } } }

You can bypass svc_run and call svc_getreq directly. To do this, you need to know the file descriptors of the socket(s) associated with the programs for which you are waiting. Thus, you can write your own ``selects'' that wait on both the RPC socket and your own descriptors. 

Using broadcast RPC calls

The pmap and RPC protocols implement broadcast RPC. Here are the main differences between broadcast RPC and normal RPC calls:

   #include <rpc/pmap_clnt.h>
   

enum clnt_stat clnt_stat;

clnt_stat = clnt_broadcast(prog, vers, proc, xargs, argsp, xresults, resultsp, eachresult); ulong prog; /* program number */ ulong vers; /* version number */ ulong proc; /* procedure number */ xdrproc_t xargs; /* xdr routine for args */ caddr_t argsp; /* pointer to args */ xdrproc_t xresults; /* xdr routine for results */ caddr_t resultsp; /* pointer to results */ bool_t (*eachresult)(); /* call with each result obtained */

The procedure eachresult is called each time a valid result is obtained. It returns a boolean that indicates whether or not the client wants more responses.
   bool_t               done;
   

done = eachresult(resultsp, raddr); caddr_t resultsp; struct sockaddr_in *raddr; /* address of machine that sent response*/

If done is TRUE, then broadcasting stops and clnt_broadcast returns successfully. Otherwise, the routine waits for another response. The request is rebroadcast after a few seconds of waiting. If no responses come back, the routine returns with RPC_TIMEDOUT. To interpret clnt_stat errors, feed the error code to clnt_perrno 

Batching

The RPC architecture is designed so that clients send a call message and wait for servers to reply that the call succeeded. This implies that clients do not compute while servers are processing a call. This is inefficient if the client does not want or need an acknowledgement for every message sent. It is possible for clients to continue computing while waiting for a response, using RPC batch facilities.

RPC messages can be placed in a pipeline of calls to a desired server; this is called batching. Batching assumes the following:

Since the server does not respond to every call, the client can generate new calls in parallel with the server executing previous calls. Furthermore, the TCP/IP implementation can buffer up many call messages and send them to the server in one write system call. This overlapped execution greatly decreases the interprocess communication overhead of the client and server processes and the total elapsed time of a series of calls.

Since the batched calls are buffered, the client should eventually do a legitimate call to flush the pipeline.

A contrived example of batching follows. Assume a string-rendering service (like a window system) has two similar calls: one renders a string and returns void results, while the other renders a string and remains silent. The service (using the TCP/IP transport) may look like the following:

   #include <stdio.h>
   #include <rpc/rpc.h>
   #include <rpcsvc/windows.h>
   

void windowdispatch();

main() { SVCXPRT *transp;

transp = svctcp_create(RPC_ANYSOCK, 0, 0); if (transp == NULL){ fprintf(stderr, "could not create an RPC server\n"); exit(1); } pmap_unset(WINDOWPROG, WINDOWVERS); if (!svc_register(transp, WINDOWPROG, WINDOWVERS, windowdispatch, IPPROTO_TCP)) { fprintf(stderr, "could not register WINDOW service\n"); exit(1); } svc_run(); /* never returns */ fprintf(stderr, "should never reach this point\n"); }

   void
   windowdispatch(rqstp, transp)
       struct svc_req *rqstp;
       SVCXPRT *transp;
   {
       char *s = NULL;
   

switch (rqstp->rq_proc) { case NULLPROC: if (!svc_sendreply(transp, xdr_void, 0)) { fprintf(stderr, "could not reply to RPC call\n"); exit(1); } return; case RENDERSTRING: if (!svc_getargs(transp, xdr_wrapstring, &s)) { fprintf(stderr, "could not decode arguments\n"); svcerr_decode(transp); /* tell caller of mistake */ break; } /* * call here to to render the string s */ if (!svc_sendreply(transp, xdr_void, NULL)) { fprintf(stderr, "could not reply to RPC call\n"); exit(1); } break;

       case RENDERSTRING_BATCHED:
           if (!svc_getargs(transp, xdr_wrapstring, &s)) {
               fprintf(stderr, "could not decode arguments\n");
               /*
                * we are silent in the face of protocol errors
                */
               break;
           }
           /*
            * call here to to render the string s,
            * but sends no reply!
            */
           break;
       default:
           svcerr_noproc(transp);
           return;
       }
       /*
        * now free string allocated while decoding arguments
        */
       svc_freeargs(transp, xdr_wrapstring, &s);
   }
Of course the service could have one procedure that takes the string and a boolean to indicate whether or not the procedure should respond.

To take advantage of batching, the client must perform RPC calls on a TCP-based transport. The actual calls must have the following attributes:

Here is an example of a client that uses batching to render a bunch of strings; the batching is flushed when the client gets a null string:
   #include <stdio.h>
   #include <rpc/rpc.h>
   #include <rpcsvc/windows.h>
   #include <sys/socket.h>
   #include <sys/fs/nfs/time.h>
   #include <netdb.h>
   

main(argc, argv) int argc; char **argv; { struct hostent *hp; struct timeval pertry_timeout, total_timeout; struct sockaddr_in server_addr; int addrlen, sock = RPC_ANYSOCK; register CLIENT *client; enum clnt_stat clnt_stat; char buf[1000]; char *s = buf;

       /*
        */
       if ((client = clnttcp_create(&server_addr, WINDOWPROG,
           WINDOWVERS, &sock, 0, 0)) == NULL) {
           perror("clnttcp_create");
           exit(-1);
       }
       total_timeout.tv_sec = 0;
       total_timeout.tv_usec = 0;
       while (scanf("%s", s) != EOF) {
           clnt_stat = clnt_call(client, RENDERSTRING_BATCHED,
               xdr_wrapstring, &s, NULL, NULL, total_timeout);
           if (clnt_stat != RPC_SUCCESS) {
               clnt_perror(client, "batched rpc");
               exit(-1);
           }
       }
       /*
        * now flush the pipeline
        */
       total_timeout.tv_sec = 20;
       clnt_stat = clnt_call(client, NULLPROC,
           xdr_void, NULL, xdr_void, NULL, total_timeout);
       if (clnt_stat != RPC_SUCCESS) {
           clnt_perror(client, "rpc");
           exit(-1);
       }
   

clnt_destroy(client); }

Because the server sends no message, the clients cannot be notified of any failures that may occur. Therefore, clients are on their own when it comes to handling errors.

The above example was completed to render all of the (2000) lines in the file /etc/termcap. The rendering service did nothing but throw the lines away. The example was run in the following four configurations, with the results shown: 

            Configuration                     Timing (in seconds)

            machine to itself, regular RPC            50

            machine to itself, batched RPC            16

            machine to another, regular RPC           52

            machine to another, batched RPC           10
Running fscanf on /etc/termcap requires only six seconds. These timings show the advantage of protocols that allow for overlapped execution, although these protocols are often hard to design. 

Using authentication

In the examples presented so far, the caller never identified itself to the server, and the server never required an ID from the caller. Clearly, some network services, such as a network filesystem, require stronger security measures than those presented so far. In reality, every RPC call is authenticated by the RPC package on the server and, similarly, the RPC client package generates and sends authentication parameters. Just as different transports (TCP/IP or UDP/IP) can be used when creating RPC clients and servers, different forms of authentication can be associated with RPC clients; the authentication type used as a default is type none.

The authentication subsystem of the RPC package is open-ended, that is, numerous types of authentication are easy to support. However, this section describes the only type of authentication (other than none) supported in SCO NFS. 

The client side

When a caller creates a new RPC client handle as in:

   clnt = clntudp_create(address, prognum, versnum, wait, sockp)
the appropriate transport instance defaults the associate authentication handle to be:
   clnt->cl_auth = authnone_create();
The RPC client can choose to use authentication found in UNIX systems by setting clnt->cl_auth after creating the RPC client handle:
   clnt->cl_auth = authunix_create_default();
This causes each RPC call associated with clnt to carry with it the following authentication credentials structure:
   /*
    * UNIX type credentials.
    */
   struct authunix_parms {
   	ulong	aup_time;	/* credentials creation time */
   	char	*aup_machname;	/* host name of client machine */
   	int	aup_uid;	/* client's UNIX effective uid */
   	int	aup_gid;	/* client's current UNIX group id */
   	uint	aup_len;	/* the element length of aup_gids array */
   	int	*aup_gids;	/* array of groups to which user belongs */
   };
These fields are set by authunix_create_default by invoking the appropriate system calls.

Since the RPC user created this new style of authentication, the user is responsible for destroying it with:

   auth_destroy(clnt->cl_auth);
This should be done in all cases to conserve memory.

The server side

The RPC package passes the service dispatch routine a request that has an arbitrary authentication style associated with it. This creates difficulty for the service implementors dealing with authentication issues. For example, consider the fields of a request handle passed to a service dispatch routine:

   /* 
    * An RPC service request
    */
   struct svc_req {
           ulong        rq_prog;      /* service program number */
           ulong        rq_vers;      /* service protocol version number*/
           ulong        rq_proc;      /* the desired procedure number*/
           struct opaque_auth rq_cred; /* raw credentials from the "wire" */
           caddr_t       rq_clntcred;  /* read only, cooked credentials */
   };
The rq_cred is mostly opaque, except for one field of interest: the style of authentication credentials:
   /*
    * Authentication info.  Mostly opaque to the programmer.
    */
   struct opaque_auth {
       enum_t    oa_flavor;     /* style of credentials */
       caddr_t   oa_base;       /* address of more auth stuff */
       uint     oa_length;     /* not to exceed MAX_AUTH_BYTES */
   };
The RPC package guarantees the following to the service dispatch routine:
The remote users service example can be extended so that it computes results for all users except UID 16:
   nuser(rqstp, transp)
       struct svc_req *rqstp;
       SVCXPRT *transp;
   {
       struct authunix_parms *unix_cred;
       int uid;
       unsigned long nusers;
   

/* * we do not care about authentication for the null procedure */ if (rqstp->rq_proc == NULLPROC) { if (!svc_sendreply(transp, xdr_void, 0)) { fprintf(stderr, "could not reply to RPC call\n"); exit(1); } return; }

       /*
        * now get the uid
        */
       switch (rqstp->rq_cred.oa_flavor) {
       case AUTH_UNIX:
           unix_cred = (struct authunix_parms *) rqstp->rq_clntcred;
           uid = unix_cred->aup_uid;
           break;
       case AUTH_NULL:
       default:
           svcerr_weakauth(transp);
           return;
       }
       switch (rqstp->rq_proc) {
       case RUSERSPROC_NUM:
           /*
            * make sure the caller is allowed to call this procedure.
            */
           if (uid == 16) {
               svcerr_systemerr(transp);
               return;
           }
           /*
            * code here to compute the number of users
            * and put in variable nusers
            */
           if (!svc_sendreply(transp, xdr_u_long, &nusers) {
               fprintf(stderr, "could not reply to RPC call\n");
               exit(1);
           }
           return;
       default:
           svcerr_noproc(transp);
           return;
       }
   }
Note the following: The last point underscores the relation between the RPC authentication package and the services; RPC deals only with authentication and not with individual services' access control. The services themselves must implement their own access-control policies and reflect these policies as return status in their protocols. 

Supporting multiple program versions

By convention, the first version number of program FOO is FOOVERS_ORIG, and the most recent version is FOOVERS. Suppose there is a new version of the user program that returns an unsigned short rather than a long. If we name this version RUSERSVERS_SHORT, then a server that wants to support both versions would use a double register.

   if (!svc_register(transp, RUSERSPROG, RUSERSVERS_ORIG, nuser,
       IPPROTO_TCP)) {
           fprintf(stderr, "could not register RUSER service\n");
           exit(1);
   }
   if (!svc_register(transp, RUSERSPROG, RUSERSVERS_SHORT, nuser,
       IPPROTO_TCP)) {
           fprintf(stderr, "could not register RUSER service\n");
           exit(1);
   }
Both versions can be handled by the same C procedure:
   nuser(rqstp, transp)
       struct svc_req *rqstp;
       SVCXPRT *transp;
   {
       unsigned long nusers;
       unsigned short nusers2;
   

switch (rqstp->rq_proc) { case NULLPROC: if (!svc_sendreply(transp, xdr_void, 0)) { fprintf(stderr, "could not reply to RPC call\n"); exit(1); } return; case RUSERSPROC_NUM: /* * code here to compute the number of users * and put in variable nusers */ nusers2 = nusers; if (rqstp->rq_vers == RUSERSVERS_ORIG) if (!svc_sendreply(transp, xdr_u_long, &nusers)) { fprintf(stderr, "could not reply to RPC call\n"); exit(1); } else if (!svc_sendreply(transp, xdr_u_short, &nusers2)) { fprintf(stderr, "could not reply to RPC call\n"); exit(1); return; default: svcerr_noproc(transp); return; } }



Using different serialization and deserialization

Here is an example that is essentially equivalent to the rcp(TC) command. The initiator of the RPC snd call takes its standard input and sends it to the server rcv, which prints it on standard output. The RPC call uses TCP. This also illustrates an XDR procedure that behaves differently on serialization from the way it does on deserialization. 

The XDR routine

   /*
    * The xdr routine:
    *
    * on decode, read from wire, write onto fp
    * on encode, read from fp, write onto wire
    */
   #include <stdio.h>
   #include <rpc/rpc.h>
   

xdr_rcp(xdrs, fp) XDR *xdrs; FILE *fp; { unsigned long size; char buf[MAXCHUNK], *p;

if (xdrs->x_op == XDR_FREE)/* nothing to free */ return 1; while (1) { if (xdrs->x_op == XDR_ENCODE) { if ((size = fread (buf, sizeof(char), MAXCHUNK, fp)) == 0 && ferror(fp)) { fprintf(stderr, "could not fread\n"); exit(1); } } p = buf; if (!xdr_bytes(xdrs, &p, &size, MAXCHUNK)) return(0); if (size == 0) return(1); if (xdrs->x_op == XDR_DECODE) { if (fwrite(buf, sizeof(char), size, fp) != size) { fprintf(stderr, "could not fwrite\n"); exit(1); } } } }


The sender routines

   /*
    * The sender routines
    */
   #include <stdio.h>
   #include <netdb.h>
   #include <rpc/rpc.h>
   #include <sys/socket.h>
   #include <sys/fs/nfs/time.h>
   

main(argc, argv) int argc; char **argv; { int err;

if (argc < 2) { fprintf(stderr, "usage: %s server-name\n", argv[0]); exit(-1); } if ((err = callrpctcp(argv[1], RCPPROG, RCPPROC_FP, RCPVERS, xdr_rcp, stdin, xdr_void, 0)) != 0) { clnt_perrno(err); fprintf(stderr, " could not make RPC call\n"); exit(1); } }

   callrpctcp(host, prognum, procnum, versnum, inproc, in, outproc, out)
       char *host, *in, *out;
       xdrproc_t inproc, outproc;
   {
       struct sockaddr_in server_addr;
       int socket = RPC_ANYSOCK;
       enum clnt_stat clnt_stat;
       struct hostent *hp;
       register CLIENT *client;
       struct timeval total_timeout;
   

if ((hp = gethostbyname(host)) == NULL) { fprintf(stderr, "cannot get addr for '%s'\n", host); exit(-1); } bcopy(hp->h_addr, (caddr_t)&server_addr.sin_addr, hp->h_length); server_addr.sin_family = AF_INET; server_addr.sin_port = 0; if ((client = clnttcp_create(&server_addr, prognum, versnum, &socket, BUFSIZ, BUFSIZ)) == NULL) { perror("rpctcp_create"); exit(-1); } total_timeout.tv_sec = 20; total_timeout.tv_usec = 0; clnt_stat = clnt_call(client, procnum, inproc, in, outproc, out, total_timeout); clnt_destroy(client); return ((int)clnt_stat); }


The receiving routines

   #include <stdio.h>
   #include <rpc/rpc.h>
   

main() { register SVCXPRT *transp;

if ((transp = svctcp_create(RPC_ANYSOCK, 1024, 1024)) == NULL) { fprintf("svctcp_create: error\n"); exit(1); } pmap_unset(RCPPROG, RCPVERS); if (!svc_register(transp, RCPPROG, RCPVERS, rcp_service, IPPROTO_TCP)) { fprintf(stderr, "svc_register: error\n"); exit(1); } svc_run(); /* never returns */ fprintf(stderr, "svc_run should never return\n"); } rcp_service(rqstp, transp) register struct svc_req *rqstp; register SVCXPRT *transp; { switch (rqstp->rq_proc) { case NULLPROC: if (svc_sendreply(transp, xdr_void, 0) == 0) { fprintf(stderr, "err: rcp_service"); exit(1); } return; case RCPPROC_FP: if (!svc_getargs(transp, xdr_rcp, stdout)) { svcerr_decode(transp); return; } if (!svc_sendreply(transp, xdr_void, 0)) { fprintf(stderr, "cannot reply\n"); return; } exit(0); default: svcerr_noproc(transp); return; } }



Using callback procedures

Occasionally, it is useful to have a server become a client and make an RPC call back to the process that is its client. An example is remote debugging, where the client is a window system program and the server is a debugger running on the remote machine. Most of the time, the user clicks a mouse button at the debugging window, which converts this to a debugger command and then makes an RPC call to the server (where the debugger is actually running), telling it to execute that command. However, when the debugger hits a breakpoint, the roles are reversed, and the debugger wants to make an RPC call to the window program, so that it can inform the user that a breakpoint has been reached.

In order to do an RPC callback, you need a program number to make the RPC call. Since this will be a dynamically generated program number, it should be in the transient range, 0x40000000 - 0x5fffffff. The routine gettransient returns a valid program number in the transient range and registers it with the portmapper. It talks only to the portmapper running on the same machine as the gettransient routine itself. The call to pmap_set is a test and set operation, in that it tests atomically whether a program number has already been registered and, if it has not, reserves it. On return, the sockp argument will contain a socket that can be used as the argument to an svcudp_create or svctcp_create call.

   #include <stdio.h>
   #include <rpc/rpc.h>
   #include <sys/socket.h>
   

gettransient(proto, vers, sockp) int *sockp; { static int prognum = 0x40000000; int s, len, socktype; struct sockaddr_in addr;

switch(proto) { case IPPROTO_UDP: socktype = SOCK_DGRAM; break; case IPPROTO_TCP: socktype = SOCK_STREAM; break; default: fprintf(stderr, "unknown protocol type\n"); return 0; } if (*sockp == RPC_ANYSOCK) { if ((s = socket(AF_INET, socktype, 0)) < 0) { perror("socket"); return (0); } *sockp = s; } else s = *sockp; addr.sin_addr.s_addr = 0; addr.sin_family = AF_INET; addr.sin_port = 0; len = sizeof(addr); /* * may be already bound, so do not check for error */ (void) bind(s, &addr, len); if (getsockname(s, &addr, &len)< 0) { perror("getsockname"); return (0); } while (pmap_set(prognum++, vers, proto, ntohs(addr.sin_port)) == 0) continue; return (prognum-1); }

The following pair of programs illustrate how to use the gettransient routine. The client makes an RPC call to the server, passing it a transient program number. The client waits to receive a callback from the server at that program number. The server registers the program EXAMPLEPROG, so that it can receive the RPC call informing it of the callback program number. Then at some random time (on receiving an ALRM signal in this example), it sends a callback RPC call, using the program number it received earlier. 
Client program

   /*
    * client
    */
   #include <stdio.h>
   #include <rpc/rpc.h>
   

int callback(); char hostname[256];

main(argc, argv) char **argv; { int x, ans, s; SVCXPRT *xprt;

gethostname(hostname, sizeof(hostname)); s = RPC_ANYSOCK; x = gettransient(IPPROTO_UDP, 1, &s); fprintf(stderr, "client gets prognum %d\n", x);

if ((xprt = svcudp_create(s)) == NULL) { fprintf(stderr, "rpc_server: svcudp_create\n"); exit(1); } (void)svc_register(xprt, x, 1, callback, 0);

ans = callrpc(hostname, EXAMPLEPROG, EXAMPLEPROC_CALLBACK, EXAMPLEVERS, xdr_int, &x, xdr_void, 0); if (ans != 0) { fprintf(stderr, "call: "); clnt_perrno(ans); fprintf(stderr, "\n"); } svc_run(); fprintf(stderr, "Error: svc_run should not have returned\n"); }

   callback(rqstp, transp)
           register struct svc_req *rqstp;
           register SVCXPRT *transp;
   {
           switch (rqstp->rq_proc) {
                   case 0:
                           if (!svc_sendreply(transp, xdr_void, 0)) {
                                   fprintf(stderr, "err: rusersd\n");
                                   exit(1);
                               }
                           exit(0);
                   case 1:
                           if (!svc_getargs(transp, xdr_void, 0)) {
                                       svcerr_decode(transp);
                                   exit(1);
                           }
                           fprintf(stderr, "client got callback\n");
                           if (!svc_sendreply(transp, xdr_void, 0)) {
                                   fprintf(stderr, "err: rusersd");
                                   exit(1);
                           }
           }
   }

Server program

   /*
    * server
    */
   #include <stdio.h>
   #include <rpc/rpc.h>
   #include <sys/signal.h>
   

char *getnewprog(); char hostname[256]; int docallback(); int pnum; /*program number for callback routine */

main(argc, argv) char **argv; { gethostname(hostname, sizeof(hostname)); registerrpc(EXAMPLEPROG, EXAMPLEPROC_CALLBACK, EXAMPLEVERS, getnewprog, xdr_int, xdr_void); fprintf(stderr, "server going into svc_run\n"); alarm(10); signal(SIGALRM, docallback); svc_run(); fprintf(stderr, "Error: svc_run should not have returned\n"); }

   char *
   getnewprog(pnump)
           char *pnump;
   {
           pnum = *(int *)pnump;
           return NULL;
   }
   

docallback() { int ans;

ans = callrpc(hostname, pnum, 1, 1, xdr_void, 0, xdr_void, 0); if (ans != 0) { fprintf(stderr, "server: "); clnt_perrno(ans); fprintf(stderr, "\n"); } }

Using the XDR protocol

This section explains library routines that allow a C programmer to describe arbitrary data structures in a machine-independent fashion. The eXternal Data Representation (XDR) standard is the backbone of the Remote Procedure Call package, in the sense that data for remote procedure calls is transmitted using the standard. XDR library routines should be used to transmit data that is accessed (read or written) by more than one type of machine.

This chapter contains a guide to accessing currently available XDR streams, information on defining new streams and data types, and a formal definition of the XDR standard. XDR was designed to work across different languages, operating systems, and machine architectures. Most users, particularly RPC users, need only the information in the sections ``Compiling programs that contain XDR routines'', ``Creating portable data with XDR'', and ``XDR library primitives''. Programmers wishing to implement RPC and XDR on new machines will need the information in the sections ``XDR stream access'', ``XDR streams implementation'', and ``XDR standard''. Advanced topics, not necessary for all implementations, are covered in the section ``Advanced topics -- linked lists''.

Compiling programs that contain XDR routines

If your C programs use XDR routines, include the header file <rpc/rpc.h>. This file contains all the necessary interfaces to XDR.

Creating portable data with XDR

This section shows two programs, writer and reader, that lend themselves well to using XDR.


writer

   #include <stdio.h>
   

main() /* writer.c */ { long i;

for (i = 0; i < 8; i++) { if (fwrite((char *)&i, sizeof(i), 1, stdout) != 1) { fprintf(stderr, "failed!\n"); exit(1); } } }



reader

   #include <stdio.h>
   

main() /* reader.c */ { long 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"); }

The two programs appear to be portable for the following reasons: The behavior of these two programs can be verified by piping the output of the writer program to the reader program. For example, these programs should produce identical results when run on both an SCO system and a Sun workstation, as shown in the sample runs below.
   nix% writer | reader
   0 1 2 3 4 5 6 7
   nix%
   sun% writer | reader
   0 1 2 3 4 5 6 7
   sun%
With the advent of local area networks came the concept of network pipes, in which a process on one machine produces the data, and a second process on a different machine consumes the data. A network pipe can be constructed with writer and reader. Below is an example of a network pipe in which an SCO system produces data and a Sun workstation consumes the data.
   nix% writer | rcmd sun reader
   0 16777216 33554432 50331648 67108864 83886080 100663296 117440512
   nix%
If the machine on which each program is run is changed, the results will be the same. These results occur because the byte ordering of long integers differs between these machines. Other data types can have varying sizes, byte orderings, representations, and alignments, depending on the underlying hardware of the machine. For example, the number 01234567 is stored on an SCO system as follows: 
 -------------------------------------------------
      byte            contents
 -------------------------------------------------
        0                67
        1                45
        2                23
        3                01
A Sun stores the same number in the following way: 
 -------------------------------------------------
      byte            contents
 -------------------------------------------------
        0                01
        1                23
        2                45
        3                67
Note that 16777216 is 224; when four bytes are reversed, the 1 winds up in the 24th bit.

This example shows the need for portable data, a need which exists whenever data is shared by two or more machine types. Programs can be made data-portable by replacing the read() and write() calls with calls to an XDR library routine xdr_long(). This routine is a filter that knows the standard representation of a long integer in its external form.

The following programs show writer and reader revised to include xdr_long(). 

writer

   #include <stdio.h>
   #include <rpc/rpc.h> /*xdr is a sub-library of the rpc library*/
   

main() /* writer.c */ { XDR xdrs; long i;

xdrstdio_create(&xdrs, stdout, XDR_ENCODE); for (i = 0; i < 8; i++) { if (! xdr_long(&xdrs, &i)) { fprintf(stderr, "failed!\n"); exit(1); } } }



reader

   #include <stdio.h>
   #include <rpc/rpc.h> /* xdr is a sub-library of the rpc library */
   

main() /* reader.c */ { XDR xdrs; long i, j;

xdrstdio_create(&xdrs, stdin, XDR_DECODE); for (j = 0; j < 8; j++) { if (! xdr_long(&xdrs, &i)) { fprintf(stderr, "failed!\n"); exit(1); } printf("%ld ", i); } printf("\n"); }

Here are the results from executing the new programs in three different ways: both programs on an SCO system, both programs on a Sun workstation, and one program on each machine:
   nix% writer | reader
   0 1 2 3 4 5 6 7
   nix%
   sun% writer | reader
   0 1 2 3 4 5 6 7
   sun%
   xenix% writer | rcmd sun reader
   0 1 2 3 4 5 6 7
   xenix%
Dealing with integers is only a small part of portable data. Arbitrary data structures present portability problems, particularly with respect to alignment and pointers. Alignment on word boundaries may cause the size of a structure to vary from machine to machine. Pointers are convenient to use, but have no meaning outside the machine where they are defined.

The XDR library package solves data portability problems. It allows you to write and read arbitrary C constructs in a consistent, specified, well-documented manner. Thus, it makes sense to use the library even when the data is not shared among machines on a network.

The XDR library has filter routines for many subjects, including strings (null-terminated arrays of bytes), structures, unions, and arrays, to name a few. Using more primitive routines, you can write your own specific XDR routines to describe arbitrary data structures, including elements of arrays, arms of unions, or objects pointed at from other structures. The structures themselves may contain arrays of arbitrary elements or pointers to other structures.

The rest of this section examines the two programs more closely.


Implementation details

A family of XDR stream-creation routines exists in which each member treats the stream of bits differently. In the example given, data is manipulated using standard I/O routines, so xdrstdio_create() is used. The parameters to XDR stream-creation routines vary according to their function.

In the example, xdrstdio_create() takes a pointer to an XDR structure that it initializes, a pointer to a FILE that the input or output is performed on, and the operation. The operation may be XDR_ENCODE for serializing in the writer program, or XDR_DECODE for deserializing in the reader program.


NOTE: RPC clients never need to create XDR streams. The RPC system itself creates these streams, which are then passed to the clients.

The xdr_long() primitive is characteristic of most XDR library primitives and all client XDR routines:

In this case, xxx is long, and the corresponding XDR routine is the primitive xdr_long. The client could also define an arbitrary structure xxx, in which case the client would also supply the routine xdr_xxx, describing each field by calling XDR routines of the appropriate type. In all cases, the first parameter, xdrs, can be treated as an opaque handle and passed to the primitive routines.

XDR routines are direction independent; that is, the same routines are called to serialize or deserialize data. This feature is critical to software engineering of portable data. The intention is to call the same routine for either operation; this almost guarantees that serialized data can also be deserialized. One routine is used by both producer and consumer of networked data. This is implemented by always passing the address of an object rather than the object itself; only in the case of deserialization is the object modified. This feature is not shown in the example, but its value becomes obvious when nontrivial data structures are passed among machines. If needed, the direction of the XDR operation can be obtained.

Consider a slightly more complicated example. Assume that a person's gross assets and liabilities are to be exchanged among processes. Also assume that these values are important enough to warrant their own data type:

   struct gnumbers {
           long g_assets;
           long g_liabilities;
   };
The corresponding XDR routine describing this structure would be:
   bool_t                  /* TRUE is success, FALSE is failure */
   xdr_gnumbers(xdrs, gp)
           XDR *xdrs;
           struct gnumbers *gp;
   {
           if (xdr_long(xdrs, &gp->g_assets) &&
               xdr_long(xdrs, &gp->g_liabilities))
                   return(TRUE);
           return(FALSE);
   }
The parameter xdrs is never inspected or modified; it is only passed on to the subcomponent routines. It is imperative to inspect the return value of each XDR routine call, and to give up immediately and return FALSE if the subroutine fails.

This example also shows that the type bool_t is declared as an integer whose only values are TRUE (1) and FALSE (0). This section uses the following definitions:

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

#define enum_t int /* enum_t's are used for generic enum's */

Using these conventions, xdr_gnumbers() can be rewritten as follows:
   xdr_gnumbers(xdrs, gp)
           XDR *xdrs;
           struct gnumbers *gp;
   {
           return (xdr_long(xdrs, &gp->g_assets) &&
                   xdr_long(xdrs, &gp->g_liabilities));
   }
This section uses both coding styles.

XDR library primitives

This section gives a synopsis of each XDR primitive. It starts with basic data types and moves on to constructed data types. Finally, XDR utilities are discussed. The interface to these primitives and utilities is defined in the include file <rpc/xdr.h>, which is automatically included by <rpc/rpc.h>. 

Number filters

The XDR library provides primitives that translate between C numbers and their corresponding external representations. The primitives cover the set of numbers in:

   [signed, unsigned] * [short, int, long]
Specifically, the six primitives are:
   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; ulong *lup;

bool_t xdr_short(xdrs, sip) XDR *xdrs; short *sip;

bool_t xdr_u_short(xdrs, sup) XDR *xdrs; ushort *sup;

The first parameter, xdrs, is an XDR stream handle. The second parameter is the address of the number that provides data to the stream or receives data from it.

All routines return TRUE if they complete successfully, and FALSE otherwise.


Floating-Point filters

The XDR library also provides primitive routines for C's floating-point types:

   bool_t xdr_float(xdrs, fp)
           XDR *xdrs;
           float *fp;
   

bool_t xdr_double(xdrs, dp) XDR *xdrs; double *dp;

The first parameter, xdrs, is an XDR stream handle. The second parameter is the address of the floating point number that provides data to the stream or receives data from it.

All routines return TRUE if they complete successfully, and FALSE otherwise.


NOTE: Because the numbers are represented in IEEE floating point, routines may fail when decoding a valid IEEE representation into a machine-specific representation, or vice-versa.



Enumeration filters

The XDR library provides a primitive for generic enumerations. The primitive assumes that a C enum has the same representation inside the machine as a C integer. The boolean type is an important instance of the enum. The external representation of a boolean is always one (TRUE) or zero (FALSE).

   #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;

The second parameters, ep and bp, are addresses of the associated type that provides data to, or receives data from, the stream xdrs.

The routines return TRUE if they complete successfully, and FALSE otherwise. 

No data

Occasionally, an XDR routine must be supplied to the RPC system, even when no data is passed or required. The library provides such a routine:

   bool_t xdr_void();  /* always returns TRUE */


Constructed data type filters

Constructed or compound data-type primitives require more parameters and perform more complicated functions than the primitives discussed above. This section includes primitives for strings, arrays, unions, and pointers to structures.

Constructed data-type primitives may use memory management. In many cases, memory is allocated when deserializing data with XDR_DECODE. Therefore, the XDR package must provide means to deallocate memory. This is done by an XDR operation, XDR_FREE.

The three XDR directional operations are XDR_ENCODE, XDR_DECODE, and XDR_FREE. 

Strings

In C, a string is defined as a sequence of bytes terminated by a null byte, which is not considered when calculating string length. However, when a string is passed or manipulated, a pointer to it is used. Therefore, the XDR library defines a string to be a char *, and not a sequence of characters. The external representation of a string is drastically different from its internal representation. Externally, strings are represented as sequences of ASCII characters, while internally they are represented with character pointers. Conversion between the two representations is accomplished with the routine xdr_string():

   bool_t xdr_string(xdrs, sp, maxlength)
           XDR *xdrs;
           char **sp;
           uint maxlength;
The parameters operate as follows: The routine returns FALSE if the number of characters exceeds maxlength, and TRUE if it does not.

The behavior of xdr_string() is similar to the behavior of other routines discussed in this section. The direction XDR_ENCODE is easiest to understand. The parameter sp points to a string of a certain length; if it does not exceed maxlength, the bytes are serialized.

The effect of deserializing a string is subtle.

In the XDR_FREE operation, the string is obtained by dereferencing sp. If the string is not NULL, it is freed and *sp is set to NULL. In this operation, xdr_string ignores the maxlength parameter. 
Byte arrays

Using variable-length arrays of bytes are often preferable to strings. Byte arrays differ from strings in the following ways:

The primitive xdr_bytes() converts between the internal and external representations of byte arrays:
   bool_t xdr_bytes(xdrs, bpp, lp, maxlength)
           XDR *xdrs;
           char **bpp;
           uint *lp;
           uint maxlength;
The usage of the first, second, and fourth parameters is identical to the first, second, and third parameters of xdr_string(), respectively. The length of the byte area is obtained by dereferencing lp when serializing; *lp is set to the byte length when deserializing. 
Arrays

The XDR library package provides a primitive for handling arrays of arbitrary elements. The xdr_bytes() routine treats a subset of generic arrays in which the size of array elements is known to be 1 and the external description of each element is built in. The generic array primitive xdr_array() requires parameters identical to those of xdr_bytes() plus two more: the size of array elements and an XDR routine to handle each of the elements. This routine is called to encode or decode each element of the array.

   bool_t xdr_array(xdrs, ap, lp, maxlength, elementsize, xdr_element)
           XDR *xdrs;
           char **ap;
           uint *lp;
           uint maxlength;
           uint elementsize;
           bool_t (*xdr_element)();
The parameter ap is the address of the pointer to the array. If *ap is NULL when the array is being deserialized, XDR allocates an array of the appropriate size and sets *ap to that array. The element count of the array is obtained from *lp when the array is serialized; *lp is set to the array length when the array is deserialized. The parameter maxlength is the maximum number of elements that the array is allowed to have; elementsize is the byte size of each element of the array. (The C function sizeof() can be used to obtain this value.) The routine xdr_element is called to serialize, deserialize, or free each element of the array. 
Examples

Using the data types presented so far, you can define a single user, define multiple users, and create a command history. 

Defining a single user

A user on a networked machine can be identified by the following:

A structure with this information and its associated XDR routine could be coded as follows:
   struct netuser {
           char        *nu_machinename;
           int        nu_uid;
           uint        nu_glen;
           int        *nu_gids;
   };
   #define NLEN 255 /* machine names must be shorter than 256 chars */
   #define NGRPS 20 /* user cannot be a member of more than 20 groups */
   

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


Defining multiple users

You can implement a party of network users as an array of netuser structure. The declaration and its associated XDR routines are as follows:

   struct party {
           uint p_len;
           struct netuser *p_nusers;
   };
   #define PLEN 500 /* max number of users in a party */
   

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


Creating command history

You can combine the well-known parameters to main(), argc and argv into a structure, then use an array of these structures to make up a history of commands. The declarations and XDR routines might look like:

   struct cmd {
           uint c_argc;
           char **c_argv;
   };
   #define ALEN 1000  /* args can be no longer than 1000 chars */
   #define NARGC 100  /* commands can have no more than 100 args */
   

struct history { uint h_len; struct cmd *h_cmds; }; #define NCMDS 75 /* history is no more than 75 commands */

bool_t xdr_wrap_string(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_wrap_string)); }

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

The routine xdr_wrap_string() is needed to package the xdr_string() routine because the implementation of xdr_array() only passes two parameters to the array element description routine; xdr_wrap_string() supplies the third parameter to xdr_string().

Constructed opaque data

In some protocols, handles are passed from server to client. The client passes the handle back to the server at some later time. Handles are never inspected by clients; they are obtained and submitted. That is to say, handles are opaque. The primitive xdr_opaque() is used for describing fixed sized, opaque bytes:

   bool_t xdr_opaque(xdrs, p, len)
           XDR *xdrs;
           char *p;
           uint len;
The parameter p is the location of the bytes; len is the number of bytes in the opaque object. By definition, the actual data contained in the opaque object is not machine portable. 
Fixed-Sized arrays

The XDR library does not provide a primitive for fixed-sized arrays. (The primitive xdr_array() is for varying-length arrays.) You can rewrite the example to define a single user to use fixed-sized arrays in the following fashion:

   #define NLEN 255  /* machine names must be shorter than 256 chars */
   #define NGRPS 20  /* user cannot be a member of more than 20 groups */
   

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); for (i = 0; i < NGRPS; i++) { if (! xdr_int(xdrs, &nup->nu_gids[i])) return (FALSE); } return (TRUE); }


Constructed discriminated unions

The XDR library supports discriminated unions. A discriminated union is a C union and an enum_t value that selects an ``arm'' of the union:

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

First, the routine translates the discriminant of the union located at *dscmp. The discriminant is always an enum_t. Next, the union located at *unp is translated. The parameter arms is a pointer to an array of xdr_discrim structures. Each structure contains an order pair of [value,proc]. If the union's discriminant is equal to the associated value, then the proc is called to translate the union. The end of the xdr_discrim structure array is denoted by a routine of value NULL (0). If the discriminant is not found in the arms array, then the defaultarm procedure is called, assuming it is non-NULL; otherwise, the routine returns FALSE. 
Using constructed discriminated unions

Suppose the type of a union is an integer, a character pointer (a string), or a gnumbers structure. Also, assume the union and its current type are declared in a structure. The declaration is:

   enum utype { INTEGER=1, STRING=2, GNUMBERS=3 };
   

struct u_tag { enum utype utype; /* this is the union's discriminant */ union { int ival; char *pval; struct gnumbers gn; } uval; };

The following constructs and XDR procedure (de)serialize the discriminated union:
   struct xdr_discrim u_tag_arms[4] = {
           { INTEGER, xdr_int },
           { GNUMBERS, xdr_gnumbers }
           { STRING, xdr_wrap_string },
           { __dontcare__, NULL }
           /* always terminate arms with a 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)); }

The routines were originally described in the following sections: Therefore, the value of the union's discriminant may legally take on only values listed in the u_tag_arms array. This example also demonstrates that the elements of the arm's array do not need to be sorted.

It should be noted that the values of the discriminant may be sparse, though in this example they are not. It is always good practice to explicitly assign integer values to each element of the discriminant's type. This practice both documents the external representation of the discriminant and guarantees that different C compilers emit identical discriminant values.


Pointers

In C, it is often convenient to put pointers to another structure within a structure. The primitive xdr_reference() makes it easy to serialize, deserialize, and free these referenced structures:

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

Parameter pp is the address of the pointer to the structure. Parameter ssize is the size in bytes of the structure, which you can obtain by using the C function sizeof(). Parameter proc is the XDR routine that describes the structure. When decoding data, storage is allocated if *pp is NULL.

There is no need for a primitive xdr_struct() to describe structures within structures, because pointers are always sufficient.

Note that xdr_reference() and xdr_array() are not interchangeable external representations of data. 

Using pointers

Suppose there is a structure containing a person's name and a pointer to a gnumbers structure containing the person's gross assets and liabilities. The construct is:

   struct pgn {
           char *name;
           struct gnumbers *gnp;
   };
The corresponding XDR routine for this structure is:
   bool_t
   xdr_pgn(xdrs, pp)
           XDR *xdrs;
           struct pgn *pp;
   {
           if (xdr_string(xdrs, &pp->name, NLEN) &&
               xdr_reference(xdrs, &pp->gnp, sizeof(struct gnumbers),
               xdr_gnumbers))
                   return(TRUE);
           return(FALSE);
   }

Pointer semantics and XDR

In many applications, C programmers attach double meaning to the values of a pointer. Typically, the value NULL or zero means data is not needed, yet some application-specific interpretation applies. In essence, the C programmer is encoding a discriminated union efficiently by overloading the interpretation of the value of a pointer. For instance, in the following example, a NULL pointer value for gnp could indicate that the person's assets and liabilities are unknown. That is, the pointer value encodes two things: whether or not the data is known and, if it is known, where it is located in memory. Linked lists are an extreme example of the use of application-specific pointer interpretation.

The primitive xdr_reference() cannot and does not attach any special meaning to a NULL-value pointer during serialization. That is, passing an address of a pointer whose value is NULL to xdr_reference() when serialing data will most likely cause a memory fault and, on UNIX systems, a core dump for debugging.

As a programmer, you must expand all other pointers, that is, NULL-value pointers, into their specific semantics. This usually involves describing data with a two-armed discriminated union. One arm is used when the pointer is valid; the other is used when the pointer is invalid (NULL). The section ``Advanced topics -- linked lists'' shows an example (linked-lists encoding) that deals with invalid pointer interpretation. 

Non-filter primitives

XDR streams can be manipulated with the primitives discussed in this section.

   uint xdr_getpos(xdrs)
           XDR *xdrs;
   

bool_t xdr_setpos(xdrs, pos) XDR *xdrs; uint pos;

xdr_destroy(xdrs) XDR *xdrs;

The routine xdr_getpos() returns an unsigned integer that describes the current position in the data stream.


NOTE: In some XDR streams, the returned value of xdr_getpos() is meaningless; the routine returns a -1 in this case, although -1 should be a legitimate value.

The routine xdr_setpos() sets a stream position to pos.


NOTE: In some XDR streams, setting a position is impossible; in such cases, xdr_setpos() returns FALSE. This routine also fails if the requested position is out of bounds. The definition of bounds varies from stream to stream.

The xdr_destroy() primitive destroys the XDR stream. Use of the stream after calling this routine is undefined.


XDR operation directions

At times, you may want to optimize XDR routines by taking advantage of the direction of the operation (XDR_ENCODE, XDR_DECODE, or XDR_FREE). The value xdrs->x_op always contains the direction of the XDR operation. Programmers are not encouraged to take advantage of this information, so no example is presented here. However, an example in the section ``Advanced topics -- linked lists'' demonstrates the usefulness of the xdrs->x_op field.

XDR stream access

An XDR stream is obtained by calling the appropriate creation routine. Such creation routines take arguments that are tailored to the specific properties of the stream.

Streams currently exist for the serialization and deserialization of data to or from standard I/O FILE streams, TCP/IP connections, files and memory. The section ``XDR streams implementation'' describes the XDR object and how to make new XDR streams when they are required. 

Standard I/O streams

You can interface XDR streams to standard I/O by using the xdrstdio_create():

   #include <stdio.h>
   #include <rpc/rpc.h>  /*xdr streams are part of the rpc library*/
   

void xdrstdio_create(xdrs, fp, x_op) XDR *xdrs; FILE *fp; enum xdr_op x_op;

The routine xdrstdio_create() initializes an XDR stream pointed to by xdrs. The XDR stream interfaces to the standard I/O library. Parameter fp is an open file, and x_op is an XDR direction.


Memory streams

Memory streams allow the streaming of data into or out of a specified area of memory:

   #include <rpc/rpc.h>
   

void xdrmem_create(xdrs, addr, len, x_op) XDR *xdrs; char *addr; uint len; enum xdr_op x_op;

The routine xdrmem_create() initializes an XDR stream in local memory. The memory is pointed to by parameter addr; parameter len is the length in bytes of the memory. The parameters xdrs and x_op are identical to the corresponding parameters of xdrstdio_create(). Currently, the UDP/IP implementation of RPC uses xdrmem_create(). Complete call or result messages are built into memory before calling the sendto() system routine. 

Record (TCP/IP) streams

A record stream is an XDR stream built on top of a record-marking standard that is built on top of an ordinary file or BSD connection interface.

   #include <rpc/rpc.h>  /* xdr streams are a part of the rpc library */
   

xdrrec_create(xdrs, sendsize, recvsize, iohandle, readproc, writeproc) XDR *xdrs; uint sendsize, recvsize; char *iohandle; int (*readproc)(), (*writeproc)();

The routine xdrrec_create() provides an XDR stream interface that allows for a bidirectional, arbitrarily long sequence of records. The contents of the records are meant to be data in XDR form. The stream's primary use is for interfacing RPC to TCP connections. However, it can be used to stream data into or out of ordinary files.

The parameter xdrs is similar to the corresponding parameter described above. The stream does its own data buffering, similar to that of standard I/O. The parameters sendsize and recvsize determine the size in bytes of the output and input buffers, respectively; if their values are zero (0), then predetermined defaults are used. When a buffer needs to be filled or flushed, the routine readproc or writeproc, respectively, is called. The usage and behavior of these routines are similar to the system calls read() and write().

However, the first parameter to each of these routines is the opaque parameter iohandle. The other two parameters (buf and nbytes) and the results (byte count) are identical to the system routines. If xxx is readproc or writeproc, then it has the following form:

   /* returns the actual number of bytes transferred. -1 is an error.  */
   int
   xxx(iohandle, buf, len)
           char *iohandle;
           char *buf;
           int nbytes;
The XDR stream provides means for delimiting records in the byte stream. The primitives that are specific to record streams are as follows:
   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;

The routine xdrrec_endofrecord() causes the current outgoing data to be marked as a record. If the parameter flushnow is TRUE, then the stream's writeproc() will be called; otherwise, writeproc() will be called when the output buffer has been filled.

The routine xdrrec_skiprecord() causes an input stream's position to be moved past the current record boundary and onto the beginning of the next record in the stream.

If there is no more data in the stream's input buffer, then the routine xdrrec_eof() returns TRUE. This does not imply that there is no more data in the underlying file descriptor.

XDR streams implementation

This section provides the abstract data types needed to implement new instances of XDR streams. 

The XDR object

The following structure defines the interface to an XDR stream:

   enum xdr_op { XDR_ENCODE = 0, XDR_DECODE = 1, XDR_FREE = 2 };
   

typedef struct { enum xdr_op x_op; /* operation; fast additional param */ struct xdr_ops { bool_t (*x_getlong)(); /* get a long from underlying stream */ bool_t (*x_putlong)(); /* put a long to " */ bool_t (*x_getbytes)(); /* get some bytes from " */ bool_t (*x_putbytes)(); /* put some bytes to " */ uint (*x_getpostn)(); /* returns byte offset from beginning */ bool_t (*x_setpostn)(); /* repositions position in stream */ caddr_t (*x_inline)(); /* buf ptr to buffered data */ VOID (*x_destroy)(); /* free privates of this xdr_stream */ } *x_ops; caddr_t x_public; /* users' data */ caddr_t x_private; /* pointer to private data */ caddr_t x_base; /* private used for position info */ int x_handy; /* extra private word */ } XDR;

The x_op field is the current operation being performed on the stream. This field is important to the XDR primitives, but should not affect the implementation of a stream. That is, the implementation of a stream should not depend on this value. The fields x_private, x_base, and x_handy are private to the particular stream's implementation. The field x_public is for the XDR client and should never be used by the XDR stream implementations or the XDR primitives.

Macros for accessing operations x_getpostn(), x_setpostn(), and x_destroy() were defined in the section ``Non-filter primitives''. The operation x_inline() takes two parameters: an XDR * and an unsigned integer, which is a byte count.

The routine returns a pointer to a piece of the stream's internal buffer. The caller can then use the buffer segment for any purpose. From the point of view of the stream, the bytes in the buffer segment have been consumed or put.

The routine may return NULL if it cannot return a buffer segment of the requested size. (The x_inline routine is for cycle squeezers. Use of the resulting buffer is not data-portable. Programmers are encouraged not to use this feature.)

The operations x_getbytes() and x_putbytes() blindly get and put sequences of bytes from or to the underlying stream. They return TRUE if they are successful, and FALSE otherwise. The routines have identical parameters (replace xxx):

   bool_t
   xxxbytes(xdrs, buf, bytecount)
           XDR *xdrs;
           char *buf;
           uint bytecount;
The operations x_getlong() and x_putlong() receive and put long numbers from and to the data stream. It is the responsibility of these routines to translate the numbers between the machine representation and the (standard) external representation. The system primitives htonl() and ntohl() can be helpful in accomplishing this. The section ``XDR standard'' defines the standard representation of numbers. The higher-level XDR implementation assumes that signed and unsigned long integers contain the same number of bits, and that non-negative integers have the same bit representations as unsigned integers.

The routines return TRUE if they succeed, and FALSE otherwise. They have identical parameters:

   bool_t
   xxxlong(xdrs, lp)
           XDR *xdrs;
           long *lp;
Implementors of new XDR streams must make an XDR structure (with new operation routines) available to clients, using some kind of create routine.

XDR standard

This section defines the XDR standard. This standard is independent of languages, operating systems and hardware architectures. Once data is shared among machines, it should not matter that the data was produced on an SCO system, but is consumed by a Sun workstation, or conversely. Similarly, the choice of operating systems should have no influence on how the data is represented externally. For programming languages, data produced by a C program should be readable by a FORTRAN or Pascal program.

The XDR standard depends on the assumption that bytes (or octets) are portable. A byte is defined to be eight bits of data. It is assumed the hardware that encodes bytes onto various media preserves the meaning of those bytes across hardware boundaries. For example, the Ethernet standard suggests that bytes be encoded using the ``little endian'' format. Hardware implementations of both Sun workstation and SCO platforms adhere to the standard.

The XDR standard also suggests a language used to describe data. The language is a variant of C in that it is a data description language, not a programming language. In a similar way, the Xerox Courier Standard uses a variant of Mesa as its data description language. 

Basic block size

The representation of all items requires a multiple of four bytes (or 32 bits) of data. The bytes are numbered 0 through n-1. The bytes are read from or written to some byte stream such that byte m always precedes byte m+1. 

Integer

An XDR signed integer is a 32-bit piece of data that encodes an integer in the range [-2147483648,2147483647]. The integer is represented in two's complement notation. The most and least significant bytes are 0 and 3, respectively. The data description of integers is integer. 

Unsigned integer

An XDR unsigned integer is a 32-bit piece of data that encodes a nonnegative integer in the range [0,4294967295]. It is represented by an unsigned binary number whose most and least significant bytes are 0 and 3, respectively. The data description of unsigned integers is unsigned.


Enumerations

Enumerations are useful for describing subsets of the integers. Enumerations have the same representation as integers. The data description of enumerated data is as follows:

   typedef enum { name = value, .... } type-name;
For example, the three colors red, yellow, and blue could be described by an enumerated type:
   typedef enum { RED = 2, YELLOW = 3, BLUE = 5 } colors;


Booleans

Booleans are important enough and occur frequently enough to warrant their own explicit type in the standard. Boolean is an enumeration with the following form:

   typedef enum { FALSE = 0, TRUE = 1 } boolean;


Hyper integer and hyper unsigned

The standard also defines 64-bit (8-byte) numbers called hyper integer and hyper unsigned. Their representations are the obvious extensions of the integer and unsigned, defined above. The most and least significant bytes are 0 and 7, respectively. 

Floating point and double precision

The standard defines the encoding for the floating-point data types float (32 bits or 4 bytes) and double (64 bits or 8 bytes). The encoding used is the IEEE standard for normalized single- and double-precision floating point numbers. (See the IEEE floating-point standard for more information.) The standard encodes the following three fields, which describe the floating point number:

S
The sign of the number. Values 0 and 1 represent positive and negative, respectively.

E
The exponent of the number, base 2. Floats devote 8 bits to this field, while doubles devote 11 bits. The exponents for float and double are biased by 127 and 1023, respectively.

F
The fractional part of the number's mantissa, base 2. Floats devote 23 bits to this field, while doubles devote 52 bits.
Therefore, the floating-point number is described by:

(-1)^S x 2^(E-Bias) x 1.F

Just as the most and least significant bytes of a number are 0 and 3, the most and least significant bits of a single-precision floating point number are 0 and 31. The beginning and most significant bit offsets of S, E, and F are 0, 1, and 9, respectively.

Doubles have the analogous extensions. The beginning and most significant bit offsets of S, E, and F are 0, 1, and 12, respectively.

The IEEE specification should be consulted concerning the encoding for signed zero, signed infinity (overflow) and denormalized numbers (underflow). Under IEEE specifications, the ``NaN'' (not a number) is system-dependent and should not be used. 

Standard opaque data

At times, fixed-sized uninterpreted data needs to be passed among machines. This data is called opaque and is described as:

   typedef opaque type-name[n];
   opaque name[n];
where n is the (static) number of bytes necessary to contain the opaque data. If n is not a multiple of four, the n bytes are followed by enough (up to 3) zero-valued bytes to make the total byte count of the opaque object a multiple of four. 

Counted-Byte strings

The standard defines a string of n (numbered 0 through n-1) bytes to be the number n encoded as unsigned, and followed by the n bytes of the string. If n is not a multiple of four, the n bytes are followed by enough (up to 3) zero-valued bytes to make the total byte count a multiple of four. The data description of strings is as follows:

   typedef string type-name<N>;
   typedef string type-name<>;
   string name<N>;
   string name<>;
The data description language uses angle brackets (< and >) to denote anything that is of varying length, as opposed to square brackets to denote fixed-length sequences of data.

The constant N denotes an upper bound of the number of bytes that a string may contain. If N is not specified, it is assumed to be 2^32 - 1, the maximum length. The constant N would normally be found in a protocol specification. For example, a filing protocol may state that a file name can be no longer than 14 bytes, such as:

   string filename<14>;
The XDR specification does not say what the individual bytes of a string represent; this important information is left to higher-level specifications. A reasonable default is to assume that the bytes encode ASCII characters. 

Fixed arrays

The data description for fixed-size arrays of homogeneous elements is as follows:

   typedef elementtype type-name[n];
   elementtype name[n];
Fixed-size arrays of elements numbered 0 through n-1 are encoded by individually encoding the elements of the array in their natural order, 0 through n-1. 

Counted arrays

Counted arrays provide the ability to encode variable-length arrays of homogeneous elements. The array is encoded as the element count n (an unsigned integer), followed by the encoding of each of the array's elements, starting with element 0 and progressing through element n-1. The data description for counted arrays is similar to that of counted strings:

   typedef elementtype type-name<N>;
   typedef elementtype type-name<>;
   elementtype name<N>;
   elementtype name<>;
Again, the constant N specifies the maximum acceptable element count of an array; if N is not specified, it is assumed to be 2^32 - 1. 

Structures

The data description for structures is very similar to that of standard C:

   typedef struct {
           component-type component-name;
           ...
   } type-name;
The components of the structure are encoded in the order of their declaration in the structure.


Standard discriminated unions

A discriminated union is a type composed of a discriminant followed by a type selected from a set of prearranged types according to the value of the discriminant. The type of the discriminant is always an enumeration. The component types are called ``arms'' of the union. The discriminated union is encoded as its discriminant followed by the encoding of the implied arm. The data description for discriminated unions is as follows:

   typedef union switch (discriminant-type) {
           discriminant-value: arm-type;
           ...
           default: default-arm-type;
   } type-name;
The default arm is optional. If it is not specified, then a valid encoding of the union cannot take on unspecified discriminant values. Most specifications neither need nor use default arms. 

Missing specifications

The standard lacks representations for bit fields and bitmaps, since the standard is based on bytes. This does not imply that no specification should be attempted.


Library primitive / XDR standard cross-reference

The following table shows the association between the C library primitives discussed in the section ``XDR library primitives'' and the standard data types defined in this section. It also shows the subsections within these two document sections where each primitive and data type is discussed. 

 -------------|------------------|-------------------------------------
   C Primitive|  XDR Type        |  Sections
 -------------|------------------|-------------------------------------
       xdr_int|                  |
      xdr_long|  integer         |  Number filters
              |                  |  Integer
     xdr_short|                  |
 -------------|------------------|-------------------------------------
     xdr_u_int|                  |
    xdr_u_long|  unsigned        |  Number filters
              |                  |  Unsigned Integer
   xdr_u_short|                  |
 -------------|------------------|-------------------------------------
             -|  hyper integer   |  Hyper integer and hyper unsigned
              |  hyper unsigned  |
 -------------|------------------|-------------------------------------
     xdr_float|  float           |  Floating-point filters
              |                  |  Floating point and double precision
 -------------|------------------|-------------------------------------
    xdr_double|  double          |  Floating-point filters
              |                  |  Floating point and double precision
 -------------|------------------|-------------------------------------
      xdr_enum|  enum_t          |  Enumeration filters
              |                  |  Enumerations
 -------------|------------------|-------------------------------------
      xdr_bool|  bool_t          |  Enumeration filters
              |                  |  Booleans
 -------------|------------------|-------------------------------------
    xdr_string|  string          |  Strings
              |                  |  Counted-byte strings
     xdr_bytes|                  |  Byte arrays
 -------------|------------------|-------------------------------------
     xdr_array|  (varying arrays)|  Arrays
              |                  |  Counted arrays
 -------------|------------------|-------------------------------------
             -|  (fixed arrays)  |  Fixed-size arrays
              |                  |  Fixed arrays
 -------------|------------------|-------------------------------------
    xdr_opaque|  opaque          |  Constructed opaque data
              |                  |  Standard opaque data
 -------------|------------------|-------------------------------------
     xdr_union|  union           |  Constructed discriminated unions
              |                  |  Standard discriminated unions
 -------------|------------------|-------------------------------------
 xdr_reference|  -               |  Pointers
 -------------|------------------|-------------------------------------
             -|  struct          |  Hyper integer and hyper unsigned


The record marking standard

A record is composed of one or more record fragments. A record fragment is a four-byte header followed by 0 to 2^31 - 1 bytes of fragment data. The bytes encode an unsigned binary number; as with XDR integers, the byte order is from highest to lowest. The number encodes two values: a boolean that indicates whether the fragment is the last fragment of the record (bit value 1 implying the fragment is the last fragment), and a 31-bit unsigned binary value that is the length in bytes of the fragment's data. The boolean value is the high-order bit of the header; the length is the 31 low-order bits.


NOTE: This record specification is not in XDR standard form and cannot be implemented using XDR primitives.

Advanced topics -- linked lists

This section describes how to pass data structures by using linked lists of arbitrary lengths. Unlike the simpler examples covered in the earlier sections, the following examples are written using both the XDR C library routines and the XDR data description language. The section ``XDR standard'' describes the XDR data definition language used below.

The last version of xdr_gnumbers in the section ``Creating portable data with XDR'' presented a C data structure and its associated XDR routines for a person's gross assets and liabilities. The example is duplicated below:

   struct gnumbers {
           long g_assets;
           long g_liabilities;
   };
   

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

Now assume that you want to implement a linked list of such information. You could construct a data structure as follows:
   typedef struct gnnode {
           struct gnumbers gn_numbers;
           struct gnnode *nxt;
   };
   

typedef struct gnnode *gnumbers_list;

The head of the linked list can be thought of as the data object; that is, the head is not merely a convenient shorthand for a structure. Similarly, the nxt field is used to indicate whether or not the object has terminated. Unfortunately, if the object continues, the nxt field is also the address where it continues. The link addresses carry no useful information when the object is serialized.

The XDR data description of this linked list is described by the recursive type declaration of gnumbers_list:

   struct gnumbers {
           unsigned g_assets;
           unsigned g_liabilities;
   };
   

typedef union switch (boolean) { case TRUE: struct { struct gnumbers current_element; gnumbers_list rest_of_list; }; case FALSE: struct {}; } gnumbers_list;

In this description, the boolean indicates whether there is more data following it. If the boolean is FALSE, then it is the last data field of the structure. If it is TRUE, then it is followed by a gnumbers structure and (recursively) by a gnumbers_list (the rest of the object). Note that the C declaration has no boolean explicitly declared in it (though the nxt field implicitly carries the information), while the XDR data description has no pointer explicitly declared in it.

Hints for writing a set of XDR routines to successfully (de)serialize a linked list of entries can be found in the XDR description of the pointer-less data. The set consists of the mutually recursive routines xdr_gnumbers_list, xdr_wrap_list, and xdr_gnnode.

   bool_t
   xdr_gnnode(xdrs, gp)
           XDR *xdrs;
           struct gnnode *gp;
   {
           return (xdr_gnumbers(xdrs, &(gp->gn_numbers)) &&
                   xdr_gnumbers_list(xdrs, &(gp->nxt)) );
   }
   

bool_t xdr_wrap_list(xdrs, glp) XDR *xdrs; gnumbers_list *glp; { return (xdr_reference(xdrs, glp, sizeof(struct gnnode), xdr_gnnode)); }

struct xdr_discrim choices[2] = { /* called if another node needs (de)serializing */ { TRUE, xdr_wrap_list }, /* called when there are no more nodes to be (de)serialized */ { FALSE, xdr_void } }

bool_t xdr_gnumbers_list(xdrs, glp) XDR *xdrs; gnumbers_list *glp; { bool_t more_data;

more_data = (*glp != (gnumbers_list)NULL); return (xdr_union(xdrs, &more_data, glp, choices, NULL)); }

The entry routine is xdr_gnumbers_list(); its job is to translate between the boolean value more_data and the list pointer values. If there is no more data, the xdr_union() primitive calls xdr_void() and the recursion is terminated. Otherwise, xdr_union() calls xdr_wrap_list(), whose job is to dereference the list pointers. The xdr_gnnode() routine actually (de)serializes data of the current node of the linked list, and recursively calls xdr_gnumbers_list() to handle the remainder of the list. Readers should convince themselves that these routines function correctly in all three directions (XDR_ENCODE, XDR_DECODE and XDR_FREE) for linked lists of any length (including zero). Note that the boolean more_data is always initialized, but in the XDR_DECODE case it is overwritten by an externally generated value. Also note that the value of the bool_t is lost in the stack. The essence of the value is reflected in the list's pointers.

The unfortunate side effect of (de)serializing a list with these routines is that the C stack grows linearly with respect to the number of nodes in the list. This is due to the recursion. The routines are also hard to code (and understand) due to the number and nature of primitives involved (such as xdr_reference, xdr_union, and xdr_void).

The following routine collapses the recursive routines. It also has other optimizations that are discussed below.

   bool_t
   xdr_gnumbers_list(xdrs, glp)
           XDR *xdrs;
           gnumbers_list *glp;
   {
           bool_t more_data;
   

while (TRUE) { more_data = (*glp != (gnumbers_list)NULL); if (! xdr_bool(xdrs, &more_data)) return (FALSE); if (! more_data) return (TRUE); /* we are done */ if (! xdr_reference(xdrs, glp, sizeof(struct gnnode), xdr_gnumbers)) return (FALSE); glp = &((*glp)->nxt); } }

The claim is that this one routine is easier to code and understand than the three recursive routines above. The parameter glp is treated as the address of the pointer to the head of the remainder of the list to be (de)serialized. Thus, glp is set to the address of the current node's nxt field at the end of the while loop. The discriminated union is implemented in line; the variable more_data has the same use in this routine as in the routines above. Its value is recomputed and again (de)serialized in each iteration of the loop. Since *glp is a pointer to a node, the pointer is dereferenced using xdr_reference(). Note that the third parameter is truly the size of a node (data values plus nxt pointer), while xdr_gnumbers() only (de)serializes the data values. This optimization works only because the nxt data comes after all legitimate external data. There is a bug in this routine in the XDR_FREE case, in that xdr_reference() will free the node *glp. Upon return, the assignment glp = &((*glp)->nxt) cannot be guaranteed to work, since *glp is no longer a legitimate node. The following code works in all cases. The hard part is to avoid dereferencing a pointer that has not been initialized or that has been freed.
   bool_t
   xdr_gnumbers_list(xdrs, glp)
           XDR *xdrs;
           gnumbers_list *glp;
   {
           bool_t more_data;
           bool_t freeing;
           gnumbers_list *next;  /* the next value of glp */
   

freeing = (xdrs->x_op == XDR_FREE); while (TRUE) { more_data = (*glp != (gnumbers_list)NULL); if (! xdr_bool(xdrs, &more_data)) return (FALSE); if (! more_data) return (TRUE); /* we are done */ if (freeing) next = &((*glp)->nxt); if (! xdr_reference(xdrs, glp, sizeof(struct gnnode), xdr_gnumbers)) return (FALSE); glp = (freeing) ? next : &((*glp)->nxt); } }

This is the first example in this document that actually inspects the direction of the operation (xdrs->x_op). The claim is that the correct iterative implementation is still easier to understand or code than the recursive implementation. It is certainly more efficient with respect to C stack requirements.