Digital TCP/IP Services for OpenVMS
ONC RPC Programming


Previous | Contents

If the printmessage procedure at the bottom of the printmsg.c program of Example 2-1 were converted into a remote procedure, you could call it from anywhere in the network, instead of only from the program where it is embedded. Before doing this, it is necessary to write a protocol specification in RPC language that describes the remote procedure, as shown in the next section.

Example 2-1 Printing a Remote Message Without ONC RPC


/* 
** printmsg.c: OpenVMS print a message on the console 
*/ 
#include <descrip.h> 
#include <opcdef.h> 
#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
 
extern int SYS$SNDOPR(struct dsc$descriptor_s *, unsigned short); 
 
static int printmessage(char *); 
 
main(argc, argv) 
    int   argc; 
    char *argv[]; 
{ 
    char *message; 
    int exit(); 
 
    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. 
*/ 
static int 
printmessage(msg) 
    char *msg; 
{ 
    struct dsc$descriptor_s desc; 
    union { 
        char  buffer[256]; /* Preallocate space for text */ 
        struct opcdef opc; 
        } message; 
    int status; 
 
    /* 
    ** Build the message request block. 
    */ 
    message.opc.opc$b_ms_type   = OPC$_RQ_RQST; 
    message.opc.opc$b_ms_target = OPC$M_NM_CENTRL; 
    message.opc.opc$w_ms_status = 0; 
    message.opc.opc$l_ms_rqstid = 0; 
    strcpy((char *) &message.opc.opc$l_ms_text, msg); 
    desc.dsc$a_pointer = (char *) &message.opc; 
    desc.dsc$w_length  = (char *) &message.opc.opc$l_ms_text - 
                         (char *) &message + 
                         strlen((char *) &message.opc.opc$l_ms_text); 
    /* 
    ** Send the message to the console. 
    */ 
    status = SYS$SNDOPR(&desc,        /* MSGBUF */ 
                         0);          /* CHAN */ 
    if (status & 1) 
        return 1; 
    return 0; 
    } 

2.2.1 RPC Protocol Specification File Describing Remote Procedure

To create the specification file, you must know all the input and output parameter types. In Example 2-1, the printmessage procedure takes a string as input, and returns an integer as output. Example 2-2 (see SYS$EXAMPLES:[UCX.RPC]MSG.X) is the RPC protocol specification file that describes the remote version of the printmessage procedure.

Remote procedures are part of remote programs, so Example 2-2 actually declares a remote program containing a single procedure, PRINTMESSAGE. By convention, all RPC services provide for a NULL procedure (procedure 0), normally used for "pinging." (See the Digital TCP/IP Services for OpenVMS Management manual.) The RPC protocol specification file in Example 2-2 declares the PRINTMESSAGE procedure to be in version 1 of the remote program. No NULL procedure (procedure 0) is necessary in the protocol definition because RPCGEN generates it automatically.

In RPC language, the convention (though not a requirement) is to make all declarations in uppercase characters. Notice that the argument type is string, not char*, because a char* in C is ambiguous. Programmers usually intend it to mean a null-terminated string of characters, but it could also be a pointer to a single character or to an array of characters. In RPC language, a null-terminated string is unambiguously of type string.

Example 2-2 RPC Protocol Specification File Simple Example


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

2.2.2 Implementing the Procedure Declared in the Protocol Specification

Example 2-3 (see SYS$EXAMPLES:[UCX.RPC]MSG_SERVER.C) defines the remote procedure declared in the RPC protocol specification file of the previous example.

In this example, the declaration of the remote procedure, printmessage_1, differs from that of the local procedure printmessage in four ways:

  1. (1) It includes the <rpc/rpc.h> file and the "msg.h" header files. The rpc/rpc.h file is located in the directory UCX$RPC:. To ensure portability in header files references, most of the examples in this manual assume you have defined the symbol RPC to be equal to UCX$RPC:
    $ DEFINE RPC UCX$RPC: 
    

    before using the RPCGEN compiler and the DECC compiler.
  2. (2) It has _1 appended to its name. In general, all remote procedures called by RPCGEN skeleton routines are named by the following rule: The name in the procedure definition (here, PRINTMESSAGE) is converted to all lowercase letters, and an underscore (_) and version number (here, 1) is appended to it.
  3. (3) It takes a pointer to a string instead of a string itself. This is true of all remote procedures -- they always take pointers to their arguments rather than the arguments themselves; if there are no arguments, specify void.
  4. (4) It returns a pointer to an integer instead of an integer itself. This is also characteristic of remote procedures---they return pointers to their results. Therefore, it is important to have the result declared as a static; if there are no arguments, specify void.

Example 2-3 Remote Procedure Definition


 
/* 
** msg_server.c: OpenVMS implementation of the remote procedure 
** "printmessage" 
*/ 
 
#include <descrip.h>  /* OpenVMS descriptor definitions */ 
#include <opcdef.h>   /* OpenVMS $SNDOPR() definitions */ 
#include <rpc/rpc.h>  /* always needed */ (1)
#include "msg.h"      /* msg.h will be generated by RPCGEN */ 
 
extern int SYS$SNDOPR(struct dsc$descriptor_s *, unsigned short); 
 
/* 
** Remote version of "printmessage" 
*/ 
int * 
printmessage_1(msg) (2)
    char **msg; (3)
{ 
    struct dsc$descriptor_s desc; 
    union { 
        char  buffer[256]; /* Preallocate space for text */ 
        struct opcdef opc; 
        } message; 
    static int result; 
    int status; 
 
    /* 
    ** Build the message request block. 
    */ 
    message.opc.opc$b_ms_type   = OPC$_RQ_RQST; 
    message.opc.opc$b_ms_target = OPC$M_NM_CENTRL; 
    message.opc.opc$w_ms_status = 0; 
    message.opc.opc$l_ms_rqstid = 0; 
    strcpy((char *) &message.opc.opc$l_ms_text, *msg); 
    desc.dsc$a_pointer = (char *) &message.opc; 
    desc.dsc$w_length  = (char *) &message.opc.opc$l_ms_text - 
                         (char *) &message + 
                         strlen((char *) &message.opc.opc$l_ms_text); 
    status = SYS$SNDOPR(&desc,        /* MSGBUF */ 
                         0);          /* CHAN */ 
    if (status & 1) 
 result = 1; 
    else 
        result = 0; 
    return &result; (4)
    } 
 

2.2.3 The Client Program That Calls the Remote Procedure

Example 2-4 (see SYS$EXAMPLES:[UCX.RPC]RPRINTMSG.C) declares the main client program, rprintmsg.c, that calls the remote procedure.

In this example, the following events occur:

  1. (1) First, the RPC library routine clnt_create creates a client "handle." The last parameter to clnt_create is "tcp", the transport on which you want to run your application. (Alternatively, you could have used "udp".)
  2. (2) Next, the program calls the remote procedure printmessage_1 in exactly the same way as specified in msg_server.c, except for the inserted client handle as the second argument.
  3. (3) The remote procedure call can fail in two ways: The RPC mechanism itself can fail or there can be an error in the execution of the remote procedure. In the former case, the remote procedure, printmessage_1, returns NULL.
  4. (4) In the later case, error reporting is application-dependent. In this example, the remote procedure reports any error via *result.

Example 2-4 Client Program that Calls the Remote Procedure


 
/* 
** rprintmsg.c: remote OpenVMS version of "printmsg.c" 
*/ 
 
#include <stdio.h> 
#include <rpc/rpc.h>     /* always needed  */ 
#include "msg.h"         /* msg.h will be generated by RPCGEN */ 
 
main(argc, argv) 
    int   argc; 
    char *argv[]; 
{ 
    CLIENT *cl; 
    char   *message; 
    int    *result; 
    char   *server; 
 
    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.  We tell 
    ** the RPC package to use the TCP protocol when 
    ** contacting the server. 
    */ 
    cl = clnt_create(server, MESSAGEPROG, MESSAGEVERS, "tcp"); (1)
    if (cl == NULL) { 
        /* 
        ** Couldn't establish connection with server. 
        ** Print error message and stop. 
        */ 
        clnt_pcreateerror(server); 
        exit(1); 
        } 
 
    /* 
    ** Call the remote procedure "printmessage" on the server 
    */ 
    result = printmessage_1(&message, cl); (2)
 
    if (result == NULL) { (3)
        /* 
        ** An error occurred while calling the server. 
        ** Print error message and stop. 
        */ 
        clnt_perror(cl, server); 
        exit(1); 
        } 
 
    /* 
    ** Okay, we successfully called the remote procedure. 
    */ 
    if (*result == 0) { (4)
        /* 
        ** Server was unable to print our message. 
        ** Print error message and stop. 
        */ 
        fprintf(stderr, "%s: %s couldn't print your message\n", argv[0], server); 
        exit(1); 
        } 
 
    /* 
    ** The message got printed on the server's console 
    */ 
    printf("Message delivered to %s!\n", server); 
    exit(0); 
    } 
 

2.2.4 Running RPCGEN

Use the RPCGEN protocol compiler on the RPC protocol specification file, MSG.X, (from Example 2-2) to generate client and server RPC code automatically:
$ RPCGEN MSG.X 

Using RPCGEN like this---without options---automatically creates the following files from the input file MSG.X:


Note

The /TABLE option of RPCGEN creates an additional output file of index information for dispatching service routines. See Section 2.6.4 for more information about dispatch tables.

2.2.5 Compiling the Client and Server Programs

After the RPCGEN protocol compilation, use two cc compilation statements to create a client program and a server program:


Note

If you want to use the shareable version of the RPC object library, reference the shareable version of the library, SYS$SHARE:UCX$RPCXDR_SHR/SHARE, in your LINK options file.

2.2.6 Copying the Server to a Remote System and Running It

Copy the server program msg_server to a remote system called space in this example. Then, run it in as a detached process there:

$ RUN/DETACHED MSG_SERVER 

Note

You can invoke servers generated by RPCGEN from the command line as well as with port monitors such as INETd, if you generate them with the /INET_SERVICE option.

From a local system (earth) you can now print a message on the console of the remote system space:

$ MCR SYS$DISK:[]RPRINTMSG "space" "Hello out there..." 

The message Hello out there... appears on the console of the system space. You can print a message on any console (including your own) with this program if you copy the server to that system and run it.

2.3 Advanced Example: Using RPCGEN to Generate XDR Routines

Section 2.2 explained how to use RPCGEN to generate client and server RPC code automatically to convert a simple procedure to one that runs remotely over the network. The RPCGEN protocol compiler can also generate the external data representation (XDR) routines that convert local data structures into network format (and vice versa).

The following sections present a more advanced example of a complete RPC service---a remote directory listing service that uses RPCGEN to generate both the client and server skeletons as well as XDR routines.

2.3.1 The RPC Protocol Specification

As with the simple example, you must first create an RPC protocol specification file. This file, DIR.X, is shown in Example 2-5 (see SYS$EXAMPLES:[UCX.RPC]DIR.X).


Note

You can define types (such as readdir_res in Example 2-5) by using the struct, union, and enum keywords, but do not use these keywords in later variable declarations of those types. For example, if you define union results, you must declare it later by using results, not union results. The RPCGEN protocol compiler compiles RPC unions into C structures, so it is an error to declare them later by using the union keyword.

Running RPCGEN on DIR.X creates four output files:

The first three files have already been described. The fourth file, DIR_XDR.C, contains the XDR routines that convert the declared data types into XDR format (and vice versa). For each data type present in the .X file, RPCGEN assumes that the RPC/XDR library contains a routine with the name of that data type prefixed by xdr_, for example, xdr_int. If the .X file defines the data type, then RPCGEN generates the required XDR routines (for example, DIR_XDR.C). If the .X file contains no such data types, then RPCGEN does not generate the file. If the program uses a data type but does not define it, then you must provide that XDR routine. This enables you to create your own customized XDR routines.

Example 2-5 RPC Protocol Specification File---Advanced Example


/* 
 * dir.x: Remote directory listing protocol 
 */ 
 
/* maximum length of a directory entry */ 
const MAXNAMELEN = 255; 
/* a directory entry */ 
typedef string nametype<MAXNAMELEN>; 
 
/* a link in the listing */ 
typedef struct namenode *namelist; 
/* 
 * A node in the directory listing 
 */ 
struct namenode { 
     nametype name;          /* name of directory entry */ 
     namelist next;          /* next entry */ 
}; 
 
/* 
 * The result of a READDIR operation. 
 */ 
union readdir_res switch (int Errno) { 
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; 

2.3.2 Implementing the Procedure Declared in the Protocol Specification

Example 2-6 (see SYS$EXAMPLES:[UCX.RPC]DIR_SERVER.C) consists of the dir_server.c program that implements the remote READDIR procedure from the previous RPC protocol specification file.

Example 2-6 Remote Procedure Implementation


/* 
** dir_server.c: remote OpenVMS readdir implementation 
*/ 
#include <errno.h> 
#include <rms.h> 
#include <rpc/rpc.h>   /* Always needed */ 
#include "dir.h"       /* Created by RPCGEN */ 
 
extern int SYS$PARSE(struct FAB *); 
extern int SYS$SEARCH(struct FAB *); 
 
extern char *malloc(); 
 
readdir_res * 
readdir_1(dirname) 
    nametype *dirname; 
{ 
    char   expanded_name[NAM$C_MAXRSS+1]; 
    struct FAB fab; 
    struct NAM nam; 
    namelist       nl; 
    namelist      *nlp; 
    static readdir_res res; /* must be static! */ 
    char   resultant_name[NAM$C_MAXRSS+1]; 
    int exit(); 
 
    /* 
    ** Initialize the FAB. 
    */ 
    fab = cc$rms_fab; 
    fab.fab$l_fna = *dirname; 
    fab.fab$b_fns = strlen(*dirname); 
    fab.fab$l_dna = "SYS$DISK:[]*.*;*"; 
    fab.fab$b_dns = strlen(fab.fab$l_dna); 
 
    /* 
    ** Initialize the NAM. 
    */ 
    nam = cc$rms_nam; 
    nam.nam$l_esa = expanded_name; 
    nam.nam$b_ess = NAM$C_MAXRSS; 
    nam.nam$l_rsa = resultant_name; 
    nam.nam$b_rss = NAM$C_MAXRSS; 
    fab.fab$l_nam = &nam; 
 
    /* 
    ** Parse the specification and see if it works. 
    */ 
    if (SYS$PARSE(&fab) & 1) { 
 /* 
 ** Free previous result 
 */ 
 xdr_free(xdr_readdir_res, &res); 
 
        /* 
        ** Collect directory entries. 
        ** Memory allocated here will be freed by xdr_free 
        ** next time readdir_1 is called 
        */ 
        nlp = &res.readdir_res_u.list; 
        while (SYS$SEARCH(&fab) & 1) { 
     resultant_name[nam.nam$b_rsl] = '\0'; 
            nl = (namenode *) malloc(sizeof(namenode)); 
     *nlp = nl; 
            nl->name = (char *) malloc(nam.nam$b_name + 
                                       nam.nam$b_type + 
                                       nam.nam$b_ver + 1); 
     strcpy(nl->name, nam.nam$l_name); 
            nlp = &nl->next; 
            } 
        *nlp = NULL; 
 
        /* 
        ** Return the result 
        */ 
        res.Errno = 0; 
        } /* SYS$PARSE() */ 
    else 
        res.Errno = fab.fab$l_sts; 
    
    return &res; 
    } 
 

2.3.3 The Client Program that Calls the Remote Procedure

Example 2-7 (see SYS$EXAMPLES:[UCX.RPC]RLS.C) shows the client side program, rls.c, that calls the remote server procedure.

Example 2-7 Client Program that Calls the Server


/* 
* rls.c: Remote directory listing client 
*/ 
#include <errno.h> 
#include <rms.h> 
#include <stdio.h> 
#include <rpc/rpc.h>    /* always need this */ 
#include "dir.h" 
 
main(argc, argv) 
     int   argc; 
     char *argv[]; 
{ 
     CLIENT *cl; 
     char   *dir; 
     namelist nl; 
     readdir_res *result; 
     char   *server; 
     int exit(); 
 
    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 DIRPROG on 
    ** the server designated on the command line.  Use 
    ** the tcp protocol when contacting the server. 
    */ 
    cl = clnt_create(server, DIRPROG, DIRVERS, "tcp"); 
    if (cl == NULL) { 
        /* 
        ** Couldn't establish connection with server. 
        ** Print error message and stop. 
        */ 
        clnt_pcreateerror(server); 
        exit(1); 
        } 
 
    /* 
    ** Call the remote procedure readdir on the server 
    */ 
    result = readdir_1(&dir, cl); 
    if (result == NULL) { 
        /* 
        ** An RPC error occurred while calling the server. 
        ** Print error message and stop. 
        */ 
        clnt_perror(cl, server); 
        exit(1); 
        } 
 
    /* 
    ** Okay, we successfully called the remote procedure. 
    */ 
    if (result->Errno != 0) { 
        /* 
        ** A remote system error occurred. 
        ** Print error message and stop. 
        **/ 
        errno = result->Errno; 
        perror(dir); 
        exit(1); 
        } 
 
    /* 
    ** Successfully got a directory listing. 
    ** Print it out. 
    */ 
    for (nl = result->readdir_res_u.list; 
             nl != NULL; 
                 nl = nl->next) 
        printf("%s\n", nl->name); 
    exit(0); 
    } 

2.3.4 Running RPCGEN

As with the simple example, you must run the RPCGEN protocol compiler on the RPC protocol specification file DIR.X:
$ RPCGEN DIR.X 

RPCGEN creates a header file, DIR.H, an output file of client skeletons routines, DIR_CLNT.C, and an output file of server skeleton routines, DIR_SVC.C. For this advanced example, RPCGEN also generates the file of XDR routines, DIR_XDR.C.

2.3.5 Compiling the File of XDR Routines

The next step is to compile the file of XDR routines, DIR_XDR.C:

$ CC/DECC DIR_XDR 

2.3.6 Compiling the Client and Server Programs

After the XDR compilation, use two CC and LINK sequences to create the client program and the server program:


Note

If you want to use the shareable version of the RPC object library, reference the shareable version of the library, SYS$SHARE:UCX$RPCXDR_SHR, in your LINK options file.

2.3.7 Copying the Server to a Remote System and Running It

Copy the server program dir_server to a remote system called space in this example. Then, run it as a detached process:

$ RUN/DETACHED DIR_SERVER 

From the local system earth invoke the RLS program to provide a directory listing on the system where dir_server is running in background mode. The following example shows the command and output (a directory listing of /usr/pub on system space):

$ MCR SYS$DISK:[]RLS "space" "/usr/pub" 
. 
.. 
ascii 
eqnchar 
kbd 
marg8 
tabclr 
tabs 
tabs4 

Note

Client code generated by RPCGEN does not release the memory allocated for the results of the RPC call. You can call xdr_free to deallocate the memory when no longer needed. This is similar to calling free, except that you must also pass the XDR routine for the result. For example, after printing the directory listing in the previous example, you could call xdr_free as follows:
xdr_free(xdr_readdir_res, result); 

2.4 Debugging Applications

It is difficult to debug distributed applications that have separate client and server processes. To simplify this, you can test the client program and the server procedure as a single program by linking them with each other rather than with the client and server skeletons. To do this, you must first remove calls to client creation RPC library routines (for example, clnt_create). To create the single debuggable file RLS.EXE, compile each file and then link them together as follows:
$ CC/DECC RLS.C 
$ CC/DECC DIR_CLNT.C 
$ CC/DECC DIR_SERVER.C 
$ CC/DECC DIR_XDR.C 
% LINK RLS,DIR_CLNT,DIR_SERVER,DIR_XDR,UCX$RPC:UCX$RPCXDR/LIBRARY 

The procedure calls are executed as ordinary local procedure calls and you can debug the program with a local debugger. When the program is working, link the client program to the client skeleton produced by RPCGEN and the server procedures to the server skeleton produced by RPCGEN.

There are two kinds of errors possible in an RPC call:

  1. A problem with the remote procedure call mechanism.
    This occurs when a procedure is unavailable, the remote server does not respond, the remote server cannot decode the arguments, and so on. As in Example 2-7, an RPC error occurs if result is NULL.
    The program can print the reason for the failure by using clnt_perror, or it can return an error string through clnt_sperror.
  2. A problem with the server itself.
    As in Example 2-6, an error occurs if opendir fails; that is why readdir_res is of type union. The handling of these types of errors is the responsibility of the programmer.

2.5 The C Preprocessor

The C preprocessor, CC/DECC/PREPROCESSOR, runs on all input files before they are compiled, so all the preprocessor directives are legal within an .X file. RPCGEN may define up to five macro identifiers, depending on which output file you are generating. The following table lists these macros:
Identifier Usage
RPC_HDR For header-file output
RPC_XDR For XDR routine output
RPC_SVC For server-skeleton output
RPC_CLNT For client-skeleton output
RPC_TBL For index-table output

Also, RPCGEN does some additional preprocessing of the input file. Any line that begins with a percent sign (%) passes directly into the output file, without any interpretation. Example 2-8 demonstrates this processing feature.

Example 2-8 Using the Percent Sign to Bypass Interpretation of a Line


/* 
 * time.x: Remote time protocol 
 */ 
program TIMEPROG { 
     version TIMEVERS { 
          unsigned int TIMEGET(void) = 1; 
     } = 1; 
} = 44; 
 
#ifdef RPC_SVC 
%int * 
%timeget_1() 
%{ 
%    static int thetime; 
% 
%    thetime = time(0); 
%    return (&thetime); 
%} 
#endif 


Previous | Next | Contents