Contents
I have worked with client-server applications for a couple of years now, all of these applications use RPC as the layer of communication between the client and the server. I found it strange that no real article existed on this matter here on CodeProject, so I decided to write one of my own to spread my knowledge on this matter.
The matter is on the other hand a bit big, so I have decided to split it into several articles of different levels of difficulty. This is the second article and it will introduce you to context handles. You should have read the previous article before reading this one.
What is a context handle and what is it good for? Well, we use context handles all the time when we are programming, but often they have different names in different places. You can think of a context handle as the equivalent to the this
-pointer in C++, the HANDLE
returned from CreateFile
, or the FILE*
-pointer returned from fopen
. They are all different context handles that behave somewhat different, but they accomplish the same thing: they all connect to an object.
The different context handles you are used to, are more or less opaque, in RPC the context handles are totally opaque as they are actually pointers of type void*
.
It is time to expand the Hello World example from my previous article with the use of a rather simple context handle. There is no better time than now.
[
uuid(00000003-EAF3-4A7A-A0F2-BCE4C30DA77E),
version(1.0),
explicit_handle
]
interface ContextExample
{
typedef [context_handle] void* CONTEXT_HANDLE;
CONTEXT_HANDLE Open(
[in] handle_t hBinding,
[in, string] const char* szString);
void Output(
[in] CONTEXT_HANDLE hContext);
void Close(
[in, out] CONTEXT_HANDLE* phContext);
}
What has changed in this example from the previous one? We have added a typedef
of a context handle named CONTEXT_HANDLE
("Oooh", the masses roar). We have also added two functions to open and close the context handle. The Output
function has been changed so that it takes a context handle instead of a string
. And that's it, nothing else has changed.
As you can see, we are using explicit binding handles, but the Output
and the Close
functions do not refer to the binding handle directly. They both use the binding handle indirectly through the context handle. A client side context handle contains the binding handle it is connected to, so there is no real need to send both the binding handle and the context handle to the functions.
It's time to take the generated files and put them to use in our server application.
#include <iostream>
#include <string>
#include "ContextExample.h"
DWORD HandleError(const char* szFunction, DWORD dwError);
CONTEXT_HANDLE Open(
handle_t hBinding,
const char* szString)
{
std::string* pContext = new std::string(szString);
CONTEXT_HANDLE hContext = pContext;
std::clog << "Open: Binding = " << hBinding
<< "; Context = " << hContext << std::endl;
return hContext;
}
void Output(
CONTEXT_HANDLE hContext)
{
std::clog << "Output: Context = " << hContext << std::endl;
std::string* pContext = static_cast<std::string*>(hContext);
std::cout << *pContext << std::endl;
}
void Close(
CONTEXT_HANDLE* phContext)
{
std::clog << "Close: Context = " << *phContext << std::endl;
std::string* pContext = static_cast<std::string*>(*phContext);
delete pContext;
*phContext = NULL;
}
void __RPC_USER CONTEXT_HANDLE_rundown(CONTEXT_HANDLE hContext)
{
std::clog << "CONTEXT_HANDLE_rundown: Context =
" << hContext << std::endl;
Close(&hContext);
}
DWORD WINAPI RpcServerListenThreadProc(LPVOID )
{
return RpcServerListen(
1,
RPC_C_LISTEN_MAX_CALLS_DEFAULT,
FALSE);
}
RPC_STATUS CALLBACK SecurityCallback(RPC_IF_HANDLE , void* )
{
return RPC_S_OK;
}
int main()
{
RPC_STATUS status;
std::clog << "Calling RpcServerUseProtseqEp" << std::endl;
status = RpcServerUseProtseqEp(
reinterpret_cast<unsigned char*>("ncacn_ip_tcp"),
RPC_C_PROTSEQ_MAX_REQS_DEFAULT,
reinterpret_cast<unsigned char*>("4747"),
NULL);
if (status)
return HandleError("RpcServerUseProtseqEp", status);
std::clog << "Calling RpcServerRegisterIf" << std::endl;
status = RpcServerRegisterIf2(
ContextExample_v1_0_s_ifspec,
NULL,
NULL,
RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH,
RPC_C_LISTEN_MAX_CALLS_DEFAULT,
(unsigned)-1,
SecurityCallback);
if (status)
return HandleError("RpcServerRegisterIf", status);
std::clog << "Creating listen thread" << std::endl;
const HANDLE hThread = CreateThread(NULL, 0, RpcServerListenThreadProc,
NULL, 0, NULL);
if (!hThread)
return HandleError("CreateThread", GetLastError());
std::cout << "Press enter to stop listening" << std::endl;
std::cin.get();
std::clog << "Calling RpcMgmtStopServerListening" << std::endl;
status = RpcMgmtStopServerListening(NULL);
if (status)
return HandleError("RpcMgmtStopServerListening", status);
std::clog << "Waiting for listen thread to finish";
while (WaitForSingleObject(hThread, 1000) == WAIT_TIMEOUT)
std::clog << '.';
std::clog << std::endl << "Listen thread finished" << std::endl;
DWORD dwExitCodeThread = 0;
GetExitCodeThread(hThread, &dwExitCodeThread);
CloseHandle(hThread);
if (dwExitCodeThread)
return HandleError("RpcServerListen", dwExitCodeThread);
std::cout << "Press enter to exit" << std::endl;
std::cin.get();
}
void* __RPC_USER midl_user_allocate(size_t size)
{
return malloc(size);
}
void __RPC_USER midl_user_free(void* p)
{
free(p);
}
Now the server implementation is rather big compared to our first standalone application, but not so much bigger than the server example in the previous article.
So what has changed? The implementation of the Open
and Close
functions were added. As you can see, the context handle is in fact a pointer to a std::string
but disguised as a CONTEXT_HANDLE
(that in turn is a pointer of type void*
). The server is now more verbose, it writes what it is doing and any errors to the console window. The actual functionality to listen to incoming RPC calls is now implemented in a thread, this in turn allows the server to be shutdown by pressing enter, instead of killing it by closing the window. The memory allocation and de-allocation routines remain the same.
Something that I haven't talked about yet is the rundown routine. What is a rundown routine, you may ask. A rundown routine is a chance for the server to free any resources allocated by a context handle, if the client is disconnected from the server. The rundown routine is automatically called by the RPC runtime for each open context handle in the client, if the client somehow is disconnected from the server.
Time has come to write our client application that will connect to the server and use the new context handles.
#include <iostream>
#include "ContextExample.h"
DWORD HandleError(const char* szFunction, DWORD dwError);
int main()
{
RPC_STATUS status;
unsigned char* szStringBinding = NULL;
std::clog << "Calling RpcStringBindingCompose" << std::endl;
status = RpcStringBindingCompose(
NULL,
reinterpret_cast<unsigned char*>("ncacn_ip_tcp"),
reinterpret_cast<unsigned char*>("localhost"),
reinterpret_cast<unsigned char*>("4747"),
NULL,
&szStringBinding);
if (status)
return HandleError("RpcStringBindingCompose", status);
handle_t hBinding = NULL;
std::clog << "Calling RpcBindingFromStringBinding" << std::endl;
status = RpcBindingFromStringBinding(
szStringBinding,
&hBinding);
if (status)
return HandleError("RpcBindingFromStringBinding", status);
std::clog << "Calling RpcStringFree" << std::endl;
status = RpcStringFree(
&szStringBinding);
if (status)
return HandleError("RpcStringFree", status);
std::clog << "Calling RpcEpResolveBinding" << std::endl;
status = RpcEpResolveBinding(hBinding, ContextExample_v1_0_c_ifspec);
if (status)
return HandleError("RpcEpResolveBinding", status);
RpcTryExcept
{
std::clog << "Calling Open" << std::endl;
CONTEXT_HANDLE hContext = Open(hBinding, "Hello Context World!");
std::cout << "Press enter to call Output" << std::endl;
std::cin.get();
std::clog << "Calling Output" << std::endl;
Output(hContext);
std::cout << "Press enter to call Close" << std::endl;
std::cin.get();
std::clog << "Calling Close" << std::endl;
Close(&hContext);
}
RpcExcept(1)
{
HandleError("Remote Procedure Call", RpcExceptionCode());
}
RpcEndExcept
std::clog << "Calling RpcBindingFree" << std::endl;
status = RpcBindingFree(
&hBinding);
if (status)
return HandleError("RpcBindingFree", status);
std::cout << "Press enter to exit" << std::endl;
std::cin.get();
}
void* __RPC_USER midl_user_allocate(size_t size)
{
return malloc(size);
}
void __RPC_USER midl_user_free(void* p)
{
free(p);
}
What has changed since the last time? We are checking the state of the binding handle before we are using it by using RpcEpResolveBinding
. This will try to verify that the server is running (or not) before we use any of its exposed RPC functions. Then we create a context, outputs the string
using this context and finally closes the context. The client is now more verbose, it writes what it is doing and any errors to the console window. The memory allocation and de-allocation routines remain the same.
To test the rundown routine in the server, I have added some points in the code where it waits for the user to press enter. If you kill the application by closing the window instead of pressing enter, you will see the rundown in effect.
This section describes some techniques useful when using context handles in RPC applications.
The example showed in this article is not thread-safe. If two threads does things in the server at the same time, things could start behaving strange or even crash. In this simple example it does not matter, but it could easily be fixed by using some sort of mutual exclusive synchronization objects (critical sections). This will be addressed in a future article.
If the server becomes unavailable/crashes, the client application should call the function RpcSmDestroyClientContext
to free its context data.
Using context handles in RPC is a very powerful thing, it helps you to develop object oriented applications with RPC. An object on the server can be represented by a context handle on the client, and using encapsulation, the implementation of an object on the client can often map functions directly to the server, by using context handles.
Still, we have only scratched the surface of the world of RPC and client/server applications. In my future articles, we will dig even deeper.
- 2012-12-22
- Finally updated to work with Visual Studio 2010 and fixed security callback
- 2003-08-31