O'Reilly logo

Microsoft RPC Programming Guide by Ward Rosenberry, John Shirley

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Manipulate the data structure in your application the same as any structure, includ ing passing data in the remote procedure call. However, the client stub uses the special procedures to manage the binding. The customized binding handle must be the first parameter in a remote procedure (or the global handle for the implicit method) to act as the binding handle for the call. A customized handle acts as a standard parameter if it is not the first parameter.

Example 3-10 shows how to define a customized binding handle in an interface definition.

Example 3~10: Defining a Customized Binding Handle

I* FILE NAME: search. idl */ [

uuid(2450F730-5170-10lA-9A93-08002B2BC829),

version (1.0) ,

pointer_default (ref ) ] interface search /* Search remote file for data */

const long LINESIZE = 100;

/* Constant for maximum line size */

Example 3~ 10'- Defining a Customized Binding Handle (continued)

const long FILENAME_SIZE = 100; /* Constant for file name length */

const long BINDING_SIZE = 32; /* Constant for host name size */

const short FILE_ERROR = -1; /* Status for search file error */

const short NO_MATCH =0; /* Status for no match found */

/*

** Customized binding handle definition

** contains the file name and the string

** binding to use.

*/

typedef [handle] struct { /* customized handle type O */

unsigned char binding [BINDING_SIZE] ;

unsigned char filename [FILENAME_SIZE] ; } search_spec; /* © */

/*

** Search for a string match on the file specified

** in the customized binding handle above.

*/

short searchit( /* search a file on the server */

[in] search_spec custom_handle , /* customized binding handle©*/ [in, string] char search_string[LINESIZE] , /* target string */ [out, string] char return_string[LINESIZE] , /* results */

[out] error_status_t * error /* comm/ fault status */

O Use the handle attribute in the interface definition to associate a customized binding handle with a data type.

© The f ile_spec data type is a structure whose members are file specifications. This is application-specific information used by the bind procedure to obtain server binding information.

© The customized binding handle is the first parameter of a procedure declara tion. This is an example of explicit binding.

You must implement bind and unbind procedures. Example 3-11 shows how you can implement these procedures inside a client file.

Example 3~ 1 1: Bind and Unbind Procedures

I* FUNCTION: search_spec_bind */

handle_t RPC API

search_spec_bind(custom_handle) /* bind procedure for customized handle O */

search_spec custom_handle;

rpc_binding_handle_t binding_h;

printf ("\n\t ( Selecting server binding: %s)\n\n", /* Display server binding*/ custom_handle . binding) ;

status =

RpcBindingFromStringBinding ( /* Convert the character string */

custom_handle. binding, /* binding into an RPC handle */

&binding_h,

Example 3~H: Bind and Unbind Procedures (continued)

);

CHECK_STATUS(status, "Invalid string binding", RESUME); exit (EXIT_FAILURE); return (binding_h); }

/* FUNCTION: search_spec_unbind */ void RFC API

search_spec_unbind( /* unbind procedure for customized handle © */

custom_handle, binding_h)

search_spec custom_handle; handle_t binding_h; {

status =

RpcBindingFree( /* Free the binding handle */

&binding_h);

CHECK_STATUS(status, "Can't free binding handle:", RESUME); return; }

O The bind procedure takes an input parameter of the customized handle data type, and returns a primitive binding handle. You construct the procedure name from the data type name, search_spec, to which you append _bind. In this example searcb_spec_bind constructs a binding handle from arguments passed on the client command line to obtain a primitive binding handle.

© The unbind procedure takes input parameters of the customized handle data type and a primitive binding handle. You construct the procedure name from the data type name, search_spec, to which you append _unbind. In this example search>_spec_unbind calls the RFC runtime routine, RpcBindingFree, to free the binding handle.

Example 3-12 shows how an application client can use a customized binding han dle.

Example 3~12: A Client with a Customized Binding Handle

I* FILE NAME: client_send.c */

int

MAIN_DECL main(ac, av)

int ac ;

char *av[];

{

short search_status; /* status from search */

idl_char match[LINESIZE]; /* string to look for */ search_spec custom_handle; /* customized binding handle O */

match[0] = '\0'; /* Initialize some strings */

custom_handle.binding[0] = '\0'; custom_handle.filename[0] = '\0';

Example 3~12: A Client with a Customized Binding Handle (continued)

I*

** There should be 4 parameters to searchit:

**

** searchit <hostname> <filename> <matchstring> **

** where **

** <hostname> is the hostname where the file to be searched

** exists.

**

** <filename> is the name of the file to be searched. **

** <matchstring> is the string to search <filename> for. **

*/

if (ac != 4) /* Exit if not the right number of parameters */

{

printf("\t\nUsage: searchit <hostname> <filename> <matchstring>\n\n");

exit (EXIT_FAILURE) ;

}

/*

** Set up the string binding, the filename, and the

** match string from the command line.

*/

strcpy ((char *)custom_handle.binding, "ncacn_ip_tcp:"); /* © */

strcat ((char *)custom_handle.binding, av[l]);

strcpy ((char *)custom_handle.filename, av[2]);

strcpy ((char *)match, av[3]);

/*

** Search the given file on the given host for the

** given string...

*/

search_status = searchit( /* Remote procedure with input © */

custom_handle,

match,

result,

&rpc_status

O The application allocates the customized binding handle.

@ Initialize the customized binding information in the client before calling the remote procedure. For this example, when we invoke the client, we input the server host name, remote data filename, and search string as arguments.

© The remote procedure is called with the customized binding handle as the first parameter.

Authentication

Authentication in a distributed environment is a broad topic that is outside the scope of this book. Although we do not provide details on implementing security in Microsoft RFC applications, we will mention the major aspects and some trade offs involved in selecting various models.

Microsoft RFC can use the security features of Microsoft Windows NT which are built into the named pipes (ncacn_np) and local RFC (ncalrpc) transports. You must restrict your application to using one of the two listed transports to use this security system.

You can use the Windows NT security features by specifying options to the end-point parameter in a string binding. Options have names such as anonymous, identification, or impersonation, controlling which level of security to use.

Alternatively, you can use RFC security available in Microsoft RFC. This form of security is transport-independent so your application can use other transports in addition to named pipes and local RFC. Microsoft RFC security currently uses the Windows NT Security Service as the only supported security provider.

RFC security offers three kinds of protection: authentication, data integrity, and data privacy. Data integrity and data privacy involve extra encryption and decryp tion cycles which can be time consuming, so use these features only when neces sary.

On client systems you can use RFC security by including the RpcBindingSetAuth-Info routine in your client program. Briefly, this routine places the client's identity information into the binding handle which is passed to the server as the first parameter in a remote procedure call.

Servers extract the client authentication information from the client binding handle using the RpcBindinglnqAuthClient routine. Servers use this information to verify a client's authenticity.

The server system supplies its identity information to clients by registering it with the RpcServerRegisterAuthlnfo routine. Clients or other servers can extract this information to authenticate the server's identity. Use the RpcBindinglnqAuthlnfo routine to extract server authentication information from the server binding han dle.

To recap, using the transport level security built into named pipes and local RFC does not necessarily add lots of new code to an application. If you want to use security over transports other than named pipes or local RFC (for instance, TCP/IP or DECnet), you'll need to use RFC security features which can require extra pro gramming overhead.

Error Parameters or Exceptions

Microsoft RFC client applications require special error-handling techniques to deal with errors that may occur during a remote procedure call. The following discus sion pertains to both client and server development.

Server and communication errors are raised to the client as exceptions during a remote procedure call. RFC exceptions are similar to the RFC error status codes. Errors have names with S as the second component, as in RPC_S_ADDRESS_ERROR. Exceptions have X as the second component, as in RPC_X_NO_MEMORY.

Types of exceptions include the following:

• Exceptions raised on the client system, such as when the client process is out of memory (RPC_X_NQ_MEMORY).

• Exceptions raised to the client application by the client stub, such as when the stub has received bad data (RPC_X_BAD_STUB_DATA).

• Exceptions raised by the client stub on behalf of the server. These errors can occur in the server stub, in the remote procedures, or in the server's RFC run time library. The server transport layer does not return exceptions to the client.

A distributed application can have errors from a number of sources, so you will need to decide whether you want to handle errors with exception handling code or error parameters. This may simply be a matter of personal preference or consis tency.

Using Exception Handlers in Clients or Servers

You can handle exceptions by writing exception handler code in the application to recover from an error or gracefully exit the application. Microsoft RFC supplies a set of macros as a framework to handle exceptions in your client or server code. (Example 5-6, in Chapter 5, uses RFC exception handling macros.) If your applica tion is written for Win32 only, use the Win32 versions of these macros.

Using Remote Procedure Parameters to Handle Errors

In your ACF, you can add error parameters to remote procedures in order to con veniently handle communication and server errors. The RFC runtime library then stores errors values in these parameters rather than raising exceptions. You can also use a combination of exception handlers and error parameters.

When the simple arithmetic application in Chapter 1 encounters some errors, it returns a hexadecimal number. You must convert this number to a decimal error code number and then look it up to find out what happened. By making three simple changes to the arithmetic application you can get it to return the actual RFC error code:

• Add an error_status_t parameter to the remote procedure declaration in the MIDL file.

• Add an error_status_t parameter to the remote procedure implementation in the server.

• Declare the variable in the client file.

The sum_arrays procedure declaration in the following MIDL file has an [out] parameter of the type error_status_t.

void sum_arrays ( /* The sum_arrays procedure doesn't return a value */ [in] long_array a, /* 1st parameter is passed in */

[in] long_array b, /* 2nd parameter is passed in */

[out] long_array c, /* 3rd parameter is passed out */

[out] error_status_t *rpc_status /* error parameter is passed out */

We've added the error_status_t parameter to the remote procedure in man ager.c. The initialized status value can be changed by the stubs if an error occurs.

void sum_arrays(a, b, c, rpc_status) /* sum_arrays implementation */ long_array a; long_array b; long_array c; error_status_t *rpc_status; /* error status parameter */

int i;

*rpc_status = RPC_S_OK; /* initializes the status value */ for(i = 0; i < ARRAY_SIZE; i++)

c[i] = a[i] + b[i]; /* array elements are added together */

The client code declares the variable along with the rest of the variables. Then a CHECK_STATUS macro converts the RFC error code to a status message.

/* FILE NAME: client.c */

/* This is an arithmetic client module with error handling. */

#include <stdio.h>

#include <stdlib.h>

Mnclude "ari,th.h" /* header file created by MIDL compiler */

ttinclude "status.h" /* needed for CHECK_STATUS macro */

long_array a ={100,200,345,23,67,65,0,0,0,0}; long_array b ={4,0,2,3,1,7,5,9,6,8};

main ()

long_array result;

int i;

/* declare variable and initialize */

error_status_t rpc_status=RPC_S_OK;

/* remote procedure with status */ sum_arrays(a, b, result, &rpc_status);

I* report error and abort */

CHECK_STATUS (rpc_status, "ERROR:", ABORT);

puts ("sums: ") ;

for(i =0; i < ARRAY_SIZE; i++)

printf ( "%ld\n" , result [i] ) ; }

The CHECK_STATUS macro shown in Example 3-13 converts the RFC error code to an error message.

Example 3~ 13: The CHECK_STATUS Macro

/* FILE NAME: status. h */

ttinclude <stdio.h>

ttinclude <stdlib.h>

#include "..\rpcerror.h" /* maps error codes to error messages O */

#def ine RESUME 0 #def ine ABORT 1

#define CHECK_STATUS ( input_status , comment, action) \ { \

if (input_status != RPC_S_OK) { \

error_stat = DceErrorinqText ( input_status , error_string) ; \ /* © */ fprintf (stderr, "%s %s\n", comment, error_string) ; \ if (action == ABORT) \

exit(l); \ } \

static int error_stat;

static unsigned char error_string[DCE_C_ERROR_STRING_LEN] ;

O The file rpcerror.h is shown in Appendix C, The Arithmetic Application.

© Although Microsoft RFC does not support the DCE RFC routine dce_error_inq_text , we've emulated its function here.

Compiling and Linking Clients

Figure 3-3 shows the files and libraries required to produce an executable client. When complex data types are used, the MIDL compiler produces the client stub auxiliary file (appl_x.c) when the interface is compiled. Example 3-14 shows the portion of a makefile that:

• Compiles the C language stubs and client code along with the header file pro ducing server object files

• Links the server object files to produce the executable server file

Chapter 3: How to Write Clients

75

Write client application files.

Include the header file(s) produced by interface compilation.

Generate client application and stub object files.

Use the client stub and auxiliary files produced by interface compilation.

Create the executable client file by linking the client application, stub, and auxiliary object files with the Microsoft RFC library.

Text Editor

picture20

Figure 3~3- Producing a client

Example 3~14: Using a Makefile to Compile and Link a Client

# FILE NAME: Makefile

# Makefile for the inventory application implicit client #

# definitions for this make file #

APPL=inv

IDLCMEfcmidl

NTRPCLIBS=rpcrt4.lib rpcns4.1ib libcmt.lib kerne!32.1ib

!include <ntwin32 .mak>

## NT c flags

cflags = -c -WO -Gz -D_X86_=1 -EWIN32 -EMT /I. /I., /nologo

Example 3~14: Using a Makefile to Compile and Link a Client (continued)

## NT nmake inference rules

$(cc) $(cdebug) $(cflags) $(cvarsmt) $< $(cvtomf)

#

# CLIENT BUILD #

client: client.exe

client.exe: client.obj getbind.obj intbind.obj $(APPL)_c.obj $(APPL)_x.obj $(link) $(linkdebug) $(conflags) -out:client.exe -map:client.map \

client.obj getbind.obj intbind.obj $(APPL)_c.obj $(APPL)_x.obj \

$(NTRPCLIBS)

# client and server sources client.obj: client.c $(APPL).h getbind.obj: getbind.c intbind.obj: intbind.c

Local Testing

You can compile a local version of your client to test and debug remote proce dures without using remote procedure calls. To do a local test, compile the client object files and remote procedure implementations without the stub or auxiliary files. The code that finds a server is also unnecessary for a local test. Applications in this book use the compiler directive, /DLOCAL, to distinguish a test compilation used in a local environment from a compilation used in a distributed environment. Example 3-15 shows the portions of a makefile that produce the inventory applica tion for local testing.

Example 3~15: Using a Makefile to Produce a Local Version of an Application

# FILE NAME: Makefile

# Makefile for the inventory application implicit client #

# definitions for this make file #

APPL=inv

IDLCMD=midl

NTRPCLIBS=rpcrt4.lib rpcns4.1ib libcmt.lib kerne!32.1ib

!include <ntwin3 2.mak>

## NT c flags

cflags = -c -WO -Gz -D_X86_=1 -DWTN32 -EMT /I. /I., /nologo

## NT nmake inference rules

Example 3~15: Using a Makefile to Produce a Local Version of an Application (continued)

$(cc) $(cdebug) $(cflags) $(cvarsmt) $< $(cvtomf)

#

# LOCAL BUILD of the client application to test locally

#

local: Iclient.exe

lclient.exe: Iclient.obj Imanager.obj invntry.obj

$(link) $(linkdebug) $(conflags) -out:Iclient.exe -map:Iclient.map \

Iclient.obj Imanager.obj invntry.obj \

$(NTRPCLIBS)

# Local client sources invntry.obj: ..\invntry.c

$(cc) $(cdebug) $(cflags) $(cvarsmt) /DLOCAL /I. /I.. \

/Foinvntry.obj ..\invntry.c Iclient.obj: client.c $(APPL).h

$(cc) $(cdebug) $(cflags) $(cvarsmt) /DLOCAL /I. /I.. \

/Folclient.obj client.c Imanager.obj: ..\manager.c $(APPL).h

$(cc) $(cdebug) $(cflags) $(cvarsmt) /DLOCAL /I. /I.. \ /Folmanager.obj ..\manager.c

In this Chapter:

• Kinds of Pointers

• Kinds of Arrays

• Memory Usage

picture21

Pointers, Arrays, and Memory Usage

In C, pointers and arrays have a close correlation due to the way applications access the information they contain. Pointers and arrays work essentially the same in distributed and local applications. But there are a few restrictions in distributed applications because the client and server have different address spaces. In most of this chapter we discuss pointers and arrays for clients. See also Chapter 5, How to Write a Server, for a discussion of memory allocation for pointers and arrays in remote procedures.

To make your applications more efficient, MIDL offers several kinds of pointers and arrays to reduce network traffic and stub overhead. This chapter uses the inventory application to demonstrate the use of pointers and arrays in distributed applications.

Kinds of Pointers

A pointer is a variable containing the address of another data structure or variable. As in C, you declare a pointer in an interface definition by using an asterisk (*) followed by a variable. For example, the inventory application has the following procedure declaration:

void whatis_part_price( /* get part price from inventory */

[in] part_num number,

[out] part_price *price );

In a distributed application, the client and server do not share the same address space. This means the data a pointer refers to in the client is not available in the remote procedure of the server. The opposite is also true. Therefore, pointer data is copied between the client and server address spaces during a remote procedure call. For the whatis_part_price procedure, data that the pointer argument refers to on the server is copied back to the client and placed in the memory referred to by

the price pointer. This copying of pointer data does not occur during a local pro cedure call.

MIDL has three kinds of pointers: reference, unique, and full. We'll describe them here in order of increasing capability. Keep in mind, though, that more capabilities result in more stub overhead.

A reference pointer is used to refer to existing data. It is the simplest kind of pointer. Consequently, it has a performance advantage over other kinds of point ers because stub overhead is minimal. For example, the whatis_part_price proce dure uses a reference pointer. This procedure passes by reference a pointer to an allocated part_price data structure. The remote procedure returns output data to the same memory location with the part price. Thus, for reference pointers, the data can change but not the address itself. The [ref ] attribute specifies a refer ence pointer in an interface definition.

Sometimes you need a pointer that can do more, such as handling singly-linked lists in which the end of the list is marked with a null pointer. For this situation, a unique pointer might be used because it can be null. A unique pointer has many capabilities usually associated with pointers. Use unique pointers in interface defi nitions when a remote procedure call allocates new memory for the client. In this case, the client stub actually allocates the memory. Unique pointers cannot address data that is also addressed by other pointers in the remote procedure, so you should avoid using complex data structures with cycles (doubly-linked lists). The [unique] attribute specifies a unique pointer in the interface definition.

A full pointer has all of the capabilities associated with unique pointers. In addi tion, it allows two pointers to refer to the same address, as in a double linked list. The [ptr] attribute specifies a full pointer in the interface definition. Full pointer capability incurs significant stub overhead, so use full pointers only when neces sary.

A pointer attribute must be applied where the pointer is defined with an asterisk. For instance, if you define a typedef that resolves to a pointer, you cannot apply the pointer attribute where you use the typedef.

The following sections discuss the use of pointers, and tell you when you need a reference or full pointer. Table 4-1 and Example 4-5 summarize what you need to know to declare and use pointers.

Pointers as Output Parameters

Due to the overhead of transmitting data, you have to declare MIDL parameters to be input, output, or both. In MIDL, as in C, input parameters are passed in by value, which means a copy of each input parameter is available in the procedure. Passing input parameters by value makes sense for a small amount of data. This technique offers simplicity since programmers don't have to deal with pointers or addresses. However, passing by value also means that any change to the variable

Chapter 4: Pointers, Arrays, and Memory Usage

in the procedure cannot reflect back to the original parameter when the call com pletes.

To fill in data for an output parameter (or modify an input/output parameter), both C and MIDI must pass by reference a memory address using a pointer or array parameter. During a remote procedure call, the parameter refers to existing memory, which is passed by reference to the client stub. When the remote proce dure completes execution, data is sent back by the server stub to the client stub, which unmarshalls it into the memory referred to by the pointer. Therefore, the data is available to the client application when the client stub returns to the appli cation.

Example 4-1 shows an output parameter in the whatis_part_price procedure dec laration from the inventory interface definition. Pointer parameters (*price) are reference pointers by default.

Example 4-1: Defining an Output Parameter

void whatis_part_price ( /* get part price from inventory */

[in] part_num number,

[out] part_price *price /* reference pointer */

);

The part_price structure must be allocated in the client prior to the remote pro cedure call, but values are assigned in the remote procedure and transmitted back. The whatis_part_price remote procedure call in the client looks like this:

part_record part; /* structure for all data about a part */

case 'p': whatis_part_price(part.number, &(part.price)}; printf("price:%10.2f\n", part.price.per_unit); break;

In the server, whatis_part_price reads a part record from the database for the part number input. It then assigns the values from the part record to the price structure members. Finally, the procedure returns and the price information is marshalled and transmitted by the server stub. The whatis_part_price remote procedure looks like this:

void whatis_part_price(number, price) part_num number; part_price *price; {

parc_record *part; /* a pointer to a part record */

. read_part_record(number, &part); price->units = part->price.units; price->per_unit = part->price.per_unit; return;

You can see from the preceding explanation that an output parameter must refer to existing storage on the client, and therefore that it is always a reference pointer. In fact, the MIDL compiler refuses to let you declare an output-only parameter with the unique or ptr attribute.

Suppose we don't know how much memory should be allocated for output data, so we want a procedure to return data in a parameter as newly allocated memory. We cannot just allocate some memory and hope it's enough because if the data output is greater, data will overwrite into other memory. To solve this, we pass a pointer to a pointer. We describe how to do this later in the chapter.

A parameter used as both input and output is passed by reference. Programs com monly modify data by passing a pointer to a data structure into a procedure, which passes back the same pointer but with modified data. Optional (NULL) parameters can be used as input/output parameters. This feature is described in the following section.

Pointers as Input Parameters

Suppose our inventory interface has the following procedure declaration:

void store_parts(

[in] part_record *partl,

[in] part_record *part2 );

Assume this procedure adds new parts to the database. The procedure takes as parameters two pointers to structures of type part_record, (already defined in the interface) to store all data about a part.

The remote procedure call in a client can look like the following:

part_record *partl, *part2;

parti = (part_record *)malloc(sizeof(part_record));

part2 = (part_record *)itialloc(sizeof (part_record) ) ;

/* part structures are filled in */

partl->nuniber = 123;

part2->number = 124;

store_parts(parti, part2);

In this simple case, the client stub marshalls and transmits the data the pointers refer to. (This procedure is not implemented in any applications in this book, so no server code is shown.)

One reason reference pointers reduce overhead is that the stubs make certain assumptions about the use of the pointer. Since pointer parameters are reference pointers by default, one of these assumptions is that a pointer parameter points to valid data of the type specified.

Suppose we want optional parameters in our procedure definition. In this case, the client passes a null pointer value for the parameter, so the remote procedure knows to ignore it. For the stubs to know the parameter is a null value, the param eter must be a unique pointer so the stubs do not attempt to copy any data for the parameter.

Example 4-2 shows how to modify our store_parts procedure declaration so that both parameters are unique pointers.

Example 4-2: Defining Optional Procedure Parameters

void store_parts_l ( /* O */

[in,unique] part_record *partl, [in,unique] part_record *part2

typedef [unique] part_record *part_record_ptr; void store_parts_2 ( /* © */

[in] part_record_ptr parti,

[in] part_record_ptr part2

O To specify an optional parameter, use the unique attribute on an input (or input/output) parameter.

© As an alternative to method 1 for specifying an optional parameter, define a unique pointer data type and use the data type for the procedure parameter.

The client can now supply a NULL pointer:

store_part s_l(part1, NULL);

If an input/output parameter is a unique pointer with a null value on input, it is also null on output because the client does not have an address to store a return value.

Microsoft RFC allows two pointers to refer to the same data. This practice is known as pointer aliasing.

picture22

To minimize overhead, stubs cannot manage more than one reference pointer referring to the same data in a single remote procedure call. For example, suppose our store_parts procedure does something useful if we pass in the same pointer

for both arguments. The following type of remote procedure call causes unpre dictable behavior:

store_parts(parti, parti); /* WRONG—do not use ref pointer aliasing */

This call will not work as expected because the parameters (reference pointers) both point to the same address. Reference pointers and unique pointers do not allow two pointers to refer to the same data.

If store_parts_l were used instead of store_parts, the call would work correctly, because the arguments were specifically defined in the interface definition as full pointers with the ptr attribute.

Using Pointers to Pointers for New Output

A pointer refers to a specific amount of memory. For a procedure parameter to output newly allocated memory, we use a pointer to refer to another pointer that refers to data (or to another pointer and so on). This is also known as multiple levels of indirection.

If you use just one pointer for a procedure parameter, you would have to make two remote procedure calls to allocate new memory. The first remote procedure call obtains the size of the server's data structure. Then the client allocates memory for it. The second remote procedure call obtains data from the server and fills the previously allocated memory. In a distributed application, using two pointers allows the client and server stubs to allocate all the necessary memory in one remote procedure call. The client stub must generate a copy of the memory allo cated on the server.

The whatare_subparts procedure in the inventory application contains a parameter with a pointer to a pointer:

[out] part_list **subparts

The procedure allocates memory for the left pointer, and the right pointer is a parameter passed by reference to return the address of the left pointer. To accom plish this, MIDI must use two kinds of pointers:

The right pointer is a reference pointer and the left pointer is a unique pointer. The reference pointer by itself cannot have new memory automatically allocated because it will point to the same address throughout the remote call. However, for the unique pointer, the amount of memory allocated by the server is allocated automatically by the client stub when the call returns.

When a pointer attribute is applied in an interface definition where there are pointers to pointers, it applies only to the right pointer and does not propagate to any other pointers.

Example 4-3 demonstrates how to return data in a parameter by using two point ers. The procedure needs to output a data structure (in this case a structure with a conformant array). The final size of the data structure is unknown when you call the remote procedure.

Example 4-3: Defining Pointers to Pointers for Memory Allocation

pointer_default(unique) /* the pointer default is a unique pointer O */ ] interface inventory

void whatare_subparts( /* get list of subpart numbers for a part */

[in] part_num number,

[out] part_list **subparts /* a pointer to a pointer €) */

);

O Parameters or type definitions with multiple pointers use a pointer default to specify the kind of pointer for all but the right one. To establish a pointer default, use the pointer_default attribute in the interface definition header. In this example, the unique argument establishes a unique pointer default.

© If memory is allocated during remote procedure execution, output parameters require multiple pointers. By default, the right pointer of a procedure parame ter is a reference pointer. The left pointer must be a unique pointer. This is accomplished through the pointer_default attribute.

The part_list structure is allocated during the remote procedure call. On the server, the remote procedure allocates memory and assigns data. The server stub marshalls and transmits the data back to the client. The server stub then frees the memory allocated in the remote procedure. The client stub allocates memory and unmarshalls the transmitted data into the new memory. The remote procedure call in a client for whatare_subparts looks like:

part_record part; . /* structure for all data about a part */ part_list *subparts; /* pointer to parts list data structure */

case 's': whatare_subparts(part.number, &subparts); for(i = 0; i < subparts->size; i++)

printf("%ld ", subparts->numbers[ i ]); printf("\ntotal number of subparts:%ld\n", subparts->size)

When you finish with the data, free the memory allocated by unique pointers:

free(subparts); break;

See Example 5-9 in Chapter 5 for the server implementation of the remote proce dure whatare_subparts.

Pointers as Procedure Return Values

As we have described previously, the client must allocate memory for reference pointer data before it is used in a remote procedure call. This simplifies the client stub by giving unmarshalling code a place to put data after the server sends it. Now consider the following remote procedure call in client application code:

unsigned long *a; a = proc();

The address of the procedure assignment, a, is available only when the procedure returns, and not during its execution. Therefore, we cannot use the method just described for a reference pointer to allocate memory in the client prior to the call, and expect the stub to complete the assignment for us. Procedures that return pointer results always return full pointers, so that the stub allocates any necessary memory and unmarshalls data into it for us. Example 4-4 shows a procedure that returns a pointer.

Example 4-4: Defining a Procedure that Returns a Pointer

typedef [string, unique] char *paragraph; /* description of part O */

paragraph get_part_description( /* return a pointer to a string© */

[in] part_num number );

O A pointer attribute (unique) on a pointer data type (char *paragraph) speci fies the kind of pointer for that data type wherever it is used in the interface. (If a pointer data type does not have a pointer attribute, the pointer specified with the pointer_default attribute applies.) To specify a pointer to a string, apply the string attribute as well.

@ Procedures that return a pointer result always return a full pointer. A proce dure result cannot be a reference pointer because new storage is always allo cated by the client stub, which copies data into it when the call returns.

The call to get_part_description looks like:

part_record part; /* structure for all data about a part */

Chapter 4: Pointers, Arrays, and Memory Usage

87

case 'd': part.description = get_part_description(part.number); printf("description:\n%s\n", part.description);

When you finish with the data, free the memory allocated by unique pointers:

if(part.description != NULL) free(part.description);

/* free memory allocated */

On the server, the remote procedure allocates memory that the server stub copies and transmits back to the client. The server stub then frees the memory allocated. Example 5-8 shows how to allocate memory in the get_part_description remote procedure.

Pointer Summary

As reference pointers, unique pointers, and full pointers represent increases in capability, they also require increases in stub overhead needed to manage them. Therefore, you must differentiate among reference, unique, and full pointers in the interface definition. Table 4-1 summarizes and compares pointer types. Example 4-5 shows how to recognize which kind of pointer applies in an interface defini tion. A visible ref or unique pointer attribute overrides a default.

Table 4-1: A Summary of Reference and Unique Pointers

Reference Pointer Unique Pointer

Full Pointer

Microsoft RFC Programming Guide

Example 4-5: How to Determine Kinds of Pointers

pointer_default(unique); ] inventory interface

/*O*/

typedef [string, unique] char *paragraph ;/*©*/

paragraph get_part_description( [in] part_num number,

/*©*/

void whatis_part_price(

[in] part_num number, [out] part_price *price

/* O */

void whatare_subparts(

[in] part_num number, [out] part_list **subparts

/*© */

typedef struct { [ref] part_num

*number;

/* © */

Example 4-5: How to Determine Kinds of Pointers (continued)

[ref] part_quantity *quantity; [ref] account_num *account; } part_order;

void store_parts_l (

[in, unique] part_record *partl, [in, unique] part_record *part2

O The MIDL compiler attempts to assign the appropriate kind of pointer to pointers without a full, unique, or ref attribute. The pointer_default interface header attribute specifies which kind of pointer applies when one cannot be automatically determined. You can give the pointer_default attribute an argument of ref, unique, or full. If a pointer attribute is not specified for the data type, the interface requires a pointer default to specify the kind of pointer for the following cases:

• Pointers in typedefs (see callout 2)

• Multiple pointers other than the right pointer (see callout 5)

• Pointers that are members of structures or cases of discriminated unions (see callout 6)

© A pointer type attribute specifies the kind of pointer used. In this example, all occurrences that use the paragraph data type are unique pointers. If none of the pointer attributes — ref, unique or full — is present in the typedef, the pointer_default attribute specifies the kind of pointer.

© A pointer return value of a procedure is always a unique pointer because new memory is allocated. The paragraph data structure is a pointer to a string.

O A pointer parameter of a procedure is a reference pointer by default. Parame ter reference pointers must always point to valid storage (never null). (See also callout 7.)

0 With multiple pointers, the pointer_default attribute specifies all pointers except the right-most pointer. In this example, the right pointer is a reference pointer because it is a parameter pointer. The left pointer is determined by the pointer default. In this procedure, the left pointer must be a unique pointer so the array of parts in the subparts structure is automatically allo cated by the client stub when the call returns.

© When a structure member or discriminated union case is a pointer, you must assign it a unique or ref attribute, either explicitly or through the attribute pointer_default. This interface definition specifies the structure members as reference pointers in order to override the unique pointer default. Unique or

full pointers are unnecessary for these structure members; therefore, it is more efficient to use reference pointers to minimize the overhead associated with unique pointers.

0 An input or input/output pointer parameter can be made an optional proce dure parameter by applying the unique attribute. An attribute of either unique or ptr is required if you pass a value of NULL in a call.

Kinds of Arrays

You can use the following kinds of arrays in RFC applications:

• Fixed arrays contain a specific number of elements defined in the interface definition. They are defined just like standard C declarations.

• Varying arrays have a fixed size but clients and servers select a portion to transmit during a remote procedure call. The interface definition specifies sub set bound variables used by the clients and servers to set the bounds.

• Conformant arrays have their size determined in the application code. The interface definition specifies an array size variable that the clients and servers use to control the amount of memory allocated and data transmitted.

Selecting a Portion of a Varying Array

For some clients or servers you need to use only a portion of an array in a remote procedure call. If this is the case, it is more efficient to transmit only the needed portion of the array. Procedures or structures that use varying arrays with data limit variables allow you to select the portion of an array that is processed by a remote procedure call.

A varying array has a fixed size when the application is compiled, but the portion of the array that contains the relevant, transmissible data is determined at runtime. For example, given the varying array arr [100], you can specify any index values in the range 0 < L < U < 99, where L represents the lower data limit of the array and (/represents the upper data limit.

An array is varying if you declare it in your interface definition with two extra attributes: first_is to indicate where transmission starts (Z), and either length_is or last_is to indicated where transmission stops ((/). Whether you use length_is or last_is depends on convenience.

Suppose that the following procedure appears in an interface definition:

const long SIZE = 100;

void proc(

[in] long first, [in] long length,

[in, first_is(first), length_is(length)] data_t arr[SIZE] );

To select a portion of the array to transmit, assign values to the variables first and length. For input parameters, the client sets them prior to the remote proce dure call. Be sure the upper data limit value does not exceed the size of the array, for example:

long first = 23; long length = 54; data_t arr[SIZE];

proc(first, length, arr);

The transmitted array portion is represented by the indices |23| . . . [76| (23 + 54 -1). The entire array is available in the client and the server, but only the portion represented by the data limit variables is transmitted and meaningful for the given remote procedure call. If the data limit parameters are also output, the remote pro cedure can set them to control the portion of the array transmitted back to the client.

A structure is an alternate way to define a varying array in an interface definition; for example:

typedef struct varray_t {

long first;

long length;

[first_is(first), length_is(length)] data_t arr[SIZE]; } varray_t;

proc([in] varray_t varray);

Managing the Size of a Conformant Array

Conformant arrays are defined in an interface definition with empty brackets or an asterisk (*) in place of the first dimension value.

. . . cl[*] . . . . . . c2[] [10] . . .

The conformant -array cl [ * ] has index values [o] ... \M\ in which the dimension variable, M, represents the upper bound of the array. The dimension variable is specified in the interface definition and used in the application code at runtime to establish the array's actual size.

To specify an array size variable or a maximum upper bound variable, use one of the array size attributes, size_is or max_is, in an interface definition. These vari ables are used in the application to represent the size of the array. You can use either one, depending on which you find most convenient. Example 4-6 shows how a conformant array is defined in a structure.

Microsoft RFC Programming Guide

Example 4-6: A Conformant Array in an Interface Definition

typedef struct part_list{

long size; /" numoer or pares in array w "/

[size_is(size) ] part_num numbers[*]; /* conformant array of parts©*/

} part_list;

/* list of part numbers */ /* number of parts in array O */

typedef struct part_record { /* data for each part */

part_num number;

part_name name;

paragraph description;

part_price price;

part_guantity quantity;

part_list subparts; /* Conformant array or struct must be last © */ } part_record;

void whatare_subparts(

[in] part_num number, [out] part_list **subparts

/* get list of subparts numbers for a part */

/*©*/

O When an array member of a structure (numbers [*]) has an array attribute, the dimension variable (size) must also be a structure member. This assures that the dimension information is always available with the array when it is trans mitted. The dimension variable member must be, or must resolve to, an inte ger.

© The size_is attribute specifies a variable (size) that represents the number of elements the array dimension contains. In the application, the array indices are 0 ... |size-l[ . For example, if size is equal to 8 in the application code, then the array indices are

© If a conformant array is a member of a structure, it must be last so that your application can allocate any amount of memory needed. A conformant struc ture (structure containing a conformant array member) must also be the last member of a structure containing it.

O Use a conformant structure and multiple levels of indirection for remote pro cedures that allocate a conformant array. Chapter 5 implements this proce dure.

To specify a variable that represents the highest index value for the first dimension of the array rather than the array size, use the max_is attribute instead of the size_is attribute. For example, the conformant structure defined in Example 4-6 can also be defined as follows:

Chapter 4: Pointers, Arrays, and Memory Usage

typedef struct part_list{

long max;

[max_is (max) ] part_num numbers [*] ; } part_list;

The variable max defines the maximum index value of the first dimension of the array. In the application, the array indices are \Q\ . . . |max| . For example, if max is equal to 7 in the application code, then the array indices are

To avoid making mistakes in application development, be consistent in the inter face definitions you write. Use either the size_is attribute or the max_is attribute for all your conformant arrays.

Conformant arrays as procedure parameters

When you call a remote procedure that contains a conformant array, you must pass the number of elements that are contained by the array. When a client calls the whatare_subparts remote procedure of Example 4-3, the dimension informa tion is available in the part_list structure. However, if an array is passed as a parameter, the dimension information must also be an in parameter of the proce dure.

For example, instead of obtaining an array of all the subparts for a part (as the ivhatare_subparts procedure does) you may want only the first five subparts. This procedure is defined as follows:

void get_n_subparts ( /* get n subpart numbers for a part */

[in] part_num number,

[in] long n,

[out,size_is (n) ] part_num subparts [] );

In the client, the input includes the part number, a 5 representing the number of subparts desired, and a previously allocated array, large enough for the five sub-part numbers. The output is the array with the first five subpart numbers. (The get_n_subparts procedure is not defined in the inventory interface definition.)

Dynamic memory allocation for conformant arrays

Suppose the following procedures appear in interface definitions:

procl([in] long size, [in, size_is (size) ] data_t arr[]); proc2([in] long max, [in, max_is(max)] data_t arr[]);

You have to allocate memory for each array needed in the application. To allocate dynamic memory for conformant arrays, use a scheme such as the following:

unsigned long s,m;

data_t *s_arr, *m_arr; /* pointers to some data structures */

/* some application specific constants */ s = SIZE; m = MAX;

I* allocation of the arrays */

s_arr = (data_t *)malloc( (s) * sizeof(data_t) ); m_arr = (data_t *)malloc( (m+1) * sizeof(data_t) );

/* the remote procedure calls */ proc1(s, s_arr); proc2(m, m_arr);

In this example, SIZE is defined in the client to represent an array size and MAX is defined to represent the maximum index value of an array. Notice an array that has the max_is attribute in its interface definition must have an extra array ele ment allocated because arrays begin with an index value of 0.

Memory allocation for conformant structures

Structures containing a conformant array require memory allocation in the client before they are input to a remote procedure call, because a statically allocated conformant structure has storage for only one array element. For example, the fol lowing is the part_list structure of the inventory interface:

typedef struct part_list{

long size;

[size_is(size)] part_num numbers[*] } part_list;

The structure in the header file generated by the MIDI compiler has an array size of only one, as follows:

typedef struct part_list {

unsigned long size;

part_num numbers[1]; } part_list;

The application is responsible for allocating memory for as much of the array as it needs. Use a scheme such as the following to allocate more memory for a confor mant structure:

part_list *c; /* a pointer to the conformant structure */

long s;

s = 33; /* the application specific array size */

c = (part_list *)malloc(sizeof(part_list) + (sizeof(part_num)*(s-1)));

Notice that since the declared structure's size contains an array of one element representing the conformant array, the new memory allocated needs one array ele ment less than the requested array size.

Memory Usage

Distributed applications usually involve more complicated memory management than single-system applications because the address spaces are on separate machines. Fortunately, for many programming situations, Microsoft RPC's default

memory usage method can automate most of the memory management details, freeing programmers to concentrate on the application itself. In the default method, memory on clients and servers is allocated automatically by the stub code for each part of the data structure being stored.

However, while this automation is certainly convenient, it can sometimes result in large stub code and slower performance, especially when the data structures being managed are complex. Consequently, Microsoft RFC offers alternative memory usage methods which can help optimize performance, decrease stub size, or let you tailor your application to specific programming circumstances.

Before we look at specific methods, let's look at the kind of data structures that are passed between clients and servers. Sizeable amounts of data are usually passed between clients and servers as pointers. Simple pointer data can usually be handled by the stub code using the default memory management scheme. But more complex data structures such as linked lists might benefit from the use of alternative memory management methods. Linked lists can be made up of many nodes connected with pointers. The size of a linked list is often variable and mem bers need to be inserted or deleted in the middle easily.

A two-dimensional linked list could represent a sparse array which your applica tion sends to a compute server to be multiplied. Tree structures are a natural form for parsed language data. For example, you might call a "parse server" with a file name and it could return a syntax tree of the data broken down according to grammar rules. Arithmetic expressions are often represented internally in tree form. Graphs of nodes are used in resource allocation problems, usually represent ing networks of computers, of cities, and so on.

In any case, linked lists consist of multiple nodes which must be allocated storage space in both clients and servers. By default, the client and server stub code which marshalls and unmarshalls data uses a crude but effective algorithm to manage the pointers. It makes separate calls to midl_user_allocate and midl_user_free to allo cate and deallocate each individual node in the data structure. While this approach can add stub overhead to the application, it relieves you from having to concern yourself with memory management details.

In addition to the default method, there are three other memory usage methods which you can use by including ACF attributes or by making slight changes to the IDL file. The methods together, are:

• node-by-node allocation and deallocation (the default)

• single buffer allocation

• client application allocated buffers

• persistent storage on the server

Of the four methods, the first two rely solely on the stubs to allocate and free memory while the last two involve the application. In previous chapters we

Microsoft RFC Programming Guide

explained that you must include user-written versions of midl_user_allocate and midl_user_free in both the client and server parts of your application. The reason for this is that the client and server stubs or, in some cases, your application code, calls these procedures to allocate and deallocate memory used by application parameters.

Table 4-2 shows whether the stub code or the application is responsible for mem ory management in each method.

Table 4-2: What Allocates Memory

Client Stub Code Application

Node by node alloca tion and deallocation

user allocate

Server

Stub Code

Application

m idl_ user^free

Single buffer alloca tion

m idl_ user_allocate m idl_ user^free

Client application-allocated buffers

m idl_ user_allocate m idl_ user_Jree

Persistent storage on the server

midl_user_allocate l_ user_Jree

The following sections examine the reasoning behind each memory usage method. The sections also describe how to use ACF attributes to select a method for use with a given situation. All of the alternative (non-default) memory usage methods use attributes that are extensions of DCE IDL. The use of these attributes requires the /ext MIDL compiler switch at compile time.

Node-By-Node Allocation and Deallocation

When you are passing simple pointers back and forth between a client and a server, you needn't worry about choosing a particular memory usage method. The stub code, which marshalls and unmarshalls parameters, will allocate and deallo cate memory for you on both the client and the server.

On the other hand, separate stub calls to midl_user_allocate for each node in a complex linked list can add unnecessary stub overhead to the application. If you

are worried about the overhead, perhaps you could use this method to get your application up and running and then choose another method if you think memory usage is a bottleneck.

Using Contiguous Server Memory

When memory on the server is contiguous, as it ordinarily is with Microsoft Win dows NT, you might increase performance by directing the stub to allocate a sin gle linear buffer for the entire tree or graph.

In this case, the client stub determines the size of the buffer needed by chasing all of the pointers in the structure. This approach relieves the server stubs from mak ing separate calls to midl_user_allocate for each node in the data structure. Because data can be accessed sequentially, memory performance might also be improved by using this technique.

To use this technique, apply the ACF attribute allocate (all_nodes) to the pointer type in a typedef in the ACF file.

/* ACF fragment */

typedef [allocate(all_nodes)] pointer_name;

Allocating Buffers with the Client Application

When you know how big a data structure is, you can specify the buffer size in the client application and pass it to the server as a parameter to the remote procedure. This technique can help minimize the stub size on clients and servers and improve the performance of the affected remote procedure call because the client stub doesn't have to chase pointers. The server stub allocates the buffer space with one call to midl_user_allocate, using the size parameter taken from the remote proce dure call. The runtime library will raise an exception if insufficient memory is allo cated, however. After the call completes, the server stub frees the memory with one call to midl_user_Jree.

The client side can benefit from this technique, too. For instance, say your applica tion has a multiplication interface that multiplies matrixes as in multiplyjnatrix (matrix *ml *m2). -Now let's say that the client makes many calls to this same interface. In this case, it's probably more efficient for the client application to allo cate and control memory directly, reusing the memory that is allocated only once, rather than have the client stubs allocate and free memory with each call.

Even when you know the buffer size, you might not want to take the time to use this technique. But if memory allocation causes a bottleneck in your application, the technique may help.

This method requires two steps. First, add a size parameter to the procedure decla ration in the IDL file, as illustrated in the following IDL file fragment in which we include the parameter cBytes.

Microsoft RFC Programming Guide

I* IDL file function declaration (fragment) */

void GetEmployeeRecord (

[in, string] char EnployeeName [NAMESIZE] ,

[in] short cBytes,

[out, ref] P_RECORD_TYPE pRecord /* record for named employee */ );

Second, in the ACF file, apply the ACF byte_count attribute to the parameter that will store the size of the buffer.

/* ACF file (fragment */

GetEmployeeRecord ( [byte_count (cEytes) ] pRecord ) ;

Now the server stub will make a single call to midl_user_allocate using the cBytes size parameter to allocate memory for this buffer.

Persistent Storage on the Server

Persistent state, or "context," offers a way to manage data on the server so that you can reuse it from call to call, and clean it up properly after you're done with it. One example of persistent state might be a dictionary server or a symbol table server. You pass the server a tree which it saves away, and then you make queries against it later. This technique can save time because your application does not need to copy the same data into a buffer each time it's needed.

To use this method, apply the allocate(dont_free) attribute to the ACF typedef declaration in the ACF file, as in the following usage example.

/* ACF fragment */

typedef [allocate (all_nodes, dont_free) ] pointer_name;

Using this method, the server stub does not call midl_user_/ree when the remote procedure call completes. Instead, the server application must call midl_user_Jree when its procedures are finished using the data structure. To make the parameters available for use by other remote procedure calls on the server, you must copy the pointers to global variables.

In Chapter 7, Context Handles, we'll see a different way of managing server con text through the use of context handle types. While context handles require more programming than the simpler persistent data technique mentioned here, they offer more automatic functions which you may want to use. For instance, context handles track and free memory resources automatically and they can associate server contexts with specific clients.

• Some Background on Call Processing

• Initializing the Server

• Writing Remote Procedures

• Compiling and

Linking Servers HOW tO W^tC d

picture23

RPC servers are more complicated than clients—at least at this introductory stage— because the servers have a more complicated role: they have to be continuously active and be prepared to handle multiple calls in any order. This chapter uses the inventory example as the basis for showing the various issues required by servers.

Before reading this chapter, it's a good idea to read Chapter 1, Overview of an RFC Application, for an overview of a distributed application, and Chapter 2, Using a Microsoft RPC Interface, for features of interface definitions. You should also read Chapter 3, How to Write Clients, to understand how clients use servers.

You write the following two distinct portions of code for all servers:

• Server initialization includes most of the RFC-specific details including RPC runtime routines. This code is executed when the server begins, before it pro cesses any remote procedure calls.

• The manager portion, or remote procedure implementations, include special techniques for memory management.

Some Background on Call Processing

Chapter 1 describes how a typical distributed application works:

• Figure 1-9 shows the initialization steps to prepare a server before it processes remote procedure calls.

• Figure 1-10 shows how a client finds a server using the automatic binding method.

• Figure 1-11 shows the basic steps during a remote procedure call after the client finds the server.

99

Microsoft RFC Programming Guide

To understand server initialization, it is useful at this point to explain how the RFC runtime library handles an incoming call. Figure 5-1 shows how the server system and RFC runtime library handle a client request.

Server Host

Server

RFC

Runtime

Library

picture24

Request Queue

picture25

O Call is placed in request queue

Call from client

Figure 5-1. How the server runtime library handles a call

0 A call request for the server comes in over the network. The request is placed in a request queue for the endpoint. (The server initialization can select more than one protocol sequence on which to listen for calls, and each protocol sequence can have more than one endpoint associated with it.) Request queues temporarily store all requests, thus allowing multiple requests on an endpoint. If a request queue fills, however, the next request is rejected.

© The RFC runtime library dequeues requests one at a time from all request queues and places them in a single call queue. The server can process remote procedures concurrently, using threads. If a thread is available, a call is imme diately assigned to it. (Server initialization can select the number of threads for processing remote procedure calls.) In this figure, only one thread is exe cuting. If all threads are in use, the call remains in the call queue until a thread is available. If the call queue is full, the next request is rejected.

€) After a call is assigned to a thread, the interface specification of the client call is compared with the interface specifications of the server. An interface speci fication is an opaque data structure containing information (including the UUID and version number) that identifies the interface. Opaque simply means

the details are hidden from you. If the server supports the client's interface, processing goes to the stub code. If the server does not support the client's interface, the call is rejected.

When the call finally gets to the stub, it unmarshalls the input data. Unmarshalling involves memory allocation (if needed), copying the data from the RFC runtime library, and converting data to the correct representation for the server system.

Initializing the Server

The server initialization code includes a sequence of runtime calls that prepare the server to receive remote procedure calls. The initialization code typically includes the following steps:

1. Register the interface with the RFC runtime library.

2. Create server binding information by selecting one or more protocol sequences for the RFC runtime library to use in your network environment.

3. Advertise the server location so the clients have a way to find it. A client uses binding information to establish a relationship with a server. Advertising the server usually includes storing binding information in a name service database. Occasionally an application stores server binding information in an application-specific database, or displays it, or prints it.

4. Manage endpoints in a local endpoint map.

5. Listen for remote procedure calls.

During server execution, no remote procedure calls are processed until the initial ization code completes execution. RFC runtime routines are used for server initial ization. (Table B-2 in Appendix B, RFC Runtime Routines Quick Reference, lists all the RFC runtime routines for servers.)

Example 5-1 shows the necessary header files and data structures for server initial ization of the inventory application.

Example 5-1: Server Header Files and Data Structures

I* FILE NAME: server.c */

ftinclude <stdio.h>

#include <stdlib.h>

ftinclude <ctype.h>

#include "inv.h" /* header created by the MIDL compiler O */

#include "status.h" /* contains the CHECK_STATUS macro */

#define STRINGLEN 50

main (argc, argv) int argc; char *argv[]; {

error_status_t status; /* error status (nbase.h) © */

/* RFC vectors @ */

Example 5-1: Server Header Files and Data Structures (continued)

rpc_binding_vector_t *binding_vector; /* binding handle list (rpcdce.h) */ rpc_protseq_vector_t *protseq_vector; /*protocol sequence list(rpcdce.h) */

char entry_name[STRINGLEN]; /* name service entry name */

char annotation[STRINGLEN]; /* annotation for endpoint map */

char hostname[STRINGLEN]; /* used to store the computer name */

EWORD hostname_size=STRINGLEN; /* required by GetComputerName */

/* For the rest of the server initialization, register interfaces, */ /* create server binding information, advertise the server, */

/* manage endpoints, and listen for remote procedure calls. */

O Always include the C language header file (created by the MIDL compiler) from all interfaces the server uses. This file contains the definitions of data types and structures that are needed by the RFC runtime routines.

© An unsigned32 variable is needed to report errors that may occur when an RFC runtime routine is called.

© Some RFC runtime routines use a data structure called a vector. A vector in RFC applications contains a list (array) of other data structures and a count of elements in the list. Vectors are necessary because the number of elements on the list is often unknown until runtime. The rpc_binding_vector_t is a list of binding handles in which each handle refers to some binding information. The list in rpc_protseq_vector_t contains protocol sequence information representing the communication protocols available to a server. RFC runtime routines create vectors, use vectors as input, and free the memory of vectors.

Many header files such as rpc.h and rpcndr.h are included in the interface header inv.h. The rpc.h file in turn has included within it header files such as rpcdce.h, rpcnsi.h, and rpcnterr.h. Many of these header files are associated with RFC-specific interface definitions. These interface definitions contain data structure defi nitions you may need to refer to in order to access structure members and make runtime calls.

Object UUIDs are scattered throughout the RFC runtime routines as parameters for developing certain kinds of applications. You do not need to use object UUIDs to develop many applications so they are not covered in this book.

Registering Interfaces

All servers must register their interfaces so that their information is available to the RFC runtime library. This information is used when a call from a client comes in, so the client is sure the server supports the interface, and the call can be correctly dispatched to the stub.

Before a client makes a call, it checks its interface against the one advertised in the server's binding information. But that does not guarantee that the server supports

the client's interface. For example, it is possible for a complex server to temporar ily suspend support for a specific interface. Therefore, when a remote procedure call arrives, a comparison is made between the client's and server's interface speci fications. If the server supports the client's interface, the RFC runtime library can dispatch the call to the stub.

Use an interface handle to refer to the interface specification in application code. An interface handle is a pointer defined in the C language header file and gener ated by midl. For example, the server interface handle for the inventory applica tion is inv_Vl_0_s_ifspec. The interface handle name contains the following:

• The interface name given in the interface definition header (inv).

• The version numbers in the version attribute (vl_0). If the interface definition has no version declared, version 0.0 is assumed.

• The letter s or c depending on whether the handle is for the server or client portion of the application.

• The word if spec.

The default style of interface names generated by the midl version 2.0 compiler is compatible with names generated by the OSF DCE IDL compiler. Note that the midl version 1.0 compiler generates another form of interface handle name such as inv_ClientIfHandle and inv_Server If Handle. To generate older names that are compatible with midl version 1.0 interface names, you must use the /oldnames option with a midl version 2.0 compiler.

Example 5-2 is a portion of C code that registers one interface.

Example 5-2: Registering an Interface with the Runtime Library

I* The header files and data structures precede registering interfaces. */

/************************** REGISTER INTERFACE ***************************/

status =

RpcServerRegisterlf( /* O */

inv_Vl_0_s_ifspec, /* interface specification (inv.h) */

NULL,

NULL ); CHECK_STATUS(status, "Can't register interface:", ABORT); /* © */

/* For the rest of the server initialization, create server binding */ /* information, advertise the server, manage endpoints, and listen for */ /* remote procedure calls. */

O The RpcServerRegisterlf routine is a required call to register each interface the server supports. The interface handle, inv_Vl_0_s_ifspec, refers to the inter face specification.

© The CHECK_STATUS macro is defined in the status.h file. It is an application-specific macro used in this book to process status values returned from RFC runtime calls (see Example 3-12).

Multiple interfaces may be registered from a single server by calling the RpcSeruer-Registerlf routine with a different interface handle.

The second and third arguments to the RpcServerRegisterlf call are used in com plex applications to register more than one implementation for the set of remote procedures. When only one implementation exists, these arguments are set to NULL. Also, in the event of a symbol name conflict between the remote procedure names of an interface and other symbols in your server (such as procedure names), you can use these arguments to assign different names to the server code's remote procedures.

Creating Server Binding Information

Server binding information is created when you select protocol sequences during server initialization. RFC uses protocol sequences (described in Chapter 3, How to Write Clients) to identify the combinations of communications protocols that RFC supports. Most servers offer all available protocol sequences so that you do not limit the opportunities for clients to communicate with the server.

Recall that besides a protocol sequence, binding information includes a host net work address. A server process runs on only one host at a time, so this binding information is obtained from the system and not controlled in your server code.

When a protocol sequence is selected, an endpoint is also obtained. You have sev eral choices when obtaining endpoints.

Using dynamic endpoints

Chapter 3 describes the difference between dynamic and well-known endpoints. Most servers use dynamic endpoints for their flexibility and to avoid the problem of two servers using the same endpoints. Dynamic endpoints are selected for you by the RFC runtime library and vary from one invocation of the server to the next. When the server stops running, dynamic endpoints are released and may be reused by the server system.

Example 5-3 is a portion of the inventory server initialization showing the selection of one or all protocol sequences and dynamic endpoints. For this example, invoke the server with a protocol sequence argument to select a specific protocol sequence. If you invoke this server without an argument, the server uses all avail able protocols.

Example 5~3-' Creating Server Binding Information

I* Registering interfaces precedes creating server binding information. */

/****************** CREATING SERVER BINDING INFORMATION ******************/ if(argc > 1) { status =

RpcServerUseProtseq( /* use a protocol sequence O */

(unsigned char *)argv[l], /* the input protocol sequence */ RPC_C_PROTSEC_MAX_REQS_DEFAULT, /* (rpcdce.h) */

NULL /* security descriptor (not reqd) */

);

CHEOK_STATUS( status, "Can't use this protocol sequence:", ABORT); } else {

puts ("You can invoke the server with a protocol sequence argument."); status =

RpcServerUseAllProtseqs ( /* use all protocol sequences © */

RPC_C_PROTSEO_MAX_REQS_DEFAULT, /* (rpcdce.h) */

NULL /* security descriptor (not reqd) */

); CHECK_STATUS( status, "Can't register protocol sequences:", ABORT);

status =

RpcServerlnqBindings ( /* get binding information for server©*/

&binding_vector ); CHECK_STATUS ( status, "Can't get binding information:", ABORT);

/* For the rest of the server initialization, advertise the server, */ /* manage endpoints, and listen for remote procedure calls. */

O The RpcServerUseProtseq routine is called with the chosen protocol sequence string. This call selects one protocol sequence on which the server listens for remote procedure calls. For this example, when the server is invoked, argc is the number.of arguments on the command line, and argv[l] is the protocol sequence string argument. The constant RPCjC_PROTSEQjyiAX_C^LLLS_DEFAULT sets the request queue size for the number of calls an endpoint can receive at any given moment.

© The RpcServerUseAllProtseqs routine is called to select all available protocol sequences on which the RFC runtime library listens for remote procedure calls.

© The RpcServerlnqBindings routine is a required call to obtain the set of bind ing handles referring to all of this server's binding information.

Dynamic endpoints must be registered with the server system's local endpoint map using the RpcEpRegister routine, so that clients can look them up when they try to find a server.

Using well-known endpoints

An endpoint is well-known if it is specifically selected and assigned to a single server every time it runs. Well-known endpoints are more restrictive than dynamic endpoints because, in order to prevent your servers from using the same end-points as someone else, you need to register well-known endpoints with the authority responsible for a given transport protocol. For example, the ARPANET Network Information Center controls the use of well-known endpoint values for the Internet Protocols.

Well-known endpoints are often employed for widely-used applications. One server that needs well-known endpoints is the RFC service. This service runs on each system hosting RFC servers, maintaining the database that maps servers to endpoints. When a client has a partially bound handle, and it needs to obtain an endpoint for its application's server, the client RFC runtime library contacts the server system's RFC service. In short, the RFC service is required for finding dynamic endpoints. For clients to contact it, the RFC service itself must have a well-known endpoint.

Although you do not need to register well-known endpoints in the server system's endpoint map, you are encouraged to, so that clients are unrestricted in finding your servers. Use the RpcEpRegister routine to register endpoints in the endpoint map.

Table 5-1 shows the RFC runtime routines that create server binding information with well-known endpoints.

Table 5-1: Creating Binding Information with Well-known Endpoints RFC Runtime Routine Description

RpcServerUseProtseqEp Uses a specified protocol sequence and well-known

endpoint, supplied in application code, to establish server binding information. Even though the endpoint is not dynamically generated, clients do not have an obvious way to get it. So the server must register the endpoint in the server system's endpoint map.

RpcServerUseProtseqlf

Uses a specified protocol sequence, but well-known endpoints are specified in the interface definition with the endpoint attribute. Both clients and servers know the endpoints through the interface definition.

Table 5-1: Creating Binding Information with Well-known Endpoints (continued) RFC Runtime Routine Description

RpcSeruerUseAllProtseqsIf Uses all supported protocol sequences, but well-known endpoints are specified in the interface defini tion with the endpoint attribute. Both clients and servers know the endpoints through the interface def inition.

Advertising the Server

Advertising the server means that you make the binding information available for clients to find this server. You can advertise the server by one of the following methods:

• Export to a name service database.

• Store binding information in an application-specific database.

• Print or display binding information for clients.

The method you use depends on the application, but the most common way is through a name service database. Binding information and the interface specifica tion are first exported to a server entry in the database. The information is associ ated with a recognizable name appropriate for the application. This information can now be retrieved by a client using this name. When the client imports binding information, the RFC runtime library compares the interface specifications of the client and the name service entries, to be sure the client and server are compati ble.

Conventions for naming RFC server entries rely on associating a host computer name with the server entry name, thereby creating a unique server entry name. Unique server entry names allow multiple instances of a server to coexist in one NT domain. Although it's possible for multiple servers to share use of a single server entry, problems arise if the true owner of the entry removes the entry from the name service; binding information for all other servers is removed as well.

Using this convention means that clients that use server entry names to find servers will need to know which computer a server is running on. Automatic clients usually seek servers based on the interface UUID so they are freed from having to know the server's computer name. When NT domains do not contain multiple instances of servers, you don't need to use the convention.

If you plan to store your entry names in DCE CDS, you can also export a group entry name that is not associated with a computer name. The convention for nam ing RFC group entries includes the interface name. The server entry name is added as a member of the group. When the client imports binding information using the group name, the group members are searched until a compatible server entry is found. Microsoft RFC includes the API functions that control group and profile

operations for use with DCE CDS. Note, however, that the Microsoft Locator version 1.0 does not fully support group or profile operations.

Example 5-4 is a portion of the inventory initialization code that uses the name service database to advertise the server.

Example 5-4: Advertising the Server to Clients

I* Registering interfaces and creating server binding information */ /* precede advertising the server. */

/*************************** ADVERTISE SERVER

strcpy(entry_name, "/.:/inventory_");

GetComputerNamet&hostname, &hostname_size);

strcat(entry_name, hostname);

status =

RpcNsBindingExport( /* export to a name service database O */

RPC_C_NS_SYNTAX_DEFAULT, /* syntax of entry name (rpcdce.h) */ (unsigned char *)entry_name, /* name of entry in name service */ inv_Vl_0_s_ifspec / /* interface specification (inv.h) */ binding_vector, /* binding information */

NULL /* no object UUIDs exported */

);

CHECK_STATUS(status, "Can't export to name service database:", RESUME);

/* For the rest of the server initialization, manage endpoints and */ /* listen for remote procedure calls. */

O The RpcNsBindingExport routine exports the server binding information to a name service database. The constant RPC_C_NS_SYNTAX_DEFAULT establishes the syntax the RFC runtime library uses to interpret an entry name. (Microsoft RFC currently has only one syntax.) The entry name is the recognizable name used in the database for this binding information.

The interface handle (inv_Vl_0_s_ifspec) is needed so interface information is associated with the binding information in the name service database. The binding vector is the list of binding handles that represents the binding infor mation exported. (The NULL value represents an object UUID vector. For this application, no object UUIDs are used.)

The RpcNsBindingExport routine exports well-known endpoints to the name ser vice database along with other binding information, but, because of their tempo rary nature, dynamic endpoints are not exported. Performance of the name service will degrade if it becomes filled with obsolete endpoints generated when servers restart. Also, clients will fail more often trying to bind to servers of nonexistent endpoints. Since dynamic endpoints are not in a name service database, clients need to find them from another source. The next section discusses how to manage endpoints.

Managing Server Endpoints

When the server uses dynamic endpoints, clients need a way to find them, because neither the name service database nor the interface specification store dynamic endpoints. The endpoint map is a database on each RFC server system that associates endpoints with other server binding information. As a general rule, have your server store all endpoints (dynamic and well-known) in the endpoint map. If all endpoints are placed in the endpoint map, system administrators have an easier time monitoring and managing all RFC servers on a host system.

When a client uses a partially bound binding handle for a remote procedure call, the RFC runtime library obtains an endpoint from the server system's endpoint map. (However, if a well-known endpoint is available in the interface specifica tion, the server's endpoint map is not used.) To find a valid endpoint, the client's interface specification and binding information (protocol sequence, host, and object UUID) are compared to the information in the endpoint map. When an end-point of an appropriate server is finally obtained, the resulting fully bound binding handle is used to complete the connection at that endpoint. Example 5-5 shows how a server registers its endpoints in the endpoint map.

Example 5-5: Managing Endpoints in an Endpoint Map

/* Registering interfaces, creating server binding information, and */ /* advertising the server precede managing endpoints. */

/*************************** MANAGE ENDPOINTS ****************************/

strcpy(annotation, "Inventory interface");

status =

RpcEpRegister( /* add endpoints to local endpoint map O */

inv_Vl_0_s_ifspec, /* interface specification (inv.h) */ binding_vector, /* vector of server binding handles */

NULL, /* no object UUIDs to register */

(unsigned char *)annotation /* annotation supplied (not required) */

);

CHECK_STATUS(status, "Can't add endpoints to local endpoint map:", RESUME);

status =

RpcBindingVectorFree'( /* free server binding handles©*/

&binding_vector ); CHECK_STATUS(status, "Can't free server binding handles:", RESUME);

open_inventory (); /* application specific procedure */

Example 5-5: Managing Endpoints in an Endpoint Map (continued)

I* For the rest of the server initialization, listen for remote */ /* procedure calls. */

O The RpcEpRegister routine registers the server endpoints in the local endpoint map. Use the same interface handle, binding vector, and object UUID vector as you used in the RpcNsBindingExport routine (see Example 5-4). An annota tion argument is not needed because Microsoft RFC provides no way to retrieve this information from the endpoint map.

© The RpcBindingVectorFree routine is a required call that frees the memory of the binding vector and all binding handles in it. Each call to RpcServerlnq-Bindings (see Example 5-3) requires a corresponding call to RpcBinding VectorFree. Make this call prior to listening for remote procedure calls, so the memory is available when remote procedure calls are processed.

The RpcEpRegister call is required if dynamic endpoints are established with the RpcServerUseProtseq or RpcServerUseAllProtseqs runtime routines, because each time the server is started, new endpoints are created (see Example 5-3). If well-known endpoints are established with the RpcServerUseProtseqEp runtime routine, you should use the RpcEpRegister routine, because even though the endpoint may always be the same, a client needs to find the value. If well-known endpoints are established with the RpcServerUseProtseqlf or RpcServerUseAllProtseqsIf call, they need not be registered, because the client has access to the endpoint values through the interface specification.

When a server stops running, endpoints registered in the endpoint map become outdated. The RFC service maintains the endpoint map by removing outdated end-points. However, an unpredictable amount of time exists in which a client can obtain an outdated endpoint. If a remote procedure call uses an outdated end-point, it will not find the server and the call will fail. To prevent clients from receiving outdated endpoints, use the RpcEpUnregister routine before a server stops executing.

The only way to actively manage endpoints in the endpoint map is by using Rpc EpRegister and other RFC runtime routines in the server initialization code (see Example 5-5).

Listening for Remote Procedure Calls

The final requirement for server initialization code is to listen for remote proce dure calls.

Many of the RFC runtime routines used in this book have an error status variable, used to determine whether the routine executed successfully. However, when the server is ready to process remote procedure calls, the RpcServerListen runtime rou tine is called. The RpcServerListen runtime routine does not return unless the

server is requested to stop listening by one of its own remote procedures using the RpcMgmtStopServerListening routine.

Any errors occurring during stub code or remote procedure execution are reported as exceptions, and, unless your code is written to handle exceptions, it will abruptly exit. You can use a set of RFC macros to help process some system exceptions that occur outside the application code. The macros RpcTryExcept, RpcExcept, and RpcEndExcept delineate code sections in which exceptions are controlled. If an exception occurs during the RpcTryExcept section, code in the RpcExcept section is executed to handle any necessary error recovery or cleanup such as removing outdated endpoints from the endpoint map.

These macros are not likely to be invoked when exceptions occur within the server application code itself; exceptions within a server usually cause the server to abort before the exceptions are reported back to the application.

The RpcExcept section contains clean-up code that does such things as remove outdated endpoints from the endpoint map. The RpcTryExcept and RpcExcept sections end with the RpcEndExcept macro.

Example 5-6 is a portion of C code that shows how the inventory server listens for remote procedure calls and handles exceptions.

Example 5-6: Listening for Remote Procedure Calls

I* Registering interfaces, creating server binding information, */

/* managing endpoints, and advertising the server precede listening */

/* for remote procedure calls. */

/***************** LISTEN FOR REMOTE PROCEDURE CALLS *****************/ RpcTryExcept /* thread exception handling macro O */

{

status =

RpcServerListen( /* © */

1, /* process one remote procedure call at a time */

RPC_C_LISTEN_MAX_CALLS_DEFAULT, NULL );

CHECK_STATUS(status, "rpc listen failed:", RESUME);

} «

RpcExcept (RpcExceptionCodeO) /* error recovery and cleanup */ {

close_inventory(); /* application specific procedure */

status =

RpcServerlnqBindings( /* get binding information © */

&binding_vector ); CHECK_STATUS(status, "Can't get binding information:", RESUME);

status =

RpcEpUnregister( /* remove endpoints from local endpoint map O */ inv_Vl_0_s_ifspec, /* interface specification (inventory.h) */ binding_vector, /* vector of server binding handles */

NULL /* no object UUIDs */

Example 5-6: Listening for Remote Procedure Calls (continued)

);

CHECK_STATUS(status, "Can't remove endpoints from endpoint map:", RESUME);

status =

RpcBindingVectorFree( /* free server binding handles © */

&binding_vector ); CHECK_STATUS(status, "Can't free server binding handles:", RESUME);

puts("\nServer quit!"); }

RpcEndExcept; } /* END SERVER INITIALIZATION */

O The RpcTryExcept macro begins a section of code in which you expect exceptions to occur. For this example, the RpcTryExcept section contains only the RpcServerListen routine. If an exception occurs during the remote procedure execution, the code section beginning with the RpcExcept macro is executed to handle application-specific cleanup.

@ The RpcServerListen routine is a required call that causes the runtime to listen for remote procedure calls. The first argument sets the number of threads the RFC runtime library uses to process remote procedure calls. In this example, the RFC runtime library can process one remote procedure call at a time. If your remote procedures are not thread safe, set this value to 1.

© The RpcServerlnqBindings routine obtains a set of binding handles referring to all of the server's binding information.

O The RpcEpUnregister routine removes the server endpoints from the local end-point map. If the server registered endpoints with a call to RpcEpRegister, this call is recommended before the process is removed (see Example 5-5).

© The RpcBindingVectorFree routine is called to free the memory of a binding vector and all binding handles in it. Each call to RpcServerlnqBindings requires a corresponding call to RpcBindingVectorFree.

The server initialization code for the inventory application is now complete. All of the server initialization code is shown in Example D-5. Table B-2 lists all the run time routines that servers can use.

Writing Remote Procedures

When writing your remote procedures, consider the issues of memory manage ment, threads, and client binding handles.

Remote procedures require special memory management techniques. Suppose a procedure allocates memory for data that it returns to the calling procedure. In a local application, the calling procedure can free allocated memory because the procedure and calling procedure are in the same address space. However, the

client (calling procedure) is not in the same address space as the server (remote procedure), so the client cannot free memory on the server. Repeated calls to a remote procedure that allocates memory, without some way to free the memory, will obviously waste the server's resources.

You must manage memory for remote procedures by calling programmer-supplied wrapper routines for malloc and free in remote procedures. These routines enable the server stub to free memory allocated in remote procedures, after the remote procedure completes execution.

Recall that the RpcServerListen routine in server initialization determines the num ber of threads a server uses to process remote procedure calls. If the server listens on more than one thread, the remote procedures need to be thread safe. For example, the remote procedures should not use server global data unless locks are used to control thread access. In the inventory application, when reading from or writing to the inventory application database, a lock may be needed so data is not changed by one thread while another thread is reading it. The topic of multi threaded application development is beyond the scope of this book.

So far, we have used server binding handles and server binding information to allow clients to find servers. When a server receives a call from a client, the client RFC runtime library supplies information about the client side of the binding to the server RFC runtime library. Client binding information is used in server code to inquire about the client. This client binding information includes:

• The RFC protocol sequence used by the client for the call.

• The network address of the client.

• The object UUID requested by the client. This can be simply a nil UUID.

To access client binding information in remote procedures use a client binding handle. If the client binding handle is available, it is the first parameter of the remote procedure. If you require client binding information, the procedure decla rations in the interface definition must have a binding handle as the first parame ter. No further details of client binding information are described in this book.

Managing Memory in Remote Procedures

In typical applications, you use the C library routines, malloc and free, or your own allocation scheme, to allocate and free memory that pointers must refer to. In RFC servers, when implementing a remote procedure that returns a pointer to newly allocated memory to the client, use programmer-supplied wrapper routines to malloc and free to manage memory in the remote procedures. The routines, which are named midl_user_allocate and midl_user_jree, are also called by the stub code to allocate and free memory.

Example 5-7 shows how you can write the wrapper routines for malloc and free. Example 5- 7: Programmer-Supplied Wrapper Routines for malloc and free

/*** midl_user_al locate / midl_user_free ***/

void * _RPC_API midl_user_al locate

size_t size; {

unsigned char * ptr;

ptr = malloc ( size ) ;

return ( (void *)ptr ); }

void _RPC_API midl_user_free (

ob j ect )

void * object; {

free (object) ; }

Use the midl_user_allocate routine instead of the C library routine malloc, so bookkeeping is maintained for memory management. This also ensures that mem ory on the server is automatically freed by the server stub after the remote proce dure has completed execution. Memory allocation will not accumulate on the server and get out of control.

For reference pointers, memory on the client side must already exist, so no mem ory management is required for remote procedures whose output parameters are reference pointers. After you make the remote procedure call, first the server stub automatically allocates necessary memory and copies the data for the reference pointer into the new memory. Then it calls the implementation of the remote pro cedure. Finally, the remote procedure completes, output data is transmitted back to the client stub and the server stub frees the memory it allocated.

On both the client and server, more complex memory management occurs for unique pointers than for reference pointers. If a remote procedure allocates mem ory for an output parameter, the server stub copies and marshalls the data, then the stub frees the memory that was allocated in the remote procedure. When the client receives the data, the client stub allocates memory and copies the data into the new memory. It is the client application's responsibility to free the memory allocated by the client stub.

Example 5-8 shows how to use the midl_user_allocate routine to allocate memory for unique pointers. The procedure get_part_description of the inventory

application returns a string of characters representing the description of a part in the inventory. The call in the client is as follows:

part_record part; /* structure for all data about a part */

part.description = get_part_description(part.number);

Example 5-8: Memory Management in Remote Procedures

paragraph get_part_description(number) part_num number;

part_record *part; /* a pointer to a part record */

paragraph description;

int size;

char *strcpy();

if( read_part_record(number, &part) ) {

/* Allocated data that is returned to the client must be allocated */ /* with the midl_user_allocate routine. */

size = strlen((char *)part->description) + 1; /* O */

description = (paragraph)midl_user_allocate((unsigned)size); /* © */ strcpy((char *)description, (char *)part->description);

else

description = NULL; return(description);

O An additional character is allocated for the null terminator of a string.

© The remote procedure calls the midl_user_allocate stub support routine to allocate memory in the remote procedure.

When the procedure completes, the server stub automatically frees the memory allocated by midl_user_a I locate calls. When the remote procedure call returns, the client stub automatically allocates memory for the returned string. When the client application code is finished with the data, it frees the memory allocated by the client stub as follows:

if(part.description != NULL) free(part.description);

For more complex memory management, there is a programmer-supplied counter part to the C library routine free called midl_user_Jree.

The only time you don't use the midl_user_allocate and midl_user_free routines for memory management is when you use context handles. Memory allocated for context on the server must not use these routines because subsequent calls by the client must have access to the same context as previous calls. See Chapter 7 for more information on context handles.

Allocating Memory for Conformant Arrays

The whatare_subparts procedure of the inventory application allocates memory for a conformant array in a structure, and returns a copy of the conformant structure to the client. The whatare_subparts procedure is declared in the interface definition as follows:

typedef struct part_list{ /* list of part numbers */

long size; /* number of parts in array */

[size_is(size)] part_num numbers[*]; /* conformant array of parts */

} part_list;

void whatare_subparts( /* get list of subpart numbers for a part */

[in] part_num number,

[out] part_list **subparts /* the structure containing the array */ );

Output pointer parameters are reference pointers, which must have memory allo cated in the client prior to the call. Therefore, you need a unique pointer in order for new memory to be automatically allocated by the client stub for the **sub-parts structure when the whatarejsubparts procedure returns. A pointer to a pointer is required so that the reference pointer points to a full pointer, which in turn points to the structure.

Example 5-9 shows how to allocate memory in the remote procedure for a confor mant structure. The call in the client is as follows:

part_record part; /* structure for all data about a part */

part_list *subparts; /* pointer to parts list data structure */

whatare_subparts(part.number, &subparts);

Example 5~9: Conformant Array Allocation in a Remote Procedure

void whatare_subparts(number, subpart_ptr) part_num number; part_list **subpart_ptr; {

part_record *part; /* pointer to a part record */

int i;

int size;

read_part_record(number, &part);

/* Allocated data that is output to the client must be allocated with */ /* the midl_user_allocate stub support routine. Allocate for a part_list */ /* struct plus the array of subpart numbers. Remember the part_list */ /* struct already has an array with one element, hence the -1. */

size = sizeof(part_list)

+ (sizeof(part_num) * (part->subparts.size-l)); /* O */

Example 5-9: Conformant Array Allocation in a Remote Procedure (continued)

*subpart_ptr = (part_list *)midl_user_allocate((unsigned)size); /* © */

/* fill in the values */

(*subpart_ptr)->size = part->subparts.size;

for(i =0; i < (*sutpart_ptr)->size; i++)

(*subpart_ptr)->numbers[i] = part->subparts.numbers[ i ] ; return; }

O The allocated memory includes the size of the conformant structure plus enough memory for all the elements of the conformant array. The conformant structure generated by the MIDL compiler already has an array of one element, so the new memory allocated for the array elements is one less than the num ber in the array.

© Use the RFC stub support routine midl_user_allocate to allocate memory so bookkeeping is maintained for memory management, and so the server stub automatically frees memory on the server after the remote procedure com pletes execution.

When the data for the conformant structure is returned to the client, the client stub allocates memory and copies the data into the new memory. The client application code uses the data and frees the memory allocated, as follows:

for(i =0; i < subparts->size; i++)

printf("%ld ", subparts->numbers[i]);

printf("\nTotal number of subparts:%ld\n", subparts->size); free(subparts); /* free memory allocated for conformant structure */

Compiling and Linking Servers

Figure 5-2 shows the files and libraries required to produce an executable server. When complex data types are used, the MIDL compiler produces the server stub auxiliary file (appl^y.c) when the interface is compiled. The auxiliary file contains data marshalling procedures that can be used by other interfaces. No stub auxiliary files are produced for the inventory application. Example 5-10 shows the portion of a makefile that:

• Compiles the C language stubs and server code along with the header file producing server object files.

• Links the server object files to produce the executable server file.

Microsoft RFC Programming Guide

Write server application files containing initialization code and the remote procedures.

Include the header files produced by interface compilation.

Generate client application and stub object files.

Use the server stub and auxiliary files produced by compilation.

Create the executable server file by linking the server application, stub, and auxiliary object files with the Microsoft RFC library.

Text Editor

picture26

Linker

server

Figure 5-2. Producing a server

Example 5-10: Using a Makefile to Compile and Link a Server

# FILE NAME: Makefile

# Makefile for the inventory application #

# definitions for this make file #

APPL=inv

NTRPCLIBS=rpcrt4.lib rpcns4.1ib libont.lib kerne!32.1ib

!include <ntwin32.mak>

## NT c flags

cflags = -c -WO -Gz -D_X86_=1 -CWIN32 -EMT /nologo

Example 5~10: Using a Makefile to Compile and Link a Server (continued)

## NT nmake inference rules

$(cc) $(cdebug) $(cflags) $(cvarsmt) $< $(cvtomf)

#

# SERVER BUILD #

server: server.exe

server.exe: server.obj manager.obj invntry.obj $(APPL)_s.obj $(APPL)_x.obj $(link) $(linkdebug) $(conflags) -out:server.exe -map:server.map \

server.obj manager.obj invntry.obj $(APPL)_s.obj $(APPL)_x.obj\

$(NTRPCLIBS)

# client and server sources client.obj: client.c $(APPL).h manager.obj: manager.c $(APPL).h server.obj: server.c $(APPL).h invntry.obj: invntry.c $(APPL).h

In this Chapter:

• Naming

• DefaultEntry

• Server Entries

• Some Rules for Using the Microsoft Locator

picture27

picture28

Using a Name Service

We have seen in earlier chapters that clients query a name service to find a host where a server is running. We have set up our environment in a simplistic, if not inconvenient, manner so that we could avoid discussing details about the name service. For instance, in Chapter 1, Overview of an RFC Application, our arithmetic server used a simple server entry name. While this simple name is easy to create and use, it makes it difficult for an NT domain to accommodate other identical servers because they'll all be exporting their binding information to the same entry name in the name service.

In a production environment, you don't want to restrict the number of servers you can have in a domain. That would defeat the purpose of the name service, which is to allow servers to be moved, added, and removed without affecting end-users.

In this chapter, we discuss how to use a name service to provide multiple servers in your domain, which increases reliability and availability. You accomplish this by giving a server different names when it runs on different hosts. This is necessary because each server should be uniquely identified in the name service. Towards the end, we discuss some rules and caveats for using the Microsoft Locator.

In DCE, the Cell Directory Service uses group entries and profile entries as a way to organize servers and control a client's search for server entries. The Microsoft Locator Version 1.0 does not support the use of CDS group entries or profile entries. However, Microsoft RFC includes group and profile routines in its runtime library for compatibility with DCE, for situations when you are running on a net work where other machines have DCE and you want to store binding information in CDS.

Naming

Microsoft RFC supplies the Locator as the name service used by Microsoft RFC applications to locate servers. Servers store their binding information in the Loca tor's RFC name service database where it can be retrieved by clients. Clients then use the binding information to connect to servers.

Because so many servers can exist in a Locator's domain, the collection of names is hierarchically organized. In DCE, this hierarchy really corresponds to the way that the name service stores the entries in its distributed database. But with the Microsoft Locator, the names are simply strings. The hierarchy is merely an appear ance, just for the convenience of the users—for instance, so that related servers can have similar names. But it is still useful.

Here are examples of entry names in the Locator:

/.../manufacturing/services/graphics/servers/gif_server /.:/services/graphics/servers/gif_server

Names like those in the example can help organize servers logically so clients can find them by using consistent naming patterns.

The above example shows two ways to name a server. The first example includes the name of the domain, manufacturing, as part of the name. The second exam ple avoids using the / . . . /manufacturing prefix by beginning the name with /. :. The domain name prefix is implicit because a Locator maintains entry names only from its own domain—in this case, the manufacturing domain. The second example allows server portability across domains that use similar naming conven tions.

When a server starts, it can export its name to the Locator database along with its protocol sequence and host address. Unlike DCE, Microsoft RFC does not provide independent tools for administrators to manage the entries. Consequently, your applications must do any needed entry management.

DefaultEntry

Recall that if you use automatic binding, the client stub finds a server for you. By default, a client searches the Locator for a server offering an interface with a matching interface UUID. You can override this behavior by setting the Default-Entry Windows NT registry value to a valid server entry name.

You can set the DefaultEntry on Windows NT client using the regedt32 program. In the HKEY_LOCAL_MACHINE on Local Machine window, you should select SOFTWARE/Microsoft/I^>c/NameService.

If the DefaultEntry value exists in the right portion of the window, double click on it to invoke a dialog box for typing in the server entry name. Type in the name and then click on OK.

Chapter 6: Using a Name Service

123

If the Def aultEntry value does not exist, you can add it by pulling down the Edit menu and clicking on Add Value .... In the Value Name field, type Def aultEntry. Then click on OK. In the resulting String dialog box, type in the server entry name and then click on OK.

You can set the Def aultEntry on Microsoft DOS and Windows 3.1 clients by using a text editor to add a line like the following to the C:\RPCREG.DAT configu ration file.

\Root\Software\Microsoft\Rpc\NameService\DefaultEntry=/.:/arithmetic_RIGEL.

Server Entries

A name service server entry stores binding information for an RFC server. Figure 6-1 depicts server entries in the name service database.

Server entry

Interface identifier Binding information

Server entry

Interface identifier Binding information

Figure 6-1. Server entries in the name service database

A server entry contains the following information:

• An interface identifier consists of an interface UUID and a version number. During the search for binding information, RFC name service routines use this identifier to determine if a compatible interface is found.

• Binding information is the information a client needs to find a server. It includes one or more sets of protocol sequence and host address combina tions. Well-known endpoints can also be part of the binding information, but dynamic endpoints cannot.

• Some applications use optional object UUIDs to identify application-specific objects or resources.

A reasonable naming scheme for server entries combines the host system name and a meaningful definition of what the server offers. For example, the arithmetic interface on a host system named RIGEL can have the following name service entry:

/. : /arithmetic_RIGEL

In this way, using a simple convention that all servers can follow, you are assured that each server at your site has a unique name—as long as you have only one server per host. Normally, a host should only provide one server for an interface. You can increase the number of clients a server handles by increasing the number of threads a server can spawn. In the unusual case in which your system has mul tiple servers offering the same interface, you need to distinguish each server with separate name service entries and unique entry names. For example, one server might be /.: /arithmeticl_RIGEL, and another /.: /arithmetic2_RIGEL.

If you structure your entry names to included embedded host names, using the host name again in the right-most part of the name is redundant. In this case, the arithmetic application might have the following entry name:

/.: /product_developnent/test_servers/host_RIGEL/arithmetic

When your client uses the name service to find a server, it does an import or lookup for binding information, starting at an entry name known to be in the database. Entry names must be supplied to you in one of two ways: by the name service administrator who knows the name service database organization, or by the server administrator. You use RFC name service routines to search the name service database. These routines compare the client's interface identifier with inter face identifiers in the database. When there is a match and the entry contains com patible binding information, the compatible binding information is returned.

Figure 6-2 shows how the arithmetic application uses a server entry in the name service database. The arithmetic server uses the RpcNsBindingExport runtime rou tine to export binding information to the /.: /arithmetic_RIGEL server entry. The arithmetic server's use of RpcNsBindingExport is shown in Example 1-4 in Chapter 1.

The arithmetic client uses the automatic binding method, so the client stub finds the server without using the server entry name. Instead, the automatic client requests a binding for an interface with a matching interface UUID. When client application code assists the search, as when using implicit or explicit binding methods, you can set a programmer-supplied environment variable such as ARITHMETIC_SERVER_ENTRY, to something like /.: /arithmetic_RIGEL on the client system SIRIUS, so the client stub has a name to search for in the name ser vice. In this example, the name service simply searches for the server entry name

Chapter 6: Using a Name Service

125

I.: /arithmetic_RIGEL. The server entry's binding information is returned, and the remote procedure call is completed.

System SIRIUS

picture29

Import

System RIGEL

picture30

Arithmetic Server

O Export

Figure 6-2. A simple use of a name service database

Creating a Server Entry and Exporting Binding Information

Microsoft RFC offers flexible ways for servers to construct server entry names. A name can be hard-coded in the server itself, but this method makes it difficult to change a server name because the server must be recompiled. To make your server more portable, you can specify a server name outside the program by set ting an environment variable used by the server. For instance, you can use a batch file to set a server-specific environment variable and then start the server.

@REM FILE NAME: arith.bat

©ECHO OFF

set ARITHMETIC_SERVER_ENTRY=/.:/arithmetic_rigel

server

The server constructs the entry name using the WIN32 API getenv routine to read in the environment variable. The server then uses the NSI routine RpcNsBinding-

Export to export binding information to the name service entry. If an entry does not already exist, the Locator creates one for you.

entry_name = (unsigned char *)getenv("ARITHMETIC_SERVER_ENTRY");

status =

RpcNsBindingExport ( /* export entry to name service database */

RPC_C_NS_SYNTAX_DEFAULT, /* syntax of the entry name (rpcdce.h) */

entry_name, /* entry name for name service */

arith_ServerIfHandle, /* interface specification (arith.h) */

binding_vector, /* the set of server binding handles */ NULL

);

CHECK_STATUS(status, "Can't export to name service database", ABORT);

Alternatively your server can use the WIN32 API getcomputername routine to read the computer name and append it to a string that is associated with the ARITH-METIC_SERVER_ENTRY environment variable. This method can make servers even more portable because you don't have to modify the .BAT file if you move the server to a different host.

EWDRD hostname_size=STRINGLEN; /* required by GetComputerName */

strcpy(entry_name, "ARITHMETIC_SERVER_ENTRY");

GetComputerName(khostname, &hostname_size);

strcat(entry_name, hostname);

status =

RpcNsBindingExport( /* export to a name service database */

RPC_C_NS_SYNTAX_DEFAULT, /* syntax of entry name (rpcdce.h) */ (unsigned char *)entry_name, /* name of entry in name service */ inv_ServerIfHandle, /* interface specification (inv.h) */ binding_vector, /* binding information */

NULL /* no object UUIDs exported */

);

CHECK_STATUS(status, "Can't export to name service database:", RESUME);

If you expect the server to be removed from service for a long period of time or even permanently, you should remove the server binding information from the name service using the RpcNsBindingUnexport runtime routine.

Some Rules for Using the Microsoft Locator

When your Windows NT domain is large and contains several Advanced Servers, the Locator does not always work smoothly. Changes to the database can some times result in inconsistencies.

A Windows NT domain is a group of users and their systems sharing common security and administration. A domain consists of one domain controller which maintains the master copy of the domain's user and group database. The controller also stores the master copy of the Microsoft Locator.

Domains should also contain one or more Windows NT Advanced Servers which maintain copies of the master databases stored on the domain controller. The Win dows NT Advanced Servers in the domain query the domain controller every five minutes asking whether changes have been made. The controller sends just the changes to the requesting server, minimizing network traffic. If the domain con troller becomes unavailable for some reason, for example it crashes, a Windows NT Advanced Server in the domain is promoted to be the new domain controller. If the new controller's RFC name service database is not up-to-date, missing entries must be re-exported by their servers to the new controller. Consequently, out-of-date data tends to stay out of date.

Domain controllers also maintain group entry information while Windows NT Advanced Servers do not. When a server is promoted to be the new domain con troller, group entries that existed before the promotion are lost. Consequently, Microsoft encourages users to rely on server entries rather than group entries.

The domain controller and Windows NT Advanced Servers maintain the RFC name service database in transient memory rather than in a file. This model cannot guar antee the integrity of the database structure. If the domain controller crashes, all unpropagated server entries and all group entries must be reconstructed.

picture31

In this Chapter:

• The Remote _flle Application

• Declaring Context in an Interface Definition

• Using a Context Handle in a Client

• Managing Context in a Server

picture32

Some applications require that a server maintain information between remote pro cedure calls. This is called maintaining context (or maintaining state). Global data is one way a local application can maintain information between procedure calls. In a distributed application, however, the client and server are in different address spaces, so the only data common to each are passed as parameters. Even if a set of remote procedures use server global data, there is nothing to prevent more than one client from making calls that modify the data. A context handle is the mecha nism that maintains information on a particular server for a particular client. An active context handle refers to valid (non-null) context, and includes binding infor mation because a specific server maintains information for a particular client.

The Remote_file Application

The rfile application is a simple file transfer example that copies text from the client to the server. A client uses a context handle to refer to server context. The server context is the file handle used by remote procedures to open, write, and close the file. In this application, the filename on the server may be the same or different from the filename on the client, but the server does not overwrite an existing file on the server system.

If you do not select any filenames, this application uses standard input (stdiri) of the client and standard output (stdouf) of the server to transfer a message from the client to the server. The complete rfile application is shown in Appendix E, The Rfile Application.

Microsoft RFC Programming Guide

Declaring Context in an Interface Definition

A file handle in a local application is analogous to a context handle in a dis tributed application. The information a file handle refers to is maintained by the C library and the operating system, not your application. You call some library rou tines to open or close the file, and other routines to read from or write to the file.

A context handle is maintained by the stubs and RFC runtime library, not by your application code. What you have to write is a remote procedure that returns an active context handle, and one that frees the context when you are finished with it. Other remote procedures can access or manipulate the active context.

Example 7-1 shows how to define context handles in the rfile interface defini tion.

Example 7-1: Defining Context Handles

I* FILE NAME: rfile. idl */ [

uuid(A61E4024-A53F-101A-BLAF-08002B2E5B76), version (1.0) , pointer_default (unique) ]

interface rfile /* file manipulation on a remote system */

{

typedef [context_handle] void *filehandle; /* O */

typedef byte bufferf];

filehandle remote_open( /* open for write €) */

[in] handle_t binding_h, /* explicit primitive binding handle */

[in, string] char name[], /* if name is null, use stdout in server */

[in, string] char mode[] /* values can be "r", "w", or "a" */

);

long remote_send(

[in] filehandle fh, /* © */

[in, max_is(max)] buffer buf,

[in] long max );

void remote_close (

[in, out] filehandle *fh /* O */

To define a context handle data type, apply the context_handle attribute to a void * type (or a type that resolves to void *) in a type definition. If the client-server communication breaks down or the client fails, a context handle data type allows the server to automatically clean up the user-defined context with a call to a context rundown procedure. If a context handle is applied in a type definition, then the server application developer must write a context rundown procedure.

@ At least one remote procedure initializes the context handle and returns it to the client for later use. A procedure returning a context handle result always returns a new active context handle. Also, if a parameter is an out-only con text handle, the procedure creates a new active context handle.

© A procedure with a context handle parameter that is input only must use an active context handle.

0 When the client application is finished with the server context, the context must be freed.

If the context handle is null upon return from a procedure, the remote procedure on the server has freed the context and the client stub has freed the context han dle. A remote procedure that frees a context handle requires the parameter to have the in directional attribute so the server can free the context, and the out direc tional attribute so the client stub can also free the client's copy of the context han dle.

Using a Context Handle in a Client

The client uses a context handle to refer to the server context through the remote procedure calls. In the client, the context handle refers to an opaque structure. This means that the data is hidden and cannot be manipulated by the client appli cation code. The context handle can be tested for null, but not assigned any val ues by the client application. The server code accomplishes all context modification, but the status of the context is communicated to the client through the context handle. The client stub manipulates the context handle in the client on behalf of the server. Example 7-2 shows a typical sequence of remote procedure calls when using context handles.

Example 7-2: Using a Context Handle in a Client

/* FILE NAME: client.c */

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include "rfile.h"

#define MAX 200 • • /* maximum line length for a file */

main(argc, argv)

int argc;

char *argv[];

{

FILE *local_fh; /* file handle for client file input */

char host[100]; /* name or network address of remote host */ char remote_name[100]; /* name of remote file */

rpc_binding_handle_t binding_h; /* binding handle */

filehandle remote_fh; /* context handle */

buffer *buf_ptr; /* buffer pointer for data sent */

int size; /* size of data buffer */

get_args(argc, argv, &local_fh, host, (char *)remote_name);

Microsoft RFC Programming Guide

Example 7-2: Using a Context Handle in a Client (continued)

#ifndef LOCAL

if (do_string_binding(host, &binding_h) < 0) { /* O */

f print f (stderr, "Cannot get binding\n" ) ;

exit(l); } ttendif

remote_fh = remote_open(binding_h, remote_name, (char *)"w"); /* © */ if (remote_fh == NULL) {

fprintf (stderr, "Cannot open remote file\n");

exit(l);

/* The buffer data type is a conformant array of bytes; */ /* memory must be allocated for a conformant array. */ buf_ptr = (buffer *)nialloc( (MAX+1) * sizeof (buf f er) ) ;

while ( fgets((char *)buf_ptr, MAX, local_fh) != NULL) {

size = (int) strlen( (char *)buf_ptr) ; /* data sent will not include \0 */ if( remote_send(remote_fh, (*buf_ptr) , size) < 1) { /* © */

fprintf (stderr, "Cannot write to remote file\n"); exit(l) ;

remote_close(&remote_fh) ; /* O */

}

O Before a context handle becomes valid, a client must establish a binding with the server that will maintain the context. For the explicit or implicit binding methods, your application has to perform this step. For the automatic binding method, binding occurs in the client stub during the first remote procedure call. Then, to find the server after the context handle is established, subse quent calls use it instead of a binding handle. The do_string_binding proce dure is an application-specific procedure that creates a binding handle from a host input and a generated protocol sequence. It is shown in Chapter 3, How to Write Clients.

The symbol LOCAL is used in applications in this book, to distinguish compil ing this client to test in a local environment, from compiling to run in a dis tributed environment.

© To establish an active context handle, a procedure must either return the con text handle as the procedure result or have only the out directional attribute on a context handle parameter. The context handle cannot be used by any other procedure until it is active. For the remote_open procedure, an explicit binding handle is the first parameter.

© Procedures using only an active context handle can be employed in any way the application requires. Note that for a procedure to use the context handle, a context handle parameter must have at least the in attribute. The remote_send procedure sends a buffer of text data to the server, where the remote procedure writes the data to the file referred to by the context handle.

Chapter 7: Context Handles

133

O When you have finished with the context, free the context handle to release resources.

Binding Handles and Context Handles

A procedure can use a binding handle and one or more context handles. How ever, make sure all handles in the parameter list refer to the same server because a remote procedure call cannot directly refer to more than one server at a time.

Table 7-1 shows how to determine whether a binding handle or a context handle directs the remote procedure call to the server.

Table 7-1: Binding Handles and Context Handles in a Call

Other Procedure Format Parameters

proc( . . . )

proc([in] bh . . . )

proc( . . . [in]ch . . .

proc( . . . [in,out]ch

No binding or con text handles

May include context handles

May include other context handles but no binding handles

May include other input/output or out put-only context handles but no binding handles or input-only context handles

Handle that Directs Call

The interface-wide auto matic or implicit binding handle directs the call. The explicit binding handle, bh, directs the call.

The first context handle that is an input-only pa rameter directs the call. If it is null, the call will fail.

The first non-null con text handle that is an in put/output parameter di rects the call. If all are null, the call will fail.

Managing Context in a Server

When more than one remote procedure call from a particular client needs context on a server, the server stub and server application maintain the context. This sec tion describes how to implement the procedures that manipulate context in a server.

A server context handle refers to context in the server code. It communicates the status of the context back to the client. From the perspective of the server

developer, a server context handle is an untyped pointer that can be tested for null, assigned null, or assigned any value.

Once the server context handle is active (non-null), the server maintains the con text for the particular client until one of the following occurs:

• The client performs a remote procedure call that frees the context.

• The client terminates while context is being maintained.

• Communication breaks between the client and server.

If the client terminates or the client-server communication breaks while the server maintains context, the server's RFC runtime library may invoke a context rundown procedure to clean up user data.

Writing Procedures That Use a Context Handle

Example 7-3 shows how to implement a procedure that obtains an active context handle, one that uses the active context handle, and one that frees the context handle.

Example 7~3: Procedures That Use Context Handles

I* FILE NAME: manager.c */ ttinclude <stdio.h> ttinclude <string.h> #include <io.h> #include <errno.h> ttinclude "rfile.h"

filehandle remote_open(binding_h, name, mode) /* O */

rpc_binding_handle_t binding_h;

char name [ ] ;

char mode [ ] ;

{

FILE *FILEh;

if (strlen( (char *)name) == 0) /* no file name given */

if(strcmpt(char *)mode, "r") == 0)

FILEh = NULL; /* cannot read nonexistent file */

else FILEh = stdout; /* use server stdout */

else if(access((char *)name, 0) == 0) /* file exists */

if(strcmpt(char *)mode, "w") == 0)

FILEh = NULL; /* do not overwrite existing file */

else FILEh = fopen((char *)name, (char *)mode); /* open read/append */

else /* file does not exist */

if(strcmpt(char *)mode, "r") == 0)

FILEh = NULL; /* cannot read nonexistent file */

else FILEh = fopen((char *)name, (char *)mode); /* open write/append */

return( (filehandle)FILEh ); /* cast FILE handle to context handle */

Example 7-3: Procedures That Use Context Handles (continued)

long int remote_send(fh, buf, max) /* © */

filehandle fh;

buffer buf;

long int max;

{

/* write data to the file (context) , which is cast as a FILE pointer */

return( fwrite(buf, max, 1, fh) ) ;

void remote_close(fh) /*€)*/

filehandle *fh; /* the client stub needs the changed value upon return */

{

if( (FILE *) (*fh) != stdout ) f closet (FILE *) (*fh) ) ;

(*fh) = NULL; /* assign NULL to the context handle to free it */

return; }

O Initialize data as required by later calls, and assign the application context to the server context handle. In this example, a file handle is obtained and assigned to the context handle when the procedure returns. Outside of the server process this file handle is meaningless, but when the client makes sub sequent calls, the server uses this file handle to write data or close the file.

© Use the server context handle parameter defined with the in directional attribute. This procedure must have an active context handle as input. For this example, the buffer (buf) of max number of items is written to the file. Cast the server context handle to the context's data type (FILE *).

€) Free the context by using a procedure whose context handle parameter is defined with the in and out directional attributes. This procedure must have an active context handle as input. To free the context, assign null to the server context handle and use the C library procedure free or a corresponding method to clean up your application. In this example, before freeing the file handle, the context is tested to be sure it does not refer to stdout. The server context handle is cast to the context's data type.

When this procedure returns to the client, the client stub automatically frees the context handle on the client side if the server context handle is set to NULL.

If memory must be allocated for the context, use the C library procedure malloc or another method. Do not use the stub support procedure midl_user_allocate because you do not want the allocated memory to be automatically freed by the server stub after the procedure completes.

Microsoft RFC Programming Guide

Writing a Context Rundown Procedure

A context rundown procedure allows orderly cleanup of the server context. The server RFC runtime library automatically calls it when a context is maintained for a client, and either of the following occurs:

• The client terminates without requesting that the server free the context.

• Communication breaks between the client and server.

In our example, the interface definition defines the following type as a context handle:

typedef [context_handle] void *filehandle;

Example 7-4 shows the context rundown procedure to implement in the server code. The procedure name is created by appending _rundown to the type name (filehandle). The procedure does not return a value and the only parameter is the context handle. In this example, when the context rundown procedure executes, it closes the file that represents the context.

Example 7-4: A Context Rundown Procedure

I* FILE NAME: crndwn.c */ #include <stdio.h> #include "rfile.h"

void filehandle_rundown(remote_fh)

filehandle remote_fh; /* the context handle is passed in */

{

fprintf (stderr, "Server executing context rundown\n" ) ;

if( (FILE *)remote_fh != stdout )

f close ( (FILE *)remote_fh ) ; /* file is closed if client is gone */

remote_fh = NULL; /* must set context handle to NULL */

return; }

The context handle must be defined as a type in the interface definition in order for the server runtime to automatically call the context rundown procedure. And if you define the context handle as a type, then you must implement a context run down procedure in the server.

picture33

MIDI and ACF Attributes Quick Reference

All MIDI attributes are shown in Tables A-l through A-8, and all ACF attributes are shown in Table A-9, but not all are demonstrated in this book.

Table A-l: MIDI Interface Header Attributes Attribute Description

uuid ( uuid_string)

version (major.minor) pointer_default (kind)

endpoint (string)

local

A universal unique identifier is generated by the uuidgen utility and assigned to the interface to distin guish it from other interfaces. This attribute is re quired unless the local attribute is used. A particular version of a remote interface is identified when more than one version exists. The default treatment for pointers is specified. Kinds of pointers include reference (ref) and unique (unique).

An endpoint is a number representing the transport-layer address of a server process. This attribute spec ifies a well-known endpoint on which servers will lis ten for remote procedure calls. Well-known end-points are usually established by the authority re sponsible for a particular transport protocol. The MIDI compiler can be used as a C language header file generator. When this attribute is used, all other interface header attributes are ignored and no stub files are generated by the MIDL compiler.

Microsoft RFC Programming Guide

Table A-2: MIDI Array Attributes Attribute Description

string

An array is specified to have the properties of a string.

size_is(size)

max_is (max)

first_is( first)

last_is(Iast)

length_is (length)

Conformant Array Attributes A variable is defined in the interface definition and used at runtime to establish the array size.

A variable is defined in the interface definition and used at runtime to establish the maximum index value.

Varying Array Attributes

A variable is defined in the interface definition and used at runtime to establish the lowest index value of transmit ted data. The value is not necessarily the lowest bound of the array.

A variable is defined in the interface definition and used at runtime to establish the highest index value of trans mitted data. The value is not necessarily the highest bound of the array.

A variable is defined in the interface definition and used at runtime to establish the number of elements transmit ted for a portion of the array.

Table A-3: MIDL Pointer Type Attributes Attribute Description

unique A pointer is specified as a unique pointer with the unique attribute. Unique pointers provide basic indirection and they can be null. Unique pointers cannot contain cycles or loops.

ref A pointer is specified as a reference pointer with the ref attribute.

This attribute gives basic indirection without the implementation over head associated with unique pointers.

string A pointer is specified as pointing to a string.

Appendix A: MIDI and ACF Attributes Quick Reference

139

Table A-4: MIDI Data Type Attributes Attribute Description

pointer type attributes

context_handle

handle

transmit_as ( type)

A data type with a visible pointer operator may be speci fied with a pointer type attribute (See Table A-3). A state is maintained on a particular server between re mote procedure calls from a specific client by maintaining a context handle as a data type. The context handle iden tifies the state.

A defined data type is specified as a customized handle so that the client-server binding information is associated with it.

A data type that is manipulated by clients and servers may be specified so that it is converted to a different data type for transmission over the network.

Table A-5: MIDI Structure Member Attributes

Description

Attribute

array attributes

pointer type attributes

ignore

A structure member can have array attributes if it has ar ray dimensions or a visible pointer operator. A structure member that has a visible pointer operator and the size_is or max_is attribute defines a pointer to a con formant array, not an array structure member (see Table A-2).

A structure member can have a pointer type attribute if it has a visible pointer operator (see Table A-3). Do not transfer the data in this structure member (a pointer) during a remote procedure call. This can save the overhead of copying and transmitting data to which the pointer refers.

Table A-6: MIDI Union Case Attributes Attribute i Description

pointer type attributes

A union case can have a pointer type attribute if it has a visible pointer operator. See Table A-3.

Microsoft RFC Programming Guide

Table A- 7: MIDI Procedure Parameter Attributes

Attribute

Description

in

out

array attributes

pointer type attributes context_handle

The parameter is input when the remote procedure is called.

The parameter is output when the remote procedure re turns.

A parameter with array dimensions can have array at tributes. A conformant array is a procedure parameter with a visible pointer operator and the size_is or max_is attribute (see Table A-2).

A parameter with a visible pointer operator can have a pointer type attribute. See Table A-3.

A parameter that is a void * type can have the context handle attribute.

Table A-8: MIDI Procedure Attributes Attribute Description

string

ptr

unique context handle

Procedure Result Attributes

A procedure result is specified to have the properties of a string with the string attribute.

A procedure that returns a pointer result always returns a full pointer. It may be specified with the ptr attribute but this is not necessary. Full pointers provide basic indirection and they can be null. They can also contain cycles or loops. Unique pointers provide basic indirection and they can be null. Unique pointers cannot contain cycles or loops. Unique pointers can be specified with the unique attribute. A procedure returns a context handle result in order to indi cate a state on a particular server, which is then referred to in successive remote procedure calls from a specific client.

Table A-9: ACF Attributes Attribute

Description

Binding Methods

auto_handle

iirplicit_handle (type name) explicit_handle ( type name)

The automatic binding method is selected. The implicit binding method is selected. The explicit binding method is selected.

Table A-9: ACF Attributes (continued)

Exceptions as Parameters

corrm_status Names a parameter or the procedure result to

which a status code is written if a communica tion error is reported by the client runtime to the client stub. The client remote procedure call must include the error_status_t data type in its argument list. If an error is report ed and this attribute and error_status_t da ta type are not used, the client stub raises an exception.

fault_status Names a parameter or the procedure result to

which a status code is written if an error is re ported by the server runtime to the server stub, an exception occurs in the server stub, or an exception occurs in the remote proce dure. If an error is reported and this attribute is not used, the client stub raises an excep tion.

Excluding Unused Procedures code All or selected procedures from the interface

have the associated client stub code generated

by the MIDI compiler, nocode All or selected procedures from the interface

do not have the associated client stub code

generated by the MIDL compiler.

picture34

RFC Runtime Routines Quick Reference

The following tables organize the RFC runtime routines. Table B-l shows all the routines that client applications can use, and Table B-2 shows all the routines that server applications can use. The following abbreviations are used in RFC runtime routine names:

Numbers next to the calls have the following meaning: Function is limited to using Windows NT Security. Function is supported on Microsoft Windows NT systems only. Function acts on only the local process with Microsoft RPC Version 1.0.

4

Function provided for compatibility with DCE CDS. Not supported by the Microsoft Locator Version 1.0.

Microsoft RFC Programming Guide

Table B-l Client RFC Runtime Routines

Appendix B: RFC Runtime Routines Quick Reference

145

Table B-l Client RFC Runtime Routines (continued)

Microsoft RFC Programming Guide

Table B-2 Server RFC Runtime Routines

Appendix B: RFC Runtime Routines Quick Reference

147

Table B-2 Server RFC Runtime Routines (continued)

In this Appendix:

• How to Build and Run the Application

• Application Files

picture35

The Arithmetic Application

The arithmetic application makes a remote procedure call to a procedure named sum_arrays, which adds together the values for the same array index in two long integer arrays, and returns the sums in another long integer array.

The application demonstrates the basics of a distributed application with a remote procedure call and includes these features:

• Denning a simple array in an interface definition

• Using the automatic binding method

• Exporting a server to the name service

• Checking the error status of RFC runtime calls

How to Build and Run the Application

To build the server of the distributed application, type the following:

C:\SERVER> nmake server

To run the server of the distributed application, type the following:

C:\SERVER> arith

To build the client of the distributed application, type the following:

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required