[Previous] [Contents] [Next]

The Design and Purpose of RPC

RPC alleviates the difficulties commonly associated with building distributed applications. These difficulties include all the errors that can occur in applications that communicate over a network. When an application sends a message, such as a Dynamic Data Exchange (DDE) message, to another application, it can be reasonably sure that the other application will receive it. However, even with DDE, a lot of programming effort is focused on error handling. What if an application engaged in the conversation doesn't follow the DDE protocol properly? What if it crashes? What if it sends garbage? These types of problems tend to increase exponentially when you communicate over a network. As anyone who has ever done low-level network programming knows, the number of errors that can occur is mind-boggling. Someone can trip over the network cable, the server can crash, or an application can fail to acknowledge a message. When you deal with network communications, the following rule of thumb still applies: if it can go wrong, it will.

RPC addresses these problems by providing a high-level procedural interface to the network. Until recently, all distributed computing was centered on the problem of I/O. Centralized systems, however, were not built based on I/O but rather on a procedural foundation. RPC resolved this discrepancy by providing a facility to build distributed systems based on the procedural model of its centralized ancestors. RPC is meant to be as unobtrusive as possible. The RPC model attempts to adhere closely to the Local Procedure Call (LPC) model. When RPC is implemented correctly, a programmer doesn't know whether a function has executed remotely or locally. This transparency is made possible by a special language originally designed for RPC and later commandeered by COM, Interface Definition Language (IDL).

Interface Definition Language

In RPC, as in COM, IDL defines the interface between the client and the server; all communications between the client and the server pass through the IDL interface. When programmers work on a project in which different applications are written by different teams, one problem is how to define a common interface to which everyone will adhere. With IDL, this process is automated. When the interface has been defined with IDL, all teams must adhere to it or they will not be able to compile their programs.

Here's how IDL works. You specify the name, version, and universally unique identifier (UUID) of the interface in the definition file. The UUID is a special number that ensures that RPCs are made to the correct server. Also included in the interface definition are special prototypes for all the exported functions that the client might call. All this data is saved in a file with an .idl extension. An interface definition file is somewhat analogous to a module definition (.def) file for a dynamic-link library (DLL).

The Microsoft IDL (MIDL) compiler compiles the source IDL file. Technically speaking, MIDL is a translator and not a compiler. It does not produce machine code—it translates IDL code into C. The C code generated by MIDL forms the remote procedure stubs in both the client and the server. Thus, the master IDL file produces code that is compiled and linked by both sides of the distributed application. If one side doesn't follow the specified interface, the compile and the link will fail.

You might find it interesting to examine the code produced by the MIDL compiler to see what it actually does. The MIDL compiler also uses the optional Application Configuration File (ACF) when it translates the IDL file. In the ACF, you can declare the type of binding handles used as well as optional server parameters.

Binding

The client connects to the server using a binding mechanism—a logical connection between the client and the server. Binding is a type of linking used for RPCs. Of the two standard types of linking, static and dynamic, binding is most similar to the latter. When you use dynamic linking, the address of the function called is resolved at run time. The binding mechanism in RPC differs from dynamic linking in that the procedure being called is located on a different computer. Therefore, the client cannot determine the correct address for the function because the function is in the server's address space. Only the server knows the actual address of the function called by the client. Thus, the client never actually calls the remote procedure; it asks the server to do so on its behalf. If you keep in mind that all communication is inherently I/O, it becomes obvious that the client cannot actually execute a jump to an address located on the server.

Two types of binding are available to RPC applications: manual and automatic. The manual method is more complex, and it requires that you both create and maintain the binding programmatically. However, it offers more control over the binding and the destination of RPCs. And since RPC applications are inherently complex, the manual binding method is used for most RPC applications.

When you use automatic binding, the MIDL compiler generates all the code to create and maintain the binding. This makes your job much easier, but the cost is less control. Automatic binding is usually used only in general applications that do not care which server they bind to. For example, an application that wants to get the time from a remote server is a good candidate for automatic binding.

Location Transparency

When the client calls the remote procedure, the code usually looks exactly as if it were written for an LPC. What happens, however, is radically different. When the RPC is executed, the client jumps to the client stub1 generated by the MIDL compiler. The stub packages all the function parameters into a complex data structure in Network Data Representation (NDR) format. This structure is transmitted over the network to the server, where it is unpacked by the server stub and delivered to the remote procedure as regular function parameters. Because the client stub has the same name as the remote procedure, this whole process is transparent to the programmer.

Handles

An RPC application manages two main types of handles: binding handles and context handles. Binding handles contain information about the binding between the client and server, and context handles maintain state information.

A client initiates the binding process by calling several RPC run-time functions. If everything goes smoothly (if a valid server is found), the client receives a binding handle. A binding handle is an opaque data structure that the client uses when it makes RPCs and is always the first parameter passed to a remote procedure.

There are two ways to pass binding handles in an RPC application: implicitly and explicitly. Implicit binding handles are easier to use because the code for passing them is generated by the MIDL compiler. When you use an implicit binding handle, you declare it as a global variable so that the C code generated by the MIDL compiler can package it for transmission to the remote procedure's stub. All binding handles are eventually converted to explicit binding handles, but with implicit binding handles you need not be concerned with the details.

You pass explicit binding handles as the first parameter to every RPC. You gain control at the cost of extra complexity. With explicit binding handles, you can manage simultaneous connections to multiple servers. The RPC application presented later in this appendix uses explicit binding handles. Microsoft RPC also lets you define your own structures for use as binding handles. These handles can associate data unique to each binding for the application's use, similar to the ability to store application-defined data in a window handle.

Context handles, which are created and returned by the server, store information about the state of a server. Our sample RPC application uses context handles to determine when a client goes off line. If a client terminates, a special callback function known as a context handle rundown routine is executed on the server to notify the server that the client has terminated. The server can determine which client terminated by using the value stored in the context handle passed to the rundown routine.