sil2100//vx developer log

Welcome to my personal web page

A tale of IBus, GIR and queries

Recently I have been working a lot on making IBus autopilot tests more reliable. The design was simple: fetch the expected resulting characters from IBus and compare those with what actually got written in the Unity Dash/HUD text entry. Simple, right? Wrong. IBus doesn't really allow you to just ask him: hey, when I give you the string "abc", what output string will I get for the current engine? You can do it if you explicitly use the input_context, ok. Hell starts when you want to use Python and the gi.repository IBus bindings (not the old python-ibus ones). Why? Because someone made some essential methods Introspectable="0" for no reason. But let's see how we can actually do without those. This post is a quick report from my battle with gobject-introspection of IBus in Python - with all my other trials and approaches. A lot of those!

2013-02-23 22:53

Empty

Context: IBus is an input method for Asian languages. To test IBus in Unity, we enable IBus, write input strings to the search entries and check if what we get is what IBus should return. Normally, we had hardcoded 'result' values for every input, but this is not the right way to go - since IBus won't always return the same thing, as the most probable result is dependent on previous selection choices. History could be removed before test execution, yes, but then the ibus-daemon would have to be restarted. Otherwise history will not flush.
Current best solution: querying IBus by ourselves, outside of Unity, and checking the lookup table for a given input string. But that's easier said than done.

For this purpose I wrote a quick Python application that does a direct query to IBus using a separate input context. I registered the project on Launchpad and integrated it into the autopilot Unity integration tests.
Project page: ibus-query
Bazaar branch: lp:ibus-query

Diagram showing an overview of how ibus-query works

The first thing I tried was hooking up to an existing input context of the Dash and maybe listening in to the communication between IBus and nux. Sadly this did not work for reasons unknown to me. Besides, there's still the problem of deciding when the actual input session is finished - i.e. which commit-text signal is the final one. Since we need to know the results while the input context is still focused.
So right now? We create a new InputContext, hook up to some important context signals (such as commit-text, disabled etc.), push the input query as a series of context.process_key_event() calls, run a GLib.MainLoop and return the result that we get from the previously connected signal callbacks.

But this all brings us to the topic of this post: gobject-introspection. Since the code was supposed to be used in Autopilot, I could not have used the python-ibus python bindings. These were using the old glib python bindings, while the current Autopilot uses GLib from GIR. The rule is, old glib bindings cannot be mixed with the new gobject-introspection repository bindings, as it results in undefined behavior (i.e. does not work).
For those who don't know - GIR is a new way of exporting existing, non-python code to Python users. Projects that want to have their code exported create a .gir file with definitions of which GObject functions should be introspectable for Python. This file is usually auto-generated.

So I had to use the GIR bindings. But, as mentioned earlier, I had to workaround the lack of one of important function for my usage - just because it wasn't set as introspectable in the source code. But instead of using the create_input_context() (documentation), I looked up on what theoretically is done during the function call and did those operations myself. It's less portable, but works. Consider the following code:


  # Standard thing - create the IBusBus object
  self._bus = IBus.Bus()

  # This part here is basically what self._bus.create_input_context() would do:
  # Create a DBus connection with the ibus-daemon
  try:
      self._dbusconn = dbus.connection.Connection(IBus.get_address())
  except:
      print "Error! The ibus-daemon doesn't seem to be running."
      quit()

  # Connect to the IBus object and interface
  ibus_obj = self._dbusconn.get_object(IBus.SERVICE_IBUS, IBus.PATH_IBUS)
  self._iface = dbus.Interface(ibus_obj, dbus_interface="org.freedesktop.IBus")

  # Now, this is the simple part - the DBus IBus interface simply exports a method
  # that basically does all that create_input_context() should do - allocate and return
  # the bus connection path for our new input context
  path = self._iface.CreateInputContext("IBusPoll")

  # All that is left is to create a new InputContext object
  # Here we also basically work-around some GIR stuff by directly calling the new()
  # method of the InputContext
  self._context = IBus.InputContext.new(path, self._bus.get_connection(), None)

For this to be possible, I had to browse a lot of IBus source code. But before I decided to work-around the missing method through introspection, I wanted to use another method which strangely is introspectable. The ibus_bus_create_input_context_async(). Now what sense does it make? The synchronous method is not available, but async is fine. Of course, to add to the craziness, ibus_bus_create_input_context_async_finish() is not introspectable as well. But I thought I can use the one asynchronous version of create_input_context() and be done with it.

So, the async method more or less uses something from GLib called a GAsyncReadyCallback. A callback like this is given a GAsyncResult object as its argument; this object includes the result of the operation that got finished/updated. It's also available in Python of course, no deal. The only problem is, when using the SimpleAsyncResult version in Python, you can only return the actual result as a boolean (gboolean) or numeral (gssize). So, in my case, it's not useful at all. Especially in Python.
There's no easy-to-find documentation what is really available from those functions in the newer Python GIR GLib bindings, but I decided not to dig further, searching for a solution elsewhere.

But back to my final solution used in ibus-query. On every input character we emit a process_key_event signal, which sends the character to the engine. We do that without a main loop started. The signals are emitted synchronously and sent futher to ibus-daemon through DBus. To receive the results from the daemon, we need to start a GLib.MainLoop, receiving the commit-text, update-preedit-text and disabled signals back, acting accordingly (signal descriptions). We used the disabled signal to our advantage as the mark of when we finished processing all interesting events and when we can quit() the MainLoop.

After a safe testing period and fixes, the code of ibus-query got also merged in into the Unity autopilot test suite for testing IBus (merge request). The method is still not perfect - currently it's not working for the Korean hangul input engine because of some engine implementation differences. I hope to fix that pretty soon as well.

From other news, did you hear about our Ubuntu Tablet? It's a very neat addition to our family of products - let's see where this road will take us.