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.
At this layer, the routines registerrpc and callrpc are used to make RPC calls: registerrpc obtains a unique system-wide number, while callrpc executes a remote procedure call. The rnusers call is implemented using these two routines.
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.
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>The rnusers routine is not included with this release of the software, but is shown as an example only.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 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>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.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); }
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:
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:
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
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>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.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); }
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 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.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); }
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);
}
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:
A number of assumptions are built into registerrpc:
#include <stdio.h> #include <rpc/rpc.h> #include <rpcsvc/rusers.h>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.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; } }
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:
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.
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];where chararr has already allocated space.svc_getargs(transp, xdr_chararr1, chararr);
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;After using the character array, it can be freed with svc_freeargs. In the routine xdr_finalexample given earlier, ifarrptr = NULL; svc_getargs(transp, xdr_chararr2, &arrptr); /* * use the result here */ svc_freeargs(xdrsp, xdr_chararr2, &arrptr);
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.
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 parameters to clntudp_create are as follows:
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.
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.
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>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.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 */
bool_t done;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_perrnodone = eachresult(resultsp, raddr); caddr_t resultsp; struct sockaddr_in *raddr; /* address of machine that sent response*/
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 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:
#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.
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.
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 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:
rq_cred is well formed.
Thus the service implementor may inspect the request's
rq_cred.oa_flavor
to determine which style of authentication the caller used.
The service implementor may also wish to inspect the other fields of
rq_cred
if the style is not one of the styles supported by the RPC package.
rq_clntcred field is either NULL
or points to a well-formed structure
that corresponds to a supported style of authentication credentials.
In SCO NFS only one type of authentication is supported,
so rq_clntcred should normally be cast to a pointer to an
authunix_parms structure.
If rq_clntcred is NULL,
the service implementor may wish to inspect
the other (opaque) fields of rq_cred,
in case the service knows about a new type of authentication
about which the RPC package is unaware.
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:
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;
}
}
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:
*
* 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
*/
#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);
}
#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; } }
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>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.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); }
/*
* 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
*/
#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");
}
}
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.
#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); } } }
#include <stdio.h>The two programs appear to be portable for the following reasons: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"); }
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 2This 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().
#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); } } }
#include <stdio.h> #include <rpc/rpc.h> /* xdr is a sub-library of the rpc library */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: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"); }
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.
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.
The xdr_long() primitive is characteristic of most XDR library primitives and all client XDR routines:
xdr_xxx(xdrs, fp)
XDR *xdrs;
xxx *fp;
{
}
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 0Using these conventions, xdr_gnumbers() can be rewritten as follows:#define enum_t int /* enum_t's are used for generic enum's */
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.
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.
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.
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 1The second parameters, ep and bp, are addresses of the associated type that provides data to, or receives data from, the stream xdrs.#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 routines return TRUE if they complete successfully, and FALSE otherwise.
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 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.
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 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.
Using variable-length arrays of bytes are often preferable to strings. Byte arrays differ from strings in the following ways:
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.
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.
Using the data types presented so far, you can define a single user, define multiple users, and create a command history.
A user on a networked machine can be identified by the following:
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));
}
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));
}
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().
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.
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); }
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.
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:
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.
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.
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);
}
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.
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.
The routine xdr_setpos() sets a stream position to pos.
The xdr_destroy() primitive destroys the
XDR stream.
Use of the stream after calling this routine is undefined.
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.
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*/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.void xdrstdio_create(xdrs, fp, x_op) XDR *xdrs; FILE *fp; enum xdr_op x_op;
Memory streams allow the streaming of data into or out of a specified area of memory:
#include <rpc/rpc.h>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.void xdrmem_create(xdrs, addr, len, x_op) XDR *xdrs; char *addr; uint len; enum xdr_op x_op;
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 */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.xdrrec_create(xdrs, sendsize, recvsize, iohandle, readproc, writeproc) XDR *xdrs; uint sendsize, recvsize; char *iohandle; int (*readproc)(), (*writeproc)();
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 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.
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.
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.
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.
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 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 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;
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.
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:
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.
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.
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.
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 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.
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.
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.
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.
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
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.
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.