Friday, July 5, 2013

Event Handling in wxPython

For the last several days I've been working on getting event handling working, so I figured I could tell you a bit about how event handling is implemented in wxPython.

wxWidgets Background


First of all some background about wxWidgets for those unfamilar.  (You can skip this section if you are familar at all with wxWidgets or wxPython)

Event handling in wxWidgets is more or less the same as in other GUI toolkit.  Events can be generated by the library, for example when the user clicks on a button or a timer fires, or can be created by the programmer.  The actual handling of the events occurs in wxEvtHandler, from which most of the widgets in wx are derive.  When a wxEvtHandler instance is created, a parent can be specified.  This will allow the parent to handle any events generated by the child (or its children) that it doesn't handle itself.  These events are manifested as C++ objects deriving from wxEvent and may hold details about the event that has occurred.

The programmer can specify an action for a wxEvtHandler to take when it encounters an event of a particular type (and optionally from a particular widget.)  This is described as connecting the handler to the event.  The action for the handler to take is specified in the form of a C++ callback.  Originally, the way to connect a handler to events was using compile time "event tables," which are limited to calling methods on the wxEvtHandler object they are defined on.  In modern versions of wxWidgets, it is possible to dynamically connect and disconnect a wxEvtHandler to an event at runtime. Additionally, arbitrary methods as well as functions and functors may be used as callbacks for these dynamic connections.

For a better/more detailed explanation see the wxWidgets documentation (or for the C++ averse the wxPython version)

In wxPython


For events to be useful to Python programmers, wxPython must these two things possible:  using arbitrary Python callables to handle events and creating new events.

The first feature is actually relatively straight forward for the existing wxPython bindings.  A C++ function is used as the callback that is passed to the library, which calls the Python callable. To get the pointer to the Python object to the C++ function, a C++ object that wraps the pointer is given as user data for the event.  wxWidgets makes this user data object available to the C++ callback that handles the event and takes ownership of the object, deleting it if/when the event is disconnected.

The second feature is a little more complex.  We want events created in Python to retain their Python attributes when they reach their callbacks.  (You can see an example of this here)  This sounds easy, but wxWidgets internally makes copies of the event objects. When the C++ event object is passed to the callback, it maybe at a different address than the original object, thus making it difficult to find the correct Python object to pass to the Python callable.

wxPython's solution to this is to have a PyEvent object that is Python aware and able to carry it's attributes through C++ unmolested.  It does this by storing a pointer to a Python dict object inside the C++ PyEvent object.  When the event object gets cloned, the pointer gets cloned as well (and its refcount gets incremented.)  The Python PyEvent  object defines __{set,get,del}attr__ methods that redirect to the aforementioned dict object. [1]  This way, even if the Python PyEvent objects are different and/or wrap different C++ objects, they will have the same attributes as far as the user is concerned.

Differences for wxPython-cffi


For the first feature, my solution is pretty much the same with one small twist:  PyPy doesn't have a C API that allows a Python object to be called from C++ like CPython does.  To cope with this, there are a couple of small changes that need to made.  First of all, instead of passing a pointer to the Python callable, we pass an handle created with ffi.new_handle()[2].  Second, we call a constant Python callback from the C++ callback that is connected to the event.  In this Python function we lookup the Python callable with ffi.from_handle(), create the event object, and then finally call the Python object.

This is an indication of what has been and I suspect will continue to be a theme in the project:  replacing C++ code with equivalent Python code


The second feature is a bit more difficult and I don't quite have a perfect solution right now.  What I have in place right now starts out similar to wxPython:  we hold a pointer inside the C++ PyEvent object.  Where its different is the pointer is in fact a wxSharedPointer and it points to a wrapper around a handle to a Python PyEvent object.  The idea here is that the wrapper has a destructor that will call a Python callback to release the reference to the handle so it can be garbage collected.  The wxSharedPointer is a refcounted autopointer and makes sure that the wrapper's dtor is only called when every PyEvent object pointing to it has been deleted.  The problem with this plan is the handle keeps the Python PyEvent object it represents alive, which in turn keeps the last C++ PyEvent object alive.  My thought process was that I wanted to be able to pass the original PyEvent object to the callbacks, bypassing the need to use a separate dictionary and the attribute access methods.

I'm still working out exactly how I'll handle this, but once I have it worked out, the only part of event handling left to work is releasing handles to Python callables once they've been disconnected.  I'll probably write another post once I know how that will work.


1. Its useful to note accessing the dictionary like this is possible because PyEvent, like the vast majority of wxPython types, is implemented in C and so can directly access the data members of the C++ objects it wraps.

2.  ffi.new_handle() is new in CFFI 0.7, which has yet to be released at the time of writing. See near the bottom of Misc methods on ffi in the CFFI documentation for more information.

No comments:

Post a Comment