ITworld.com
  Search  
ITworld Home Page ITworld Webcasts ITworld White Papers ITworld Newsletters ITworld News ITworld Topics Careers ITworld Voices ITwhirled Changing the way you view IT
 

The paradox of Windows APIs

ITworld.com 3/13/01

Michael L. Perry, ITworld.com

Michael_Perry
Object-oriented languages are particularly well suited to application development -- in part because they're well suited to software modeling. An application programming interface (API) such as the Windows Software Development Kit (SDK) is often expressed in C. While C can help you ensure language compatibility, it also can hinder your ability to maintain object identity in software modeling. Because C is a procedural language, and not an object-oriented language, functions written in C sometimes fail to preserve object identity.

Through the Microsoft Foundation Classes (MFC), Windows provides several tools for working around the difficulties presented by Windows APIs. (Be aware that there's no ideal MFC fix, though.) In this article, I discuss SDK and MFC limitations and demonstrate a way to maintain the object identity that's so critical to coherent interobject communication.

API amnesia: Loss of identity
So why is C so popular for APIs? Well, there are a couple of reasons. First, the language is flexible; a C-based API can be called from programs written not only in C, but also in C++, Pascal, FORTRAN, and (with a little extra work) even Java. A second, compelling reason for the language's popularity is that C allows programmers to exchange pointers to functions, effectively rendering those functions as values. (On a 32-bit platform such as Windows, a function pointer is the 32-bit address where the function's first machine instruction is stored.)

C function-pointers can be passed and called with ease, and this is a boon to API developers, who often use C function-pointers to provide support for two-way dialogs between the system library and programmers' applications. This type of dialog might be initiated, for example, when an application calls a windowing function. To establish the dialog, an application would pass the system library a "callback" pointer that points to a specific application function. This allows the library to invoke that function via the callback pointer when its specialized processing is needed; the parameters and return value of such a callback function define a protocol.

This is where the problems arise. In software modeling, it is an interface's responsibility to define a protocol. But according to the rule of identity, an interface also identifies objects, and this identification lets you create a network of objects. Robust code enforces identification; objects must identify themselves to one another in order to clarify which object in a dialog responds to which message. If objects can't make identifications, mistakes are inevitable.

On its own, a C API does not define object identity; the API is a simple a contract between a library and an application, agreeing to behave in certain ways. Developers who use the SDK "raw", without a wrapper like MFC, are vulnerable to identity anomalies when working with Windows APIs. Consider, RegisterClassEx , a Windows class that takes a pointer to the function WindowProc as a parameter. When a registered class's window receives a message, RegisterClassEx calls WindowProc -- passing in the window's handle, the message's identification, and the message's parameters. RegisterClassEx and the WindowProc form two sides of a conversation, effectively preserving the contract that allows the API and the application to interoperate.

Unfortunately, this contract does not consider the identity of passed objects. The calling application gives RegisterClassEx a procedure for handling messages, but not an actual object to implement the handling. Consequently, any conversation between application and library is expressed as a process, and not as a network of interconnected objects. In order to create the network, object identity would have to be maintained.

To understand the problems of using the raw SDK, let's compare it with the use of an MFC solution. An application written using raw SDK components generally contains many global variables, whereas an application written with the MFC wrapper uses distinct classes. A raw SDK application typically creates only one window from each of its classes, while an MFC application often creates several instances of both windows and classes. So you can see that an SDK application is more difficult to maintain -- and to use -- than its MFC counterpart. As we will see later, though, the MFC still does not present a complete solution.

Ideally, the application identifies the object
Microsoft has made some allowances for the use of objects instead of processes: a programmer's application is allowed to pass an API the referenced object's identity in addition to the callback function-pointer. The CreateWindow API, for example, provides the lpParam parameter.

When developing an application, you might be tempted to cast your window object to a void pointer and pass it through lpParam . Responding to a message, you could cast the void pointer back to the correct object type, thereby discovering the identity of the object. Unfortunately, this would not work in all cases. Many built-in window types, such as MDI child windows and dialog boxes, already use lpParam for their own purposes. You need another mechanism to preserve the identity of window objects.

Handle mapping: Take what you can get
The MFC wrapper solves this window-identity with handles (HWND, unique, unsigned 32-bit integers that the Windows API uses to identify a specific window), which are the Windows SDK's built-in mechanism for managing object identity in a windowing application.

But using handles to manage identity presents a couple of problems. First, the API assigns a window's identity. At runtime, however, the window handle must be mapped to an object that's defined by an outside program. When using handles, the API does not directly call outside objects. Instead, applications must use a hash or search algorithm to look up the objects associated with specific window handles. This extra overhead wastes precious memory and time.

The second problem with handle mapping is that a window's handle is not known until the window is fully created. Since handles are returned from the API CreateWindow , logic dictates that the handles should be mapped to their associated objects right after the window is created. Unfortunately, by the time CreateWindow returns the handle value, the Windows API has already sent the new window multiple messages, including WM_CREATE .

For the application object to properly respond to the window's messages, the object must be mapped to the window's handle as early as possible. MFC jumps through many a hoop to accomplish this early mapping. (For a lesson in programmatic prestidigitation, please see CWnd::CreateEx in the MFC source code. It's quite educational.)

To illustrate the use of handles for simulating identity, I have developed a simple object-oriented wrapper to the Windows SetTimer API. This wrapper maps the timer ID to an application-defined object, replacing the C callback function with a C++ virtual function. Unlike the CreateWindow API, the SetTimer API returns without invoking the callback function, eliminating the need for early mapping.

Here's the code:

 
class Ctimer
{
public:
 CTimer( UINT uElapse );
 virtual ~CTimer();

 virtual void OnTimer( DWORD dwTime ) = 0;

private:
  UINT m_nID;
};

typedef std::map< UINT, CTimer * > TimerMap;
TimerMap g_Timers;
VOID CALLBACK CTimer_TimerProc(
  HWND hwnd,
  UINT uMsg,
  UINT idEvent,
  DWORD dwTime )
{
  TimerMap::iterator itTimer =
    g_Timers.find(idEvent);
  if ( itTimer != g_Timers.end() )
    itTimer->second->OnTimer( dwTime );
}

CTimer::CTimer( UINT uElapse )
{
  m_nID = SetTimer(
    NULL,     1,
    uElapse,
    CTimer_TimerProc);
if ( m_nID != 0 )
    g_Timers.insert(
      TimerMap::value_type(
        m_nID, this ));
}

CTimer::~CTimer()
{
  TimerMap::iterator itTimer =
    g_Timers.find( m_nID );
  if ( itTimer != g_Timers.end() )
    g_Timers.erase( itTimer );
  KillTimer( NULL, m_nID );
}

If the API offers no help at all, play dirty
Sometimes a Windows API doesn't offer any mechanism at all to support object identity maintenance. The callback parameters don't include a self-imposed identity nor do they regurgitate an application-provided identity. Under these circumstances, an application developer must reach deep into his or her bag of seldom-used (and often unpleasant) tricks.

In this case, you must call upon the dreaded self-modifying code. When you can't use a parameter to send an object's identity to an API, you must send it via the callback function pointer. This way, you can dynamically construct a separate callback function for each application object, instead of using just one callback function for all of them. The trick is that each dynamically constructed application function contains a "hard-coded" pointer to its associated application object.

To illustrate the use of self-modifying code, I have developed a not-so-simple wrapper for the Windows SetWindowsHookEx API. Because this API provides no native mechanism for identifying an object, my wrapper places the API's object pointer into the application's callback function. The callback function then forces the assignment of the this pointer to the API's pointer function before allowing the function pointer to jump to the object's method.

Take a look at the code:


class CHook
{
public:
  // Put the callback first in the vtable.
  virtual LRESULT HookProc(
    int nCode,
    WPARAM wParam,
    LPARAM lParam );

  CHook( int idHook );
  virtual ~CHook();

  virtual void OnHook(
    int nCode,
    WPARAM wParam,
    LPARAM lParam ) = 0;

private:
  HHOOK m_hHook;
  char m_HookProc[12];
};

CHook::CHook( int idHook )
{
  DWORD dwThis = (DWORD)this;

  // mov ecx, this
  m_HookProc[0] = (char)0xB9;
  m_HookProc[1] = (char)dwThis & 0xFF;
  m_HookProc[2] = (char)(dwThis >> 8) & 0xFF;
  m_HookProc[3] = (char)(dwThis >> 16) & 0xFF;
  m_HookProc[4] = (char)(dwThis >> 24) & 0xFF;
  // mov eax, [this]
  m_HookProc[5] = (char)0xA1;
  m_HookProc[6] = (char)dwThis & 0xFF;
  m_HookProc[7] = (char)(dwThis >> 8) & 0xFF;
  m_HookProc[8] = (char)(dwThis >> 16) & 0xFF;
  m_HookProc[9] = (char)(dwThis >> 24) & 0xFF;
  // jmp dword ptr [eax]
  m_HookProc[10] = (char)0xFF;
  m_HookProc[11] = (char)0x20;

  m_hHook = SetWindowsHookEx(
    idHook,
    (HOOKPROC)(void *)m_HookProc,
    NULL,
    GetCurrentThreadId() );
}

CHook::~CHook()
{
  UnhookWindowsHookEx( m_hHook );
}

LRESULT CHook::HookProc(
  int nCode,
  WPARAM wParam,
  LPARAM lParam )
{
  OnHook( nCode, wParam, lParam );
  return CallNextHookEx(
    m_hHook,
    nCode,
    wParam,
    lParam );
}

Words of caution
You should note that the above self-modifying code is written partially in machine language, meaning that the code is both machine- and compiler-specific. It will work only on Intel 386 hardware or higher and only with Microsoft Visual C++.

One last qualifier: you should use self-modifying code only when all other means fail. The technique circumvents compiler safeguards such as type checking; therefore, problems that the compiler would usually catch could go unnoticed. Self-modifying code can also be difficult to debug, as inserting a breakpoint into a dynamically generated function is tricky business. The mere fact that I use self-modifying code in this specific case should underscore the necessity that interfaces identify objects.

Michael L. Perry has been a professional Windows developer for over six years and maintains expertise in COM+, Java, XML, and other technologies currently shaping the programming landscape. He formed Mallard Software Designs in 1998, where he applies the mathematical rigor of proof -- establishing the correctness of a solution before implementing it -- to software design. He is the moderator of ITworld.com's Windows Application Development discussion.





 
www.itworld.com    open.itworld.com     security.itworld.com     smallbusiness.itworld.com
storage.itworld.com     utilitycomputing.itworld.com     wireless.itworld.com

 
Contact Us   About Us   Privacy Policy    Terms of Service   Reprints  

CIO   Computerworld   CSO   GamePro   Games.net   Industry Standard   Infoworld   ITworld  
JavaWorld   LinuxWorld  MacUser   Macworld   Network World   PC World   Playlist  

DEMO   IDG Connect   IDG Knowledge Hub   IDG TechNetwork   IDG World Expo  

Copyright © Computerworld, Inc. All rights reserved

Reproduction in whole or in part in any form or medium without express written permission of Computerworld Inc. is prohibited. Computerworld and Computerworld.com and the respective logos are trademarks of International Data Group Inc.