Steve is a developer with INOVA Corp. in Charlottesville, Virginia. He can be contacted at [email protected].
In "Calling 16-bit DLLs from Windows 95" (DDJ, April 1996), I presented a simple technique to allow 32-bit Windows 95 applications to thunk to 16-bit DLLs. In this article, I borrow from parts of that technique to simplify Remote Procedure Calls (RPC) in 32-bit Windows 95/NT applications.
The sample programs I present here demonstrate how to build simple RPC clients and servers using Microsoft Visual C++ 4.x (sample programs are available electronically; see "Availability," page 3). The programs should compile with other compilers or previous 32-bit versions of Visual C++ as well, but you will need the Win32 SDK to obtain the RPC development tools.
What is RPC?
Remote Procedure Call programming is a specification that allows client programs to call server-based functions. Though RPC is designed to be compliant with the Open Software Foundation's Distributed Computing Environment (OSF/DCE) specification (which allows communications with UNIX-based servers), it has also become a fundamental part of the Win32 platform. Technologies such as OLE and Distributed OLE (DCOM) use RPC as an underlying transport mechanism. While a complete description of RPC programming is beyond the scope of this article, its general purpose and functionality can be described in only a few words: RPC lets you develop function calls that may execute on local or remote machines. For the most part, the location of the code that implements these functions is transparent to the application. Figure 1, which shows how the function AddData() might be executed through RPC on a remote system, depicts two different PCs connected on a network. RPC uses a transport layer over this network to communicate between client and server programs. This transport layer transfers data according to a certain type of transport protocol. Table 1 lists some of the transport protocols supported in Windows 95/NT.
Although my sample server only uses named pipes and local transport protocols, it still contains definitions for most of the other protocols as well (see the various RPC_PROTxxx values in rpcparm.h). For instance, it can also use TCP/IP, but you will need to take a look at some RPC reference material (such as on the Microsoft Developer's Network CD) for the specific parameters that are needed.
Also note in Table 1 that Windows 95 only supports named pipes on the client side. This means that a Windows 95 client can use named pipes to communicate, but the server must run on Windows NT. Named pipes are an efficient transport protocol and would certainly be a good choice for many applications -- as long as the server can run on Windows NT.
The Problem
One of my goals in developing the flat-thunking technique was to find a way to avoid the tedious intermediate steps needed to add new thunking functions. These steps involve adding the new function description to a thunking language script, then running a thunking script compiler on the script. This compiler then produces an assembly-language output file which is eventually assembled and linked to a C/C++ program.
Adding RPC function calls to a program involves some of the same tedious steps. Each new RPC function must be described in the Interface Definition Language (IDL) file script language, then compiled by the IDL compiler, which produces C output files. These C output files are then linked into the client and server C/C++ programs.
I simplified the thunking development process by providing a "generic" function that only needs to be defined once in the thunk script. This generic function is designed to handle many different parameter types without the need to change its definition.
I simplified the RPC development process by taking the same approach. The RPC IDL file contains the definition for a generic function that can handle many different types of data. Having this function handle many data types also eliminates the need to define many new RPC functions.
A Solution
The RPC client I developed implements a function InvokeRPCFunc() (see rpccltin.c, available electronically) that works like an OLE marshaling routine. It basically takes the variable parameters passed to it and groups them into a set of fixed parameters that my generic RPC function can handle. Each of these parameter types are described by a special set of DTC_*** #defines (see rpcparm.h). The client program implements a proxy method that defines each parameter type using these values, then calls InvokeRPCFunc() with a specified function.
The server contains my RPC generic function CallRPC() (see rpcsvrmn.c) that performs the opposite of InvokeRPCFunc(). It performs a function similar to OLE unmarshaling. In other words, it takes the fixed arguments passed to it and turns them back into variable arguments by pushing them onto the stack using inline assembly language. It then looks up and calls the function specified in InvokeRPCFunc(), which will receive the parameters on the stack.
Listing One hows a proxy method that loads a server-based DLL called "dummydll.dll." InvokeRPCFunc() takes four fixed arguments, followed by 0 to n variable arguments (depending on the function being called). The first argument is a dispatch table ID (in this example, SRVDISP_LOADLIBRARY; see rpcparm.h, available electronically). CallRPC() looks up the server-based function corresponding to this ID in the dispatch table (see rpcsvrds.c), then invokes it. The second argument is a pointer to the unsigned long variable that will contain the result of the function call. Note that this is always returned as an unsigned long, but it can easily be cast to any type upon return. The third argument is an array of lengths for DTC_PTR type fields. The fourth argument is a pointer to an array of DTC_ values that describe each of the variable argument types.
Describing Parameters for InvokeRPCFunc()
Each of the variable arguments passed to InvokeRPCFunc() must be described using the various DTC_ values found in rpcparm.h. For instance, the sample LoadDummyDLL() method passes one parameter -- a NULL-terminated string (which is described by DTC_STR). If you are familiar with the way my thunking code works, then you might recall that it had a few basic data types and a DTC_PTR type that could be a pointer to any type of data.
If you compare the parameter types defined in rpcparm.h with those in my thunking code's parm.h, you will see that rpcparm.h contains specific data pointer types instead of the thunking code's generic DTC_PTR data-pointer type. For instance, DTC_PCHAR tells the RPC generic function to expect a pointer to a CHAR field. You might gather from these specific data types that RPC functions always need to know the size of each data item that is being passed. That is true because RPC must transmit each data item from the client program across a transport layer to the corresponding server stub. Without this size, the RPC mechanism would have no idea how much data to transfer.
If each data item must contain a specific size, then how do you handle pointers to your own data types, such as structure pointers or dynamically allocated memory? This is where the third argument to InvokeRPCFunc() comes into play. The DTC_PTR type used in my RPC code is a special parameter type. In C terms, think of DTC_PTR as a pointer to a void (void *). Since DTC_PTR allows you to tell RPC that you have a pointer, the only thing left is to tell RPC exactly how much data you have. This is where you use the third argument to InvokeRPCFunc(). This third argument points to an array of long values that describe the lengths of the various data that the DTC_PTR values addresses.
There is one important thing to keep in mind when passing your own memory buffers: Data can be passed in both directions, but the size of the buffer is always controlled by the client. For example, the client program can define a character array of 40 bytes that will receive data from the server program. The server program can copy data into this buffer, but must not copy more than 40 bytes of data into the buffer. An RPC run-time exception occurs if the server program copies more data into the buffer than the client has allocated. It might be a good idea to also pass a parameter to the server along with the buffer that contains the buffer's maximum size.
Adding new RPC functions is now a simple matter of implementing a new function in the server and adding an entry point in the server's dispatch table (rpcsvrds.c). Another easy way to add new functions is to place them in server-based DLLs. Adding DLLs requires no changes to the server's code, only the copying of the DLL to the server. (Loading DLLs is accomplished by using some of the sample RPC server's system-level functions.)
Server-Based Functions
To make the sample RPC server easier to extend and debug, I've provided some system-level server-based functions. These system-level functions are called by building a proxy method that uses their dispatch ID (in rpcparm.h) along with their required parameters. Table 2 describes the system-level functions provided, along with their required parameter types.
Note that SRVDISP_LOADLIBRARY and SRVDISP_FREELIBRARY are identical in syntax to the LoadLibrary() and FreeLibrary() Win32 API calls. SRVDISP_CALLPROC is equivalent to doing a GetProcAddress() Win32 API call, then calling the function using the pointer returned.
Be careful when using the SRVDISP_TRACE function. It is typically a good function to use when debugging client programs, but in production can have a devastating effect on performance. The reason for this is that enabling tracing causes the server to display status for every parameter of every task. The sheer volume of data being written to the console causes the server to expend a large amount of processing time displaying output. If there are enough clients running, this time delay causes the server's queues to become overloaded with unhandled requests.
Sample Program Description
The sample programs (available electronically) demonstrate how to build simple RPC client and server applications using a mixture of C and C++. They also demonstrate how to use the various server-based systemlevel functions to do such things as loading a server-based DLL and calling functions within it. My goal in providing these samples is not to give you the complete ins-and-outs of RPC programming, but rather to give you a working set of RPC client/server code that you can extend. These programs use only a subset of RPC programming, but they do give you something that works out of the box. They are a good starting platform for building extended RPC client/server programs.
The sample code is designed to work on Windows 95/NT (x86 versions only). It makes some assumptions about the type of processor that it is running on -- it expects x86-type byte ordering and x86-style stack frame. These requirements are necessary because of the way the unmarshaling code (see CallRPC() in rpcsvrmn.c) turns the fixed arguments back into variable arguments. The inline assembly-language instructions and byte-ordering assumptions in the unmarshaling code will have to change if the server is to run on non-x86 versions of Windows NT.
It is important to keep in mind that RPC servers operate using multiple threads of execution. DLLs loaded by the server must therefore be multithreaded. This is generally not a problem if you are building your own DLLs. You can set the run-time library to multithreaded under the Settings...C/C++ Code Generation option in the Visual C++ IDE. Commercial DLLs, on the other hand, may not be able to handle multiple threads of execution, and this may result in segment faults and so forth when several client applications begin to call them at the same time. The best rule of thumb is to try the DLL with one active client, then gradually add additional clients until a problem surfaces.
Expansion
You can extend my RPC client and server many ways. For example, new functions might be added to build a server-based data repository. Or you might develop a server-based launch program to automatically download and distribute newer versions of files or programs to each client workstation. You might also want to extend the server by adding support for different NT platforms by changing the inline assemblylanguage marshaling code in rpcsvrmn.c.
There are also some advanced RPC capabilities to consider when using the sample server and clients in a production environment. For instance, because RPC allows access to remote machines and resources, security might be a concern. There are several RPC APIs for handling client and server security, but for simplicity they are not implemented in my samples.
DDJ
Listing One
BOOL CRpcClient::LoadDummyDLL(){ BYTE parms[] = {DTC_STR,DTC_END}; ULONG ulError; </p> // Tell the server to load the dummydll.dll. SRVDISP_LOADLIBRARY is // a function in the dispatch table (see rpcsvrds.c) InvokeRPCFunc(SRVDISP_LOADLIBRARY,&ulError,NULL,parms,"dummydll.dll"); </p> // This return value is our DLL handle (see LoadLibrary() // for a description) m_hDummyDLL = (HANDLE) ulError; </p> // Return success/fail to caller return(m_hDummyDLL != 0); }
Copyright © 1997, Dr. Dobb's Journal