Sun Microsystems, Inc.
spacerspacer
spacer www.sun.com docs.sun.com |
spacer
black dot
 
 
A.  XDR Technical Note What Is XDR?  Previous   Contents   Next 
   
 

Canonical Standard

The XDR approach to standardizing data representations is canonical. That is, XDR defines a single byte order, a single floating-point representation (IEEE), and so on. Any program running on any machine can use XDR to create portable data by translating its local representation to the XDR standard representations. Similarly, any program running on any machine can read portable data by translating the XDR standard representations to its local equivalents.

The single standard completely decouples programs that create or send portable data from those that use or receive portable data. A new machine learns how to convert the standard representations and its local representations. The local representations of other machines are irrelevant. Conversely, the local representations of the new machine are also irrelevant to existing programs running on other machines. Such programs can immediately read portable data produced by the new machine because such data conforms to the canonical standards that they already understand.

Strong precedents are in place for XDR's canonical approach. For example, TCP/IP, UDP/IP, XNS, Ethernet, and, indeed, all protocols below layer five of the ISO model, are canonical protocols. The advantage of any canonical approach is simplicity. In the case of XDR, a single set of conversion routines is written once and is never touched again.

The canonical approach has the disadvantage of unnecessary conversion to and from the XDR standard when data is transferred between two machines with identical byte order. Suppose two Intel machines are transferring integers according to the XDR standard. The sending machine converts the integers from Intel host byte order to XDR byte order. The receiving machine performs the reverse conversion. Because both machines observe the same byte order, their conversions are unnecessary.

The time spent converting to and from a canonical representation is insignificant, especially in distributed applications. Most of the time required to prepare a data structure for transfer is not spent in conversion but in traversing the elements of the data structure.

To transmit a tree, for example, each leaf must be visited and each element in a leaf record must be copied to a buffer and aligned there. Storage for the leaf might have to be de-allocated as well. Similarly, to receive a tree, you must allocate storage for each leaf, move data from the buffer to the leaf and properly align it, and construct pointers to link the leaves together.

Every machine pays the cost of traversing and copying data structures whether or not conversion is required. In distributed applications, communications overhead, which is the time required to move the data down the sender's protocol layers, across the network, and up the receiver's protocol layers, dwarfs conversion overhead.

XDR Library

The XDR library solves data portability problems, and also enables you to write and read arbitrary C constructs in a consistent, specified, well-documented manner. Thus, using the library can be helpful even when the data is not shared among machines on a network.

The XDR library has filter routines for 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 might contain arrays of arbitrary elements, or pointers to other structures.

Look closely at Example A-3 and Example A-4. A family of XDR stream-creation routines has each member treat the stream of bits differently. In the example, data is manipulated using standard I/O routines, so you use xdrstdio_create(). 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 might be XDR_ENCODE for serializing in the writer program, or XDR_DECODE for deserializing in the reader program.


Note - RPC users never need to create XDR streams. The RPC system itself creates these streams, which are then passed to the users.


The xdr_int() primitive is characteristic of most XDR library primitives and all client XDR routines. First, the routine returns FALSE (0) if it fails, and TRUE (1) if it succeeds. Second, for each data type, xxx, there is an associated XDR routine of the form:

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

In this case, xxx is int, and the corresponding XDR routine is a primitive, xdr_int(). 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.

An XDR routine is direction independent. That is, the same routine is called to serialize or deserialize data. This feature is critical to software engineering of portable data. The idea is to call the same routine for either operation. This practice almost guarantees that serialized data can also be deserialized.

This one routine is used by both producer and consumer of networked data. This concept 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 user can obtain the direction of the XDR operation. For details, see the section"Operation Directions".

A slightly more complicated example follows. 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 {
   int g_assets;
   int g_liabilities;
};

The corresponding XDR routine describing this structure is as follows.

bool_t                      /* TRUE is success, FALSE is failure */
xdr_gnumbers(xdrs, gp)
   XDR *xdrs;
   struct gnumbers *gp;
{
   if (xdr_int(xdrs, &gp->g_assets) &&
         xdr_int(xdrs, &gp->g_liabilities))
      return(TRUE);
   return(FALSE);
}

Note that the parameter xdrs is never inspected or modified. It is only passed on to the subcomponent routines. The routine must inspect the return value of each XDR routine call, and to halt immediately and return FALSE if the subroutine fails.

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

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

By observing these conventions, you can rewrite xdr_gnumbers() as follows:

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

This document uses both coding styles.

XDR Library Primitives

This section provides a synopsis of each XDR primitive. It starts with memory allocation and the basic data types, then 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>, automatically included by <rpc/rpc.h>.

Memory Requirements for XDR Routines

When using XDR routines, you sometimes need to to pre-allocate memory, or to determine memory requirements. When you need to control the allocation and de-allocation of memory for XDR conversion routines, you can use a routine, xdr_sizeof(). This routine returns the number of bytes needed to encode and decode data with one of the XDR filter functions (func()). The output of the xdr_sizeof() function does not include RPC headers or record markers. You must add them to get a complete accounting of the memory required. xdr_sizeof() returns a zero on error.

xdr_sizeof(xdrproc_t func, void *data)

Use xdr_sizeof() for the allocation of memory in applications that use XDR outside of the RPC environment, to select between transport protocols, and at the lower levels of RPC to perform client and server creation functions.

The next two code examples illustrate two uses of xdr_sizeof().


Example A-5 xdr_sizeof Example #1

#include <rpc/rpc.h>
 
/*
 *	This function takes as input a CLIENT handle, an XDR function
and
 *	a pointer to data to be XDR'd. It returns TRUE if the amount of
 *	data to be XDR'd may be sent using the transport associated
with
 *	the CLIENT handle, and false otherwise.
 */
bool_t
cansend(cl, xdrfunc, xdrdata)
	CLIENT *cl;
	xdrproc_t xdrfunc;
	void   *xdrdata;
{
	int fd;
	struct t_info tinfo;
 
	if (clnt_control(cl, CLGET_FD, &fd) == -1) {
		/* handle clnt_control() error */
		return (FALSE);
	}
 
	if (t_getinfo(fd, &tinfo) == -1) {
		/* handle t_getinfo() error */
		return (FALSE);
	} else {
		if (tinfo.servtype == T_CLTS) {
			/*
			 * This is a connectionless transport. Use xdr_sizeof()
			 * to compute the size of this request to see whether it
			 * is too large for this transport.
			 */
			switch(tinfo.tsdu) {
				case 0:                      /* no concept of TSDUs */
				case -2:                       /* can't send normal data */
					return (FALSE);
					break;
				case -1:                        /* no limit on TSDU size */
					return (TRUE);
					break;
				default:
					if (tinfo.tsdu < xdr_sizeof(xdrfunc, xdrdata))
						return (FALSE);
					else
						return (TRUE);
			}
		} else
			return (TRUE);
	}
}

 
 
 
  Previous   Contents   Next