Intercepting Keyboard Eventsby Christian Starkjohann
This article describes the inner workings of the iJect kernel extension. This kernel extension is a prototype for applications which have to intercept keyboard or mouse events, for instance for sniffing keystrokes or reassigning keys at the driver level. iJect reassigns CDROM eject on the new iBook from function key F12 to Apple-F12, making unintentional CD ejects unlikely.
For more information about iJect for the new iBook click here...
The Key ProblemWe want to modify the data of keyboard events before it reaches an application. An analysis of the keystrokes' data path from the device driver to applications reveals the following (simplified) graph:
There are three points where the information flow can, in principle, be intercepted:
A discussion of these three options follows: Replacing the Keyboard DriverIf we replaced the keyboard driver, we could reassign any key we like, fork off additional data pathes for sniffing and so on. However, the disadvantage of this approach is that we have to provide a modified driver for each keyboard currently in use and that we would have to keep this driver up to date if Apple implements new features. Some of the keyboard drivers may not even be available in Darwin. We therefore decided against this option. Registering with the Window ServerIn Openstep (the ancestor of Mac OS X), the Display Postscript server was well documented. It was possible to send arbitrary Postscript code down to the window server, even short Postscript programs which hooked into the keyboard event data path and initiated arbitrary actions depending on keyboard events. We are sure that the Quartz engine (which is based on PDF instead of Postscript) is capable of doing similarly complex things. Unfortunately it is very new and not publicly documented. We therefore don't know how to intercept keyboard data at this level. Intercepting at the IOHIDSystem DriverThe best place to intercept the data flow with current knowledge turned out to be the IOHIDSystem class in the kernel. Unfortunately this class is statically linked into the kernel, it's not possible to replace it with a custom driver. The main challenge was to replace the kernel's implementation of IOHIDSystem with a slightly modified version. If the IOKit had been written in Objective-C instead of C++ (like its ancestor, the DriverKit), things had been simple: We could create a subclass of IOHIDSystem and tell it to "Pose As" the IOHIDSystem class. Every creation of an IOHIDSystem object would have been an instance of our subclass. Since this is real life and not fiction, it has not been so easy. We nevertheless implemented it the Objective-C way as posing subclass, but we had to do some low-level C++ runtime hacking. iJect contains a subclass of IOHIDSystem which overwrites two virtual methods which process keyboard events. The modified versions of the methods simply call the superclass implementations, or they ignore the event by not calling the superclass implementation. The tricky part is how to make the kernel use our subclass instead of IOHIDSystem. It's necessary to understand how virtual C++ methods work. Instances of classes which have virtual methods have an additional (hidden) instance variable: a pointer to a v-table. This v-table is a function pointer table pointing to the implementations of the virtual methods. To implement the posing, we simply overwrite the pointer to IOHIDSystem's vtable with a pointer to our class' v-table. This turns the IOHIDSystem instance into an instance of our class. DiscussionOf course, this kind of hack is not as clean as it would have been in Objective-C. First we rely on the compiler's implementation. The layout of the v-table (and whether such a table is used at all) is not specified by the C++ standard. The compiler implementers are free to change it at any time they like. However, it's extremely unlikely that the GNU compiler will change in this respect. It would break all loadable code such as kernel drivers and extensions (they would have to be recompiled). Second (and more severe) there is the "Fragile Base Class" problem which plagues all C++ programs. The problem is that the v-table layout is determined at compile time. Our subclass has a v-table which conforms to IOHIDSystem at the time we compiled. If Apple decides to add virtual methods to the class, remove some or just to re-arrange the methods, our subclass will break. This is a general problem of IOKit, not just of the implementation of iJect. All drivers are subclasses of IOKit classes and a modification in the interface of the IOKit classes will break the drivers. In Objective-C, the equivalents of v-tables are computed at runtime, not at compile time. Objective-C therefore has no "Fragile Base Class" problem as far as instance methods are concerned. The size of the instsance variable section must still be preserved between versions, even in Objective-C. This is reasonable because we really need the efficiency of compile-time binding for access to data items. The only way to cope with the problem currently is to provide an updated version of iJect for incompatibly modified new kernels. ConclusionAlthough the C++ runtime lacks many of the features which make Objective-C so powerful, it has been possible to implement the concept of a "posing subclass" for a class of Apple's IOKit. This enabled a functionality which would otherwise have been impossible. |