glib mainloop sources in python (e.g. for irclib)

Rick Spencer pulled me over today to help with an IRC client that he’s working on. He’s using python irclib to talk to IRC and Gtk/Webkit for the UI. The trouble with that combination is that the two are not using the same mainloop.

We sat down for a while and finally figured out that the ‘IRC’ object in irclib has three hooks for user-provided functions to help with this:

  1. add fd watch
  2. remove fd watch
  3. add timeout

The fd watch functions are passed a python socket object to add or remove a watch for. The intent is that you will watch for the socket becoming readable. As far as I can tell, irclib always performs blocking writes on the assumption that it won’t be a problem.

GLib lacks functionality for easily hooking up watches for fds (although we have some proposals for that in bug #658020 which I will be looking at more closely soon). You can use GIOChannel but that’s always somewhat annoying and as far as I can tell cannot be used from Python with a unix fd (possibly due to a binding issue?). The remaining solution is to implement a GSource from python, which is tricky. Rick made me promise I’d blog about the code that I came up with to help with that. Here it is:

class SocketSource(GLib.Source):
    def __init__(self, callback):
        GLib.Source.__init__(self)
        self.callback = callback
        self.pollfds = []

    def prepare(self):
        return False

    def check(self):
        for pollfd in self.pollfds:
            if pollfd.revents:
                return True

        return False

    def dispatch(self, callback, args):
        self.callback()
        return True

    def add_socket(self, socket):
        pollfd = GLib.PollFD(socket.fileno(), GLib.IO_IN)
        self.pollfds.append(pollfd)
        self.add_poll(pollfd)

    def rm_socket(self, socket):
        fd = socket.fileno()
        for pollfd in self.pollfds:
            if pollfd.fd == fd:
                self.remove_poll(pollfd)
                self.pollfds.remove(pollfd)

The callback function provided to the constructor is called when one of the sockets becomes ready for reading. That maps nicely for irclib’s process_once function. The add_socket and rm_socket fit nicely with irclib’s fn_to_add_socket and fn_to_remove_socket. Using the code looks something like so:

simple = irclib.SimpleIRCClient()
source = Socketsource(simple.ircobj.process_once)
simple.ircobj.fn_to_add_socket = source.add_socket
simple.ircobj.fn_to_remove_socket = source.rm_socket
source.attach()

Timeouts are left as an exercise to the reader.