How does wxHaskell event handling work – part 1

Some Background

As some of you may have noticed, there has been a minor flurry of activity (if that’s not an oxymoron) from me recently. The wxHaskell repo went offline after the attack on and I decided to look at support for wxWidgets 2.9 as well as 2.8 while I was waiting for the repo to be restored.

Something I thought I had fixed (would have known better if I had been reading my mail more carefully) was a bug found by Eric when using the native Mac wxWidgets. Basically Eric was seeing an error dialog indicating an assertion failure, which it seemed to be safe to ignore, when starting an application.

The offending code, which you will find in eljevent.cpp, was:

EWXWEXPORT(wxClosure*,wxEvtHandler_GetClosure)(wxEvtHandler* evtHandler,
                                               int id,int type)
  wxCommandEvent  event(type,id);     //We can use any kind of event here
  wxCallback*     callback = NULL;
  bool            found    = false;

  getCallback = &callback;
  found = evtHandler->SearchDynamicEventTable( event );
  getCallback = NULL;

  if (found && callback)
    return callback->GetClosure();
    return NULL;

I put in a workaround for the problem (green text below) in what looked like an obvious way (given that ignoring the problem seemed OK – and yes, I know that this is a horrible hack)

if (evtHandler->GetDynamicEventTable() != NULL)
 found = evtHandler->SearchDynamicEventTable( event );

Unfortunately, Eric had already discovered that ignoring the problem wasn’t really an answer

I’m afraid this causes the (surprise!) event handlers for some list boxes to stop working 😦

That’ll teach me not to take the time to understand a problem before ‘fixing’ it. OK, so Laziness and Impatience failed. Time for Hubris to take over and I tweeted that I’d look into the problem properly.

In fact, the code in wxEventHandler_GetClosure() is rather dubious in a couple of respects. The first is that the static global ‘getCallback’, is clearly a very long-standing hack (the repo source code notes this – I tend to remove comments to keep blog entries to some sort of sane length). The second is that wxWidgets doesn’t document wxEvtHandler::SearchDynamicEventTable() at all. Since wxWidgets is generally very well documented, this is a warning…

How does Event handling normally work in wxWidgets?

There are a couple of ways to do event handing in wxWidgets. The simplest, which I’m not going to explore further, is to use event macros. This won’t help us in wxHaskell as we want to define our event handlers in Haskell. The solution is to use wxEventHandler::Connect() to associate a function call with an event. This means that our supplied function will get called when the event occurs. A little plumbing will be needed to allow the called function be be written in Haskell, which we’ll look at later.

So what happens when an event occurs? The following is shamelessly cribbed from the wxWidgets documentation:

When an event is received from the windowing system, wxWidgets calls wxEvtHandler::ProcessEvent on the first event handler object belonging to the window generating the event.

The normal order of event table searching by wxEvtHandler::ProcessEvent() is as follows:

  1. If the object is disabled (via a call to wxEvtHandler::SetEvtHandlerEnabled) the function skips to step (6).
  2. If the object is a wxWindow, ProcessEvent is recursively called on the window’s wxValidator. If this returns true, the function exits.
  3. SearchEventTable is called for this event handler. If this fails, the base class table is tried, and so on until no more tables exist or an appropriate function was found, in which case the function exits.
  4. The search is applied down the entire chain of event handlers (usually the chain has a length of one). If this succeeds, the function exits.
  5. If the object is a wxWindow and the event is set to set to propagate (in the library only wxCommandEvent based events are set to propagate), ProcessEvent is recursively applied to the parent window’s event handler. If this returns true, the function exits.
  6. Finally, ProcessEvent is called on the wxApp object.

Connecting event handlers in wxHaskell

Now that the outline of event handling in wxWidgets is clear, let’s look at events in wxHaskell. I’m going to do this starting from the WXCore library as the WX library, while simpler to use, is just a Haskell wrapper around WXCore.

Widget wrappers need to provide at least two event-handler related functions: one to set the event handler and another to retrieve it. The setters and getters resemble the following (this example for Button):

buttonOnCommand :: Button a -> IO () -> IO ()
buttonOnCommand button eventHandler
  = windowOnEvent button [wxEVT_COMMAND_BUTTON_CLICKED] eventHandler (\evt -> eventHandler)

buttonGetOnCommand :: Window a -> IO (IO ())
buttonGetOnCommand button
  = unsafeWindowGetHandlerState button wxEVT_COMMAND_BUTTON_CLICKED skipCurrentEvent

The setter calls a generic event handler setter for everything deriving from Window with the control to which the handler applies, a list of the events for which the handler applies and an event handler function which will be called when the event occurs. One small point to note is that the event handler is passed twice: once as a piece of state and a second time wrapped in a lambda function.

The getter function uses an unsafe function to obtain the event handler for the requested event.

Digging further into the setters

Looking further into windowOnEvent, what do we have?

windowOnEvent :: Window a -> [EventId] -> handler -> (Event () -> IO ()) -> IO ()
windowOnEvent window eventTds state eventHandler
  = windowOnEventEx window eventIds (\ownerDelete -> return ()) -> eventHandler

Similarly, windowOnEventEx is defined as:

windowOnEventEx :: Window a -> [EventId] -> handler -> (Bool -> IO ()) -> (Event () -> IO ()) -> IO ()
windowOnEventEx window eventIds state destroy eventHandler
  = do
    let id = idAny
    evtHandlerOnEvent window id id eventIds state destroy eventHandler

In other words, we eventually transform the event handler for any given control into a generic event handler for a Window a (remember, using phantom types, wxHaskell arranges that all controls eventually resolve to a Window a), and this contains a function (strictly an IO computation) which is run when the event is triggered, and a computation which is run when the event handler is deleted. This computation takes a True argument if the owner is deleted and a False argument if we are simply disconnecting the event handler.

Digging further, we have:

evtHandlerOnEvent :: EvtHandler a -> Id -> Id -> [EventId]
                  -> handler -> OnEvent
evtHandlerOnEvent object firstId lastId eventIds state destroy eventHandler =
  do evtHandlerOnEventDisconnect object firstId lastId eventIds
     evtHandlerOnEventConnect object firstId lastId eventIds state
                              destroy eventHandler

The call to evtHandlerOnEventDisconnect simply ensures that any existing event handler is cleaned up before we install a new one. In this investigation we are more interested in:

evtHandlerOnEventConnect :: EvtHandler a -> Id -> Id -> [EventId]
                          -> state -> OnEvent
evtHandlerOnEventConnect object firstId lastId eventIds state
                         destroy eventHandler =
  do closure <- createClosure state destroy eventHandler
     withObjectPtr closure $ \pclosure ->
     mapM_ (connectEventId pclosure) eventIds
    connectEventId pclosure eventId =
      evtHandlerConnect object firstId lastId eventId pclosure

We are finally getting close to where we connect up with the wxWidgets framework. The Closure a type is derived (using phantom types) from wxObject a which is itself an instance of Object a, which is basically a way of storing a C pointer.

data Object a = Object !(Ptr a)
              | Managed !(ForeignPtr (TManagedPtr a))

Now, for our purposes, we are generally using the simple case of a Ptr a Object, so looking back up at evtHandlerOnEventConnect, what we first do is to create a Closure instance:

createClosure :: state -> (Bool -> IO ()) -> (Event () -> IO ())
              -> IO (Closure ())
createClosure st destroy handler =
  do funptr  <- wrapEventHandler eventHandlerWrapper
     stptr   <- newStablePtr (Wrap st)
     closureCreate funptr (castStablePtrToPtr stptr)
     eventHandlerWrapper :: Ptr fun -> Ptr () -> Ptr (TEvent ()) -> IO ()
     eventHandlerWrapper funptr stptr eventptr =
     do let event = objectFromPtr eventptr
        prev <- swapMVar currentEvent event
        if (objectIsNull event)
          do isDisconnecting <- varGet disconnecting
             destroy (not isDisconnecting)
             when (stptr/=ptrNull)
               (freeStablePtr (castPtrToStablePtr stptr))
             when (funptr/=ptrNull)
               (freeHaskellFunPtr (castPtrToFunPtr funptr))
        else handler event
        swapMVar currentEvent prev
        return ()

In createClosure we wrap some state information, an event handler action and an action to perform when the event handler is disconnected. We obtain stable pointers to these (if they are not already stable pointers) and construct a closure which contains them. This construction can be safely passed to the C++ world without risk of being garbage collected in the Haskell world.

If we now go back to evtHandlerOnEventConnect, it should hopefully be clear that we are calling evtHandlerConnect with a window, a set of event IDs to handle and a closure containing the Haskell components which will be invoked when an event arrives.

Having looked at how event handlers get connected in wxHaskell, next time we’ll look at how they get called in some more detail.


One thought on “How does wxHaskell event handling work – part 1”

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )


Connecting to %s