Click here to Skip to main content
Click here to Skip to main content

Introduction to RPC - Part 2

By , 22 Dec 2012
 

Hello Context World!

Contents

Introduction

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.

Introducing contexts

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.

Hello Context World!

// File ContextExample.idl
[
   // A unique identifier that distinguishes this
   // interface from other interfaces.
   uuid(00000003-EAF3-4A7A-A0F2-BCE4C30DA77E),

   // This is version 1.0 of this interface.
   version(1.0),

   // This interface will use explicit binding handle.
   explicit_handle
]
interface ContextExample // The interface is named ContextExample
{
   // To fully use context handles we need to do a typedef.
   typedef [context_handle] void* CONTEXT_HANDLE;

   // Open a context on the server.
   CONTEXT_HANDLE Open(
      // Explicit server binding handle.
      [in] handle_t hBinding,
      // String to be output on the server.
      [in, string] const char* szString);

   // Output the context string on the server.
   void Output(
      // Context handle. The binding handle is implicitly
      // used through the explicit context handle.
      [in] CONTEXT_HANDLE hContext);

   // Closes a context on the server.
   void Close(
      // Context handle. The binding handle is implicitly
      // used through the explicit context handle.
      [in, out] CONTEXT_HANDLE* phContext);
}

Changes

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 contain 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.

Context server

It's time to take the generated files and put them to use in our server application.

Hello Server Context World!

// File ContextExampleServer.cpp
#include <iostream>
#include <string>
#include "ContextExample.h"

// Write a formatted error message to std::cerr.
DWORD HandleError(const char* szFunction, DWORD dwError);

CONTEXT_HANDLE Open(
   /* [in] */ handle_t hBinding,
   /* [string][in] */ 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(
   /* [in] */ 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(
   /* [out][in] */ CONTEXT_HANDLE* phContext)
{
   std::clog << "Close: Context = " << *phContext << std::endl;
   std::string* pContext = static_cast<std::string*>(*phContext);
   delete pContext;

   // We must set the context handle to NULL, or else we will get
   // a rundown later anyway.
   *phContext = NULL;
}

// The RPC runtime will call this function if the connection to the client
// is lost.
void __RPC_USER CONTEXT_HANDLE_rundown(CONTEXT_HANDLE hContext)
{
   std::clog << "CONTEXT_HANDLE_rundown: Context = 
                   " << hContext << std::endl;
   Close(&hContext);
}

// The thread that will listen for incoming RPC calls.
DWORD WINAPI RpcServerListenThreadProc(LPVOID /*pParam*/)
{
   // Start to listen for remote procedure calls
   // for all registered interfaces.
   // This call will not return until
   // RpcMgmtStopServerListening is called.
   return RpcServerListen(
      1, // Recommended minimum number of threads.
      RPC_C_LISTEN_MAX_CALLS_DEFAULT, // Recommended maximum number of threads.
      FALSE); // Start listening now.
}

// Naive security callback.
RPC_STATUS CALLBACK SecurityCallback(RPC_IF_HANDLE /*hInterface*/, void* /*pBindingHandle*/)
{
    return RPC_S_OK; // Always allow anyone.
}

int main()
{
   RPC_STATUS status;

   std::clog << "Calling RpcServerUseProtseqEp" << std::endl;
   // Uses the protocol combined with the endpoint for receiving
   // remote procedure calls.
   status = RpcServerUseProtseqEp(
      reinterpret_cast<unsigned char*>("ncacn_ip_tcp"), // Use TCP/IP protocol.
      RPC_C_PROTSEQ_MAX_REQS_DEFAULT, // Backlog queue length for TCP/IP.
      reinterpret_cast<unsigned char*>("4747"), // TCP/IP port to use.
      NULL); // No security.
   if (status)
      return HandleError("RpcServerUseProtseqEp", status);

   std::clog << "Calling RpcServerRegisterIf" << std::endl;
   // Registers the ContextExample interface.
   status = RpcServerRegisterIf2(
      ContextExample_v1_0_s_ifspec, // Interface to register.
      NULL, // Use the MIDL generated entry-point vector.
      NULL, // Use the MIDL generated entry-point vector.
      RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH, // Forces use of security callback.
      RPC_C_LISTEN_MAX_CALLS_DEFAULT, // Use default number of concurrent calls.
      (unsigned)-1, // Infinite max size of incoming data blocks.
      SecurityCallback); // Naive security callback.
   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();
}

// Memory allocation function for RPC.
// The runtime uses these two functions for allocating/deallocating
// enough memory to pass the string to the server.
void* __RPC_USER midl_user_allocate(size_t size)
{
   return malloc(size);
}

// Memory deallocation function for RPC.
void __RPC_USER midl_user_free(void* p)
{
   free(p);
}

Changes

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 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.

Context client

Time has come to write our client application that will connect to the server and use the new context handles.

Hello Client Context World!

// File ContextExampleClient.cpp
#include <iostream>
#include "ContextExample.h"

// Write a formatted error message to std::cerr.
DWORD HandleError(const char* szFunction, DWORD dwError);

int main()
{
   RPC_STATUS status;
   unsigned char* szStringBinding = NULL;

   std::clog << "Calling RpcStringBindingCompose" << std::endl;
   // Creates a string binding handle.
   // This function is nothing more than a printf.
   // Connection is not done here.
   status = RpcStringBindingCompose(
      NULL, // UUID to bind to.
      reinterpret_cast<unsigned char*>("ncacn_ip_tcp"), // Use TCP/IP protocol.
      reinterpret_cast<unsigned char*>("localhost"), // TCP/IP network address to use.
      reinterpret_cast<unsigned char*>("4747"), // TCP/IP port to use.
      NULL, // Protocol dependent network options to use.
      &szStringBinding); // String binding output.
   if (status)
      return HandleError("RpcStringBindingCompose", status);

   handle_t hBinding = NULL;

   std::clog << "Calling RpcBindingFromStringBinding" << std::endl;
   // Validates the format of the string binding handle and converts
   // it to a binding handle.
   // Connection is not done here either.
   status = RpcBindingFromStringBinding(
      szStringBinding, // The string binding to validate.
      &hBinding); // Put the result in the explicit binding handle.
   if (status)
      return HandleError("RpcBindingFromStringBinding", status);

   std::clog << "Calling RpcStringFree" << std::endl;
   // Free the memory allocated by a string.
   status = RpcStringFree(
      &szStringBinding); // String to be freed.
   if (status)
      return HandleError("RpcStringFree", status);

   std::clog << "Calling RpcEpResolveBinding" << std::endl;
   // Resolves a partially-bound server binding handle into a
   // fully-bound server binding handle.
   status = RpcEpResolveBinding(hBinding, ContextExample_v1_0_c_ifspec);
   if (status)
      return HandleError("RpcEpResolveBinding", status);

   RpcTryExcept
   {
      std::clog << "Calling Open" << std::endl;
      // Open the context handle.
      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;
      // Calls the RPC function. The hBinding binding handle
      // is used explicitly.
      Output(hContext);

      std::cout << "Press enter to call Close" << std::endl;
      std::cin.get();

      std::clog << "Calling Close" << std::endl;
      // Close the context handle.
      Close(&hContext);
   }
   RpcExcept(1)
   {
      HandleError("Remote Procedure Call", RpcExceptionCode());
   }
   RpcEndExcept

   std::clog << "Calling RpcBindingFree" << std::endl;
   // Releases binding handle resources and disconnects from the server.
   status = RpcBindingFree(
      &hBinding); // Frees the explicit binding handle.
   if (status)
      return HandleError("RpcBindingFree", status);

   std::cout << "Press enter to exit" << std::endl;
   std::cin.get();
}

// Memory allocation function for RPC.
// The runtime uses these two functions for allocating/deallocating
// enough memory to pass the string to the server.
void* __RPC_USER midl_user_allocate(size_t size)
{
   return malloc(size);
}

// Memory deallocation function for RPC.
void __RPC_USER midl_user_free(void* p)
{
   free(p);
}

Changes

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 it's 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.

Appendix

This section describes some techniques useful when using context handles in RPC applications.

Multithreading

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.

Server crashes

If the server becomes unavailable/crashes, the client application should call the function RpcSmDestroyClientContext to free its context data.

Conclusion

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.

References

Revision history

  • 2012-12-22:
    • Finally updated to work with Visual Studio 2010 and fixed security callback.
  • 2003-08-31:
    • Original article.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Anders Dalvander
Software Developer (Senior) Umetrics
Sweden Sweden
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralRe: Access Deniedmemberrishi785 Jun '08 - 5:02 
hi,
 
even i'm having the same problem. could anyone help us please?.. I'm using win xp and vstudio 2005.
 
thanks
rishi
GeneralRe: Access DeniedmemberKenryDang19 Oct '08 - 18:06 
Hi All,
I also met this problem, could anyone help me please?. I am using WINDOW XP sp3. VS 2008.
 
Thanks
pls email to kenrydang@yahoo.com
DHCUNG
 
kenry

GeneralRe: Access DeniedmemberAnders Dalvander19 Oct '08 - 19:53 
Just skip calling RpcMgmtIsServerListening, you can just call your RPC functions directly (such as Open in the example above) after calling RpcEpResolveBinding.
 
Regards,
Anders
GeneralRe: Access DeniedmemberKenryDang19 Oct '08 - 21:19 
Thank you for help. But i dont understand why using RpcMgmtIsServerListening in this example.
 
kenry

GeneralRe: Access DeniedmemberAnders Dalvander19 Oct '08 - 21:37 
RpcMgmtIsServerListening checks that the server RPC is up and running, but it seems it doesn't work on Windows 2000 and later (without registry tweaks).
 
The reason that the article and example hasn't been updated is that the process of updating published articles on CodeProject is rather complicated.
 
Regards,
Anders
GeneralRe: Access Denied [modified]memberaisoda2 Jun '12 - 5:55 
please let me know what are these registry tweaks?what should i do?
windows 7 and visual studio 10 sp1

modified 2 Jun '12 - 12:19.

GeneralRe: Access DeniedmemberAnders Dalvander2 Jun '12 - 10:00 
Read this[^]
GeneralRe: Access Deniedmemberaisoda2 Jun '12 - 23:50 
thank you for answering me ,but i do not understand what i do. :(
Generalobjects transfermembermahesh kumar s17 Nov '06 - 0:20 
I need to communicate between client and server (diff machines)..
there are structures with pointers inside which ineed to transfer from
client to server and vice versa..
client::Send(Struct * mystruct,size)
{

}
 
server::recv(Struct * mystruct,size)
{

}
I need to send recv those structure objects like a stream..imn with very little time delay..is this possible in RPC ? hw it is done ?will the objects retain their references after transmission ?
 
-Mahesh

GeneralRe: objects transfermemberAnders Dalvander17 Nov '06 - 0:52 
You'll need to serialize the object to a flat representation before sending it on the client, and deserialize the flat representation to an object on the server.
void Struct::Write(std::ostream& os) const
{
   member1.Write(os);
   member2.Write(os);
   WriteInt32(os, member3);
}
 
void Struct::Read(std::istream& is)
{
   member1.Read(is);
   member2.Read(is);
   member3 = ReadInt32(is);
}
For primitive types you could create some helper functions:
void WriteInt32(std::ostream& os, int32_t value)
{
   os.write(reinterpret_cast<const char*>(&value), sizeof(value));
}
 
int32_t ReadInt32(std::istream& is)
{
   int32_t value;
   is.read(reinterpret_cast<char*>(&value), sizeof(value));
   return value;
}
Or you could use the boost::serialization[^] library.
General'Example1_v1_0_s_ifspec' : undeclared identifiermembermaddyMathan13 Aug '06 - 16:20 
hi
im having this error and i could nto rectify it. Could anyone help one this.
error C2065: 'Example1_v1_0_s_ifspec' : undeclared identifier
please mail me if anyone know about it.
Prasat@apiit.edu.my
GeneralRe: 'Example1_v1_0_s_ifspec' : undeclared identifiermemberAnders Dalvander13 Aug '06 - 22:42 
Look in the MIDL generated header file, search for "ifspec" and use that instead.
 
Regards,
Anders
GeneralRe: 'Example1_v1_0_s_ifspec' : undeclared identifiermemberMember 858949911 Apr '13 - 22:41 
i have the same error but what do you mean use "ifspec" from the header file?
this is the ifspec code in the header:
extern RPC_IF_HANDLE Example1Explicit_v1_0_c_ifspec;
extern RPC_IF_HANDLE Example1Explicit_v1_0_s_ifspec;
 
what do we need to change?
status = RpcServerRegisterIf2(
Example1Explicit_v1_0_s_ifspec,
NULL,
NULL,
RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH,
RPC_C_LISTEN_MAX_CALLS_DEFAULT,
(unsigned)-1,
SecurityCallback);
Questionprotocol dependant network optionsmembertriendl.kj20 Apr '06 - 4:20 
I've just wondered in what form I should specify the protocol dependant network options to RpcStringBindingCompose()?
 
Are there some for TCP/IP; could I specify a read timeout on a socket?
 
--
klaus triendl
 

-- modified at 10:20 Thursday 20th April, 2006
GeneralVery nice articlememberimmihir16 Mar '06 - 9:08 
I googled it and this is by far the best intro to RPC. Thanks.Smile | :)
 
MP
 
-- modified at 15:13 Thursday 16th March, 2006
GeneralAuthentication of RPCmemberNick Zeng13 Mar '06 - 15:56 
Author:
It is said that RPC function can work under winxpsp2 through Authentication disposal, and I've make my codes changed through adding some Authentication functions:
client RpcBindingSetAuthInfo(Handle,
"localhost",
RPC_C_AUTHN_LEVEL_DEFAULT,
RPC_C_AUTHN_WINNT,
&AuthIdentity,
RPC_C_AUTHN_WINNT)
server RpcServerRegisterAuthInfo("localhost",
RPC_C_AUTHN_WINNT,
NULL,
NULL).
And the function does work without changing the registerkey or the groupedit in sp2, and now the new demand is coming that the SSP "RPC_C_AUTHN_WINNT" in these functions have to change to "RPC_C_AUTHN_GSS_KERBEROS" to improve the security level, but the new codes always make a exception of "1747" wich refers to the Unknown authen service, have you ever met this? I am hurry waitting for your callbacks.

 
Along the way to the North
GeneralRe: Authentication of RPCmemberAnders Dalvander13 Mar '06 - 20:51 
Hi Nick,
 
I've never used the authentication part of RPC, so I can't help you on this issue. Try to set the principal name in both the server and the client to NULL.
 
Regards,
Anders
QuestionSeems all works are for the rundown routine?memberzylthinking10 Mar '06 - 6:28 
if I implement the program as the article prevoius, without a typedef in idl file, passing back the pointer of the string stored in server to client as a handle, each call to server pass the handle as a parameter, while in server process virtual space the address of stored string keeped unchanged, so I can find it through the parameter, just like the context handle. So it seems the context handle is nothining but a mechanism to call the rundown routine when client offline crudely.
 
Is it right?
 
thanks.
 
zylthinking
 
-- modified at 12:31 Friday 10th March, 2006
GeneralRpcImpersonateClientmemberdevisscher8 Feb '06 - 1:44 
Hi,
 
i have to use "RpcImpersonateClient", but it return all the time error?
 
see below,
1. is it the correct statement (hBinding)
2. is it the correct place to do it ?
3. do i have to use any other security authority ?
 

Hope you have the time to give me some advice !
 

thanks,
Patrick
 
--------------------------------------------------------------
 
server :
 
CONTEXT_HANDLE Open(
/* [in] */ handle_t hBinding,
/* [string][in] */ const char* szString)
{
std::string* pContext = new std::string(szString);
CONTEXT_HANDLE hContext = pContext;
 
/****************************
RPC_STATUS status;
status=RpcImpersonateClient(hBinding);
if (!status)
Logevent("bad");
/*******************************
 
std::clog << "Open: Binding = " << hBinding
<< "; Context = " << hContext << std::endl;
return hContext;
}

 
Patrick
GeneralExcellent ArticlememberSteven_Henley22 Jan '06 - 19:16 
Mr. Anders,
 
I believe that this article to be well written and insightful.
However, I shall have to let you know whether my beliefs were
correct later on.
 
I am just a beginner with network programming and wish to
develop an application that will work across multiple platforms
to backup particular data from an application accross either
a LAN or the internet.
 
Could you give me some beginning resource ideas keeping in
mind that I am a bit lacking in terms of monetary resources.
 
I truly appreciate the fact that there are people out there,
such as yourself, who are extremely generous by contributing
their knowledge to those of us who are lacking.
 
Thanks,
Steven Henley
 
Far better to keep one's mouth shut
_ and appear stupid
 
than to open one's mouth
_ and remove all doubt.
 
I rarely follow this.
GeneralRe: Excellent ArticlememberAnders Dalvander22 Jan '06 - 21:15 
Hi Steven,
 
I'm sorry to tell you this, but I would not recommend RPC if you are aiming to support multiple platforms (Windows/Linux). Windows implements OSF DCE RPC, Linux often implements Sun ONC RPC, they are not compatible. There are implementations of OSF DCE RPC for Linux, but I don't know how well they perform or how stable they are. But if you have time to check these out they should give you less headache than rewriting RPC using sockets from scratch.
 
Regards,
Anders
GeneralRe: Excellent ArticlememberSteven_Henley23 Jan '06 - 7:54 
Hello Anders,
 
I truly appreciate the input. If not RPC, what would you recommend?
 
Please note I have the following requirements:
1. Works across multiple platforms.
a. initially Windows and Linux.
b. possibly later I may desire to expand into others.
2. File transfers may be large at times.
3. Must be internet based.
 
Again, I truly appreciate the input (better response time than most professors I had). I very much look forward to reading future articles that you author. So, know that at least one person out there very much values your time and effort.
 
I will check into OSF DCE RPC.
 
Thanks,
Steven
 
Far better to keep one's mouth shut
_ and appear stupid
 
than to open one's mouth
_ and remove all doubt.
 
I rarely follow this.
GeneralRe: Excellent ArticlememberAnders Dalvander31 Jan '06 - 3:31 
Take a look at the following articles written by Jarl Lindrud here at CodeProject:
 
http://www.codeproject.com/threads/Rcf_Ipc_For_Cpp.asp[^]
and
http://www.codeproject.com/threads/RMI_For_Cpp.asp[^]
 
// Anders
GeneralRe: Excellent ArticlememberAnders Dalvander4 Feb '06 - 23:19 
Also, take a look at ZeroC Ice: http://www.zeroc.com/ice.html[^].
 
// Anders Dalvander
General[broadcast], ncadg_ip_udp, datagrammemberjcarlile036 Jan '06 - 7:38 
Thanks for your two great articles on RPC. They're very enlightening
 
I noticed that all of your examples use the 'ncacn_ip_tcp' protocal. Do you have any examples of, or could you explain the [broadcast] IDL tag, the difference between ncacn_ip_tcp & ncadg_ip_udp, datagram, and how to broadcast a client call to all servers on a local network?
 
Basically, what I want to implement is a simple p2p network where an application running on each node will auto-configure based on what other nodes are present (all based on an integer ID number) when it comes alive. Does the server still need the same 'RpcServerUseProtseqEp', 'RpcServerRegisterIf2', 'RpcServerListen' functions? Does the client still use 'RpcStringBindingCompose', 'RpcBindingFromStringBinding' functions?
 
Thanks in advance.
 
Jen Carlile

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130516.1 | Last Updated 22 Dec 2012
Article Copyright 2003 by Anders Dalvander
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid