Steve Frécinaux

Mathusalem, tasks and interfaces

Mathusalem has made some progress since last post, it’s now able to do nearly everything it should (keep in mind that I’m targetting basic functionnalities at first), but I’m currently facing an implementation issue on the multiple task interfaces side. I guess I missed something or took the problem from the wrong side, so take this post for what it is: a request for comments.

But let’s begin with some background details:

The objects involved

Mathusalem, as it can be seen from the D-Bus point of view, is made of two kinds of objects: the manager and the tasks, each of those registered on the bus with an unique name, as you can see on last week’s screenshot. The former only handles the registering of tasks, and will provide the ability to run a query on the task list at some point, while most of the interactions are done directly with each task object, the good ol’way.

Things become tasty when thinking about this task object. To fully understand how it is intended to work, you have to keep a few use cases in mind, for instance these three ones:

  • File copy and download
  • CD/DVD burning
  • IM client connection

You can easily imagine such tasks have a few common properties (like the task title or the progress status), but they also have different requirements: copying will obviously involve files, burning will block the DVD drive. Third party app would gain from being aware of these, to show progress status on files while downloading, or to tell why the drive isn’t available in an error message.

The way I’d like to implement it is using D-Bus interfaces. Tasks involving files would implement the FileOperation interface, Task involving devices would implement DeviceOperation, and so on. Well, it’s just some kind of inheritance, each task inheriting from the Task class, and implementing one or more interfaces.

On the C side

It is planned to use the same GObject on both server- and client-side, using an helper object to keep these synchronized. There is no real problem with it, despite I’ll probably have to bypass the glib bindings to implement the interface registering on the bus. But it can’t be too tricky, since the client API has to be sane and clean.

Note: The helper object only makes the translation between D-Bus signals and method calls and GObject ones. It does not store any internal data. This object is internal to the library, you user will never see it.

The issue lives in the fact that a task object may implement a random amount of interfaces: a file copying task will implement the FileOperation task, but burning files from nautilus CD burner would involve files as well as the CD burner, and so the task would have to implement both FileOperation and DeviceOperation. Can you spot the issue ?

Yeah, that’s it: there is a fairly large amount of possible task subclasses. With two possible interfaces, you have four possible subclasses (including the one implementing nothing). Adding one interface, you have eight. And each additional interface doubles the amount of possible subclasses. As you imagine, it would not be possible to procede this way without a fairly huge amount of code, despite the interface implementation is always the same. Of course it would be possible to generate most of that code automatically, but it really does not sound like a good idea: it would make it impossible to add a custom interface using a plugin in a nice way.

Thus, what I have to do is to find a way to generate such composited objects at runtime (and then eventually find a way to reflect it on the D-Bus side), in a clean and seamless way, like if it was a real one-piece-made object. Here is two possible solutions, but none are fully satisfying. That’s why I’m asking your opinion, heh ;-)

Playing with GType

The first approach I took was to make extensive use of the GType facility, using naively the inheritance scheme I was talking about above. The idea is to replace the classical malem_task_get_type (void) function with another one, malem_task_create_type (char **interface_list). This function would then register (statically or dynamuically) a type implementing all the listed interfaces, store the returned type identifier in a hash table, and return it, for it to be instantiated.

This is fairly complicated (playing with types that way is neither usual, nor obvious, nor easy), and the types aren’t really unregistered when not used anymore. And all this machinery would be involved each time a new task is registered. But on the other hand it would be easy to call the dbus interface registering stuff for each interface actually implemented. I was thinking it was the solution, which allowed the most elabored things, but to be fair, I don’t think it’s a good solution anymore. It’s just too complicated for the (quite simple) thing I want to do.

Composition

The other solution was to mimick the way interfaces work, but on a scope limited to a particular object. Considering the interface implementation is always the same (FileOperation is a FileOperation, no matter what other interfaces are available), I was thinking I could just have a list of additional interfaces implemented by the current task. These interfaces would indeed be stock objects, and there would be a MALEM_INTERFACE macro working like the G_INTERFACE one, a way to know if the object implements a particular interface, etc. The interface creation would only be allowed during the construction.

This has the advantage of being simple (far more simpler than the first solution), mimicking something that already exist but in a custom way. The inheritance scheme can be emulated using a MALEM_FILE_OPERATION macro, but using a different code than the classical glib casting macros. I could even avoid the MALEM_FILE_OPERATION-like macros by taking the actual MalemTask as first argument for the interface-provided functions:

void
malem_file_operation_register_file (MalemTask *task,
                                    gchar     *uri)
{
        MalemFileOperation *iface;

        iface = malem_task_get_interface (task, "FileOperation");
        g_return_if_fail (iface == NULL);

        /* Do Something */
}

Then I’d have to make my helper object to know what interfaces are available for the newly created object and then to register the introspection stuff. The drawback is that the D-Bus glib bindings currently don’t allow to do such stuff, so I’d have to implement it myself. Hard but not impossible I guess. May I have your advices on this matter, DBus experts?

Writing this solution ⤽on the paper⤝ makes me feel like it’s a good solution, but I’m not sure this is really elegant. This would be transparent for the C developper, but would it be good for bindings ?

Can you think of another (better) way to handle the problem?