Go to main content

ONC+ RPC Developer's Guide

Exit Print View

Updated: March 2019
 
 

rpcgen Tutorial

rpcgen provides programmers a direct way to write distributed applications. Server procedures can be written in any language that observes procedure-calling conventions. These procedures are linked with the server stub produced by rpcgen to form an executable server program. Client procedures are written and linked in the same way.

This section presents some basic rpcgen programming examples. Refer also to the rpcgen(1) man page.

Converting Local Procedures to Remote Procedures

Assume that an application runs on a single computer and you want to convert it to run in a "distributed" manner on a network. This example shows the stepwise conversion of this program that writes a message to the system console. The following code example shows the original program.

Example 1  Single Process Version of printmesg.c
/* printmsg.c: print a message on the console */

#include <stdio.h>
#include <stdlib.h>

int printmessage(char *);
 
int main(int argc, char *argv[])
{
	char *message;
 
	if (argc != 2) {
fprintf(stderr, "usage: %s <message>\n", argv[0]);
		exit(1);
	}
	message = argv[1];
	if (!printmessage(message)) {
		fprintf(stderr,"%s: couldn't print your message\n",argv[0]);
	exit(1);
	}
	printf("Message Delivered!\n");
	exit(0);
}
 /* Print a message to the console.
 * Return a boolean indicating whether
 * the message was actually printed. */
int printmessage(char *msg)
{
	FILE *f;
 	f = fopen("/dev/console", "w");
	if (f == (FILE *)NULL) {
		return (0);
	}
	fprintf(f, "%s\n", msg);
	fclose(f);
	return(1);}

For local use on a single system, this program could be compiled and executed as follows:

$ cc printmsg.c -o printmsg
$ printmsg "Hello, there."
Message delivered!
$

If the printmessage() function is turned into a remote procedure, the function can be called from anywhere in the network.

First, determine the data types of all procedure-calling arguments and the resulting argument. The calling argument of printmessage() is a string, and the result is an integer. You can write a protocol specification in the RPC language that describes the remote version of printmessage(). The RPC language source code for such a specification is:

/* msg.x: Remote msg printing protocol */
 program MESSAGEPROG {
     version PRINTMESSAGEVERS {
        int PRINTMESSAGE(string) = 1;
 	 } = 1;
} = 0x20000001;

Remote procedures are always declared as part of remote programs. The previous code declares an entire remote program that contains the single procedure PRINTMESSAGE. In this example, the PRINTMESSAGE procedure is declared to be procedure 1, in version 1 of the remote program MESSAGEPROG, with the program number 0x20000001. See RPC Protocol and Language Specification for guidance on choosing program numbers.

Version numbers are incremented when functionality is changed in the remote program. Existing procedures can be changed or new ones can be added. More than one version of a remote program can be defined and a version can have more than one procedure defined.

Note that the program and procedure names are declared with all capital letters.

Note also that the argument type is string and not char * as it would be in C. This is because a char * in C is ambiguous. char usually means an array of characters, but it could also represent a pointer to a single character. In the RPC language, a null-terminated array of char is called a string.

You have just two more programs to write:

  • The remote procedure itself

  • The main client program that calls it

Example 2, RPC Version of printmsg.c is a remote procedure that implements the PRINTMESSAGE procedure in Example 1, Single Process Version of printmesg.c.

Example 2  RPC Version of printmsg.c
/*
 * msg_proc.c: implementation of the
 * remote procedure "printmessage"
 */
#include <stdio.h>
#include "msg.h"				/* msg.h generated by rpcgen */
 
int *
printmessage_1_svc(char **msg, struct svc_req *req)

{
	static int result;			/* must be static! */
	FILE *f;
 
	f = fopen("/dev/console", "w");
	if (f == (FILE *)NULL) {
		result = 0;
		return (&result);
	}
	fprintf(f, "%s\n", *msg);
	fclose(f);
	result = 1;
	return (&result);}

The declaration of the remote procedure printmessage_1_svc() differs from that of the local procedure printmessage() in four ways:

  1. printmessage_1_svc() takes a pointer to the character array instead of the pointer itself. This principle is true of all remote procedures when the –N option is not used. These procedures always take pointers to their arguments rather than the arguments themselves. Without the –N option, remote procedures are always called with a single argument. If more than one argument is required the arguments must be passed in a struct.

  2. printmessage_1_svc() is called with two arguments. The second argument contains information about the context of an invocation: the program, version, and procedure numbers; raw and canonical credentials; and an SVCXPRT structure pointer. The SVCXPRT structure contains transport information. All of the information is made available in case the invoked procedure requires it to perform the request.

  3. printmessage_1_svc() returns a pointer to an integer instead of the integer itself. This principle is also true of remote procedures when the –N option is not used. These procedures return pointers to the result. The result should be declared static unless the –M (multithread) or –A (Auto mode) options are used. Ordinarily, if the result is declared local to the remote procedure, references to the result by the server stub are invalid after the remote procedure returns. In the case of –M and –A options, a pointer to the result is passed as a third argument to the procedure, so the result is not declared in the procedure.

  4. An _1 is appended to the printmessage_1() name. In general, all remote procedures calls generated by rpcgen are named as follows: the procedure name in the program definition (here PRINTMESSAGE) is converted to all lowercase letters, an underbar (_) is appended to it, and the version number (here 1) is appended. This naming scheme enables you to have multiple versions of the same procedure.

The following code example shows the main client program that calls the remote procedure.

Example 3  Client Program to Call printmsg.c
/*
 * rprintmsg.c: remote version
 * of "printmsg.c"
 */
#include <stdio.h>
#include <stdlib.h>
#include "msg.h"			/* msg.h generated by rpcgen */
 
int main(int argc, char *argv[])

{
	CLIENT *clnt;
	int *result;
	char *server;
	char *message;
 
	if (argc != 3) {
		fprintf(stderr, "usage: %s host message\n", argv[0]);
		exit(1);
	}
 
	server = argv[1];
	message = argv[2];
	/*
	 * Create client "handle" used for 
 * calling MESSAGEPROG on the server
	 * designated on the command line.
	 */
	clnt = clnt_create(server, MESSAGEPROG,
								PRINTMESSAGEVERS,
								"tcp");
	if (clnt == (CLIENT *)NULL) {
		/*
		 * Couldn't establish connection
    * with server.
		 * Print error message and die.
		 */
		clnt_pcreateerror(server);
		exit(1);
	}
		/*
	 * Call the remote procedure
 * "printmessage" on the server
	 */
	result = printmessage_1(&message, clnt);
	if (result == (int *)NULL) {
		/*
		 * An error occurred while calling 
    * the server.
		 * Print error message and die.
		 */
		clnt_perror(clnt, server);
		exit(1);
	}
	/* Okay, we successfully called 
 * the remote procedure. 
 */
	if (*result == 0) {
		/*
		 * Server was unable to print 
    * our message.
		 * Print error message and die.
		 */
		fprintf(stderr,
		"%s: could not print your message\n",argv[0]);
		exit(1);
	}
 	/* The message got printed on the
 * server's console
 */
	printf("Message delivered to %s\n",
				server);
	clnt_destroy( clnt );
	exit(0);}

In the example code, a client handle is created by the RPC library routine clnt_create(). This client handle is passed to the stub routine that calls the remote procedure. See Programmer's Interface to RPC for details on how the client handle can be created in other ways. If no more calls are to be made using the client handle, destroy it with a call to clnt_destroy() to conserve system resources.

For further information about transports, see the /etc/netconfig file and its description in Oracle Solaris 11.3 Programming Interfaces Guide.

The remote procedure also returns a pointer to the result instead of the result.

The remote procedure call can fail in two ways. The RPC mechanism can fail or an error can occur in the execution of the remote procedure. In the former case, the remote procedure printmessage_1() returns a NULL. In the latter case, the error reporting is application dependent. Here, the error is returned through *result.

The compile commands for the printmsg example are:

$ rpcgen msg.x
$ cc rprintmsg.c msg_clnt.c -o rprintmsg -lnsl
$ cc msg_proc.c msg_svc.c -o msg_server -lnsl

rpcgen is used to generate the header files (msg.h), client stub (msg_clnt.c), and server stub (msg_svc.c). Then, two programs are compiled: the client program rprintmsg and the server program msg_server. The C object files must be linked with the library libnsl, which contains all of the networking functions, including those for RPC and XDR.

For more information about -C and other options, see the rpcgen(1) man page.

In this example, no XDR routines were generated because the application uses only the basic types that are included in libnsl.

rpcgen received the input file msg.x and created:

  • A header file called msg.h that contained #define statements for MESSAGEPROG, MESSAGEVERS, and PRINTMESSAGE for use in the other modules. This file must be included by both the client and server modules.

  • The client stub routines in the msg_clnt.c file. Only one routine, the printmessage_1() routine, was called from the rprintmsg client program. If the name of an rpcgen input file is FOO.x, the client stub's output file is called FOO_clnt.c.

  • The server program in msg_svc.c that calls printmessage_1_svc() from msg_proc.c. The rule for naming the server output file is similar to that of the client: for an input file called FOO.x, the output server file is named FOO_svc.c.

After the server program is created, it is installed on a remote system and run. If the systems are homogeneous, the server binary can just be copied. If they are not homogeneous, the server source files must be copied to and compiled on the remote system. For this example, the remote system is called remote and the local system is called local. The server is started from the shell on the remote system:

remote$ msg_server

Server processes generated with rpcgen always run in the background. You do not have to follow the server's invocation with an ampersand (&). Servers generated by rpcgen can also be invoked by port monitors like listen() and inetd(), instead of from the command line.

Thereafter, a user on local can print a message on the console of system remote as follows:

local$ rprintmsg remote "Hello, there."

Using rprintmsg, a user can print a message on any system console, including the local console, when the server msg_server is running on the target system.

Passing Complex Data Structures

Converting Local Procedures to Remote Procedures shows how to generate client and server RPC code. rpcgen can also be used to generate XDR routines, which are the routines that convert local data structures into XDR format and the reverse.

The following code example presents a complete RPC service: a remote directory listing service, built using rpcgen to generate both stub routines and the XDR routines.

Example 4  RPC Protocol Description File: dir.x
/*
 * dir.x: Remote directory listing protocol
 *
 * This example demonstrates the functions of rpcgen.
 */

const MAXNAMELEN = 255;                 /* max length of directory entry */
typedef string nametype<MAXNAMELEN>; /* director entry */
typedef struct namenode *namelist;      /* link in the listing */

/* A node in the directory listing */
struct namenode {
    nametype name; /* name of directory entry */
    namelist next; /* next entry */
};
/*
 * The result of a READDIR operation
 *
 * A truly portable application would use an agreed upon list of
 * error codes rather than (as this sample program does) rely upon
 * passing UNIX errno's back.
 *
 * In this example: The union is used here to discriminate between
 * successful and unsuccessful remote calls.
 */
union readdir_res switch (int err) {
    case 0:
        namelist list; /* no error: return directory listing */
    default:
        void;          /* error occurred: nothing else to return */
};
/* The directory program definition */
program DIRPROG {
    version DIRVERS {
        readdir_res
        READDIR(nametype) = 1;
    } = 1;
} = 0x20000076;

You can redefine types (like readdir_res in the previous example) using the struct, union, and enum RPC language keywords. These keywords are not used in later declarations of variables of those types. For example, if you define a union, foo, you declare using only foo, and not union foo.

rpcgen compiles RPC unions into C structures. Do not declare C unions using the union keyword.

    Running rpcgen on dir.x generates four output files:

  • Header file

  • Client stub

  • Server skeleton

  • XDR routines in the file dir_xdr.c.

The dir_xdr.c file contains the XDR routines to convert declared data types from the host platform representation into XDR format, and the reverse.

For each RPC data type used in the.x file, rpcgen assumes that libnsl contains a routine with a name that is the name of the data type, prepended by the XDR routine header xdr_ (for example, xdr_int). If a data type is defined in the.x file, rpcgen generates the required xdr_ routine. If there is no data type definition in the .x source file (for example, msg.x), then no _xdr.c file is generated.

You can write a .x source file that uses a data type not supported by libnsl, and deliberately omit defining the type in the.x file. In doing so, you must provide the xdr_ routine. This is a way to provide your own customized xdr_ routines. See Programmer's Interface to RPC for more details on passing arbitrary data types. The server-side of the READDIR procedure is shown in the following example.

Example 5  Server dir_proc.c Example
/*
 * dir_proc.c: remote readdir
 * implementation
 */

#include <errno.h>
#include <dirent.h>
#include <malloc.h>
#include <strings.h>
#include "dir.h"            /* Created by rpcgen */


readdir_res *
readdir_1_svc(nametype *dirname, struct svc_req *req)
{
    DIR *dirp;
    struct dirent *d;
    namelist nl;
    namelist *nlp;
    static readdir_res res; /* must be static! */

    /* Open directory */
    dirp = opendir(*dirname);
    if (dirp == (DIR *)NULL) {
        res.err = errno;
        return (&res);
    }
    /* Free previous result */
    xdr_free(xdr_readdir_res, (char *)&res);
    /*
     * Collect directory entries.
     * Memory allocated here is free by xdr_free the next time
     * readdir_1 is called
     */
    nlp = &res.readdir_res_u.list;
    while (d = readdir(dirp)) {
        nl = *nlp = (namenode *)
        malloc(sizeof(namenode));
        if (nl == (namenode *) NULL) {
            res.err = EAGAIN;
            closedir(dirp);
            return(&res);
        }
        nl->name = strdup(d->d_name);
        nlp = &nl->next;
    }
    *nlp = (namelist)NULL;
    /* Return the result */
    res.err = 0;
    closedir(dirp);
    return (&res);
}

The following code example shows the client-side implementation of the READDIR procedure.

Example 6  Client-side Implementation of rls.c
/*
 * rls.c: Remote directory listing client
 */
 
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include "dir.h"                       /* generated by rpcgen */
 
 
int main(int argc, char *argv[])
{
	CLIENT *clnt;
	char *server;
	char *dir;
	readdir_res *result;
	namelist nl;
 	if (argc != 3) {
		fprintf(stderr, "usage: %s host directory\n",argv[0]);
		exit(1);
	}
	server = argv[1];
	dir = argv[2];
	/*
	 * Create client "handle" used for
 * calling MESSAGEPROG on the server
 * designated on the command line.
	 */
	clnt = clnt_create(server, DIRPROG, DIRVERS, "tcp");
	if (clnt == (CLIENT *)NULL) {
		clnt_pcreateerror(server);
		exit(1);
	}
	result = readdir_1(&dir, clnt);
	if (result == (readdir_res *)NULL) {
		clnt_perror(clnt, server);
		exit(1);
	}
	/* Okay, we successfully called
 * the remote procedure.
 */
	if (result->err != 0) {
		/* Remote system error. Print
    * error message and die.
    */
		errno = result->err;
		perror(dir);
		exit(1);
	}
	/* Successfully got a directory listing.
 * Print it.
 */
	for (nl = result->readdir_res_u.list;
		nl != NULL;
		nl = nl->next) {
		printf("%s\n", nl->name);
	}
	xdr_free(xdr_readdir_res, (char *)result);
	clnt_destroy(clnt);
	exit(0);}

As in other examples, execution is on systems named local and remote. The files are compiled and run as follows:

 remote$ rpcgen -C dir.x
 remote$ cc -c dir_xdr.c
 remote$ cc rls.c dir_clnt.c dir_xdr.o -o rls -lnsl
 remote$ cc dir_svc.c dir_proc.c dir_xdr.o -o dir_svc -lnsl
 remote$ dir_svc

When you install rls() on system local, you can list the contents of /usr/share/lib on system remote as follows:

 local$ rls remote /usr/share/lib
 ascii
 eqnchar
 greek
 kbd
 marg8
 tabclr
 tabs
 tabs4
 local
$

Client code generated by rpcgen does not release the memory allocated for the results of the RPC call. Call xdr_free() to release the memory when you are finished with it. Calling xdr_free() is similar to calling the free() routine, except that you pass the XDR routine for the result. In this example, after printing the list, xdr_free(xdr_readdir_res, result); was called.


Note - Use xdr_free() to release memory allocated by malloc(). Failure to use xdr_free() to release memory results in memory leaks.

Preprocessing Directives

rpcgen supports C and other preprocessing features. C preprocessing is performed on rpcgen input files before they are compiled. All standard C preprocessing directives are allowed in the.x source files. Depending on the type of output file being generated, five symbols are defined by rpcgen.

rpcgen provides an additional preprocessing feature: any line that begins with a percent sign (%) is passed directly to the output file, with no action on the line's content. Use caution because rpcgen does not always place the lines where you intend. Check the output source file and, if needed, edit it.

rpcgen uses the preprocessing directives listed in the following table.

Table 7  rpcgen Preprocessing Directives
Symbol
Use
RPC_HDR
Header file output
RPC_XDR
XDR routine output
RPC_SVC
Server stub output
RPC_CLNT
Client stub output
RPC_TBL
Index table output

The following code example is a simple rpcgen example. Note the use of rpcgen`s pre-processing features.

Example 7  Time Protocol rpcgen Source
/*
 * time.x: Remote time protocol
 */
program TIMEPROG {
	version TIMEVERS {
		unsigned int TIMEGET() = 1;
	} = 1;
} = 0x20000044;

#ifdef RPC_SVC
%int *
%timeget_1()
%{
%	static int thetime;
%
%	thetime = time(0);
%	return (&thetime);
%}
#endif

cpp Directive

rpcgen supports C preprocessing features. rpcgen defaults to use /usr/ccs/lib/cpp as the C preprocessor. If that fails, rpcgen tries to use /lib/cpp. You can specify a library containing a different cpp to rpcgen with the –Y flag.

For example, if /usr/local/bin/cpp exists, you can specify it to rpcgen as follows:

$ rpcgen -Y /usr/local/bin test.x