Getting Started with Libpeas Extensions in Vala

Remnant from an old blog

If you've been looking for easy ways to make your application extensible using "plugins", surely you must have come across a GObject extensions library called libpeas. Its engine can seamlessly load GObject-based extension objects which act as "entry points" to the application. Currently, plugins written in C/Vala (shared object files), Python and Lua are supported.

As a project I'm currently working on (more on that soon) requires a system like this, I decided to take a look. I must say, it took me a while to get it all up and running. Which is why I decided to write a simple introduction in the hope that the journey will be less painful for others. The end goal is to make a small a Gtk+ window with buttons provided by plugins. The core of the application will be written in Vala, and plugin examples are provided in Python and Vala. Let's get started.

What you need

Do make sure you have the development versions installed too, if your distribution splits library packages.

Valadoc contains a great reference for all the libraries used in this post (libpeas-1.0 and gtk+-3.0

The structure

For this little tutorial I've poked around a bit in the source code of gedit, the project where libpeas was first conceived. I had never seen an extensible application up close before, so seeing the structure was a nice aha-moment for me.

Most of gedit is actually a shared library. The library defines basically the whole application. Then there's also a little executable called gedit which doesn't do much more than creating some objects defined in libgedit[1] to start the main loop. In fact, it's so small it's made of one C file: gedit.c.

Why is this the case? For an application to be truly extendible, plugins need to be able to deal with its internal objects. Thinking about it like this, it's only logical that extendible applications are basically a library. Of course, not all the internal objects need to be accessible by plugins. To hide an object, simply don't ship the header file defining it or in Vala's case, mark the object as private.

To summarize: one big library compiled into a shared object file which a small launcher application and plugins utilize.

Part 1: the library

Just so you know: this will be most of the work.

As our application consists of a simple Gtk window, we will define the window here. Furthermore, we will define an interface for the extension objects. For good measure, let's give our little app an original name. Like foo.

namespace Foo {

	public class Window : Gtk.Window {

		public Gtk.ButtonBox buttons { get; set; }

		private Peas.ExtensionSet extensions { get; set; }

		public Window() {
			this.buttons = new Gtk.ButtonBox(Gtk.Orientation.VERTICAL);
			this.destroy.connect(Gtk.main_quit);

			/* Get the default engine */
			var engine = Peas.Engine.get_default();

			/* Enable the python3 loader */
			engine.enable_loader("python3");

			/* Add the current directory to the search path */
			string dir = Environment.get_current_dir();
			engine.add_search_path(dir, dir);

			/* Create the ExtensionSet */
			extensions = new Peas.ExtensionSet(engine, typeof (Foo.Extension), "window", this);
			extensions.extension_added.connect((info, extension) => {
				(extension as Foo.Extension).activate();
			});
			extensions.extension_removed.connect((info, extension) => {
				(extension as Foo.Extension).deactivate();
			});

			/* Load all the plugins */
			foreach (var plugin in engine.get_plugin_list())
				engine.try_load_plugin(plugin);

			this.add(buttons);
			this.show_all();
		}
	}

	public interface Extension : Object {

		/* This will be set to the window */
		public abstract Window window { get; construct set; }

		/* The "constructor" */
		public abstract void activate();

		/* The "destructor" */
		public abstract void deactivate();
	}
}

As you can see, in the constructor of Foo.Window, a Peas.Engine is used to search for plugins in the current directory and load the plugins. A Peas.ExtensionSet takes care of creating extension objects implementing the Foo.Extension interface as their plugins are loaded by the engine.

By connecting to the extension_added signal of extensions, we make sure that after an extension object is created, its activate member function is called. By connecting to the extension_removed signal, we make sure that the deactivate() member function is called. If you want, you could see a Foo.Extension's activate and deactivate functions as some sort of constructor and destructor. In activate they set their stuff up, add a button to the window, and in deactivate they remove the button again.

If you're familiar with GObject-style construction, you'll see that we tell extensions to create every extension object with its window property set to this (the current Foo.Window instance). This way, extensions will have a reference to the window from which they can start messing with the application.

Note that the Python loader has to be explicitly enabled.

Of course you can retrieve way more information about a plugin before (and after) loading it. The variable plugin is of the type Peas.PluginInfo, which, combined with Peas.Engine.provides_extension should give you all the info you could wish for.

Save the file as foo.vala and create the library:

# Generate shared object file, C headers, vapi file and gir file
valac -o libfoo.so --library foo -H foo.h  --gir Foo-1.0.gir  -X -shared -X -fPIC --pkg libpeas-1.0 --pkg gtk+-3.0 foo.vala

# Compile typelib file (Python and Lua)
g-ir-compiler --shared-library libfoo Foo-1.0.gir -o Foo-1.0.typelib

What we get:

Part 2: the launcher

Next, we create a launch point for our application. The contents of the file should be pretty straightforward:

void main(string[] args) {
	Gtk.init(ref args);

	var window = new Foo.Window();

	Gtk.main();
}

That's all. Save it to a file called launcher.vala and compile it to the executable foo:

valac -o foo launcher.vala --vapidir . --pkg gtk+-3.0 --pkg foo -X -I. -X -L. -X -lfoo

The extra options are to make sure our library files can be found while they're not installed in a default search directory.

If you already tried to run the launcher, chances are you've run into this error message:

./foo: error while loading shared libraries: libfoo.so: cannot open shared object file: No such file or directory

Because the shared object file isn't installed properly but is located in the current directory, in order to run the launcher, the environment variable LD_LIBRARY_PATH needs to be set to the current directory. For the Python plugin, our typelib file also needs to be found in the current directory using GI_TYPELIB_PATH:

export LD_LIBRARY_PATH=.
export GI_TYPELIB_PATH=.

Now you should be able to run the launcher using ./foo. An empty window, great!

Part 3: plugins

Finally, let's write some plugins! Libpeas plugins consist of at least two files: the actual plugin (a shared object file or a script) and a plugin file containing some information about the plugin. Let's start off by writing a plugin in Vala.

Vala

class ValaExtension : Object, Foo.Extension {

	public Foo.Window window { get; construct set; }

	Gtk.Button button;

	void activate() {
		button = new Gtk.Button.with_label("Say Hello");

		/* Change label when clicked */
		button.clicked.connect(() => {
			button.set_label("Hello World!");
		});

		/* The magic, it's happening! */
		window.buttons.add(button);
		button.show();
	}

	void deactivate() {
		window.buttons.remove(button);
	}
}

/* Register extension types */
[ModuleInit]
public void peas_register_types(TypeModule module) {
	var objmodule = module as Peas.ObjectModule;

	objmodule.register_extension_type(typeof (Foo.Extension), typeof (ValaExtension));
}

A few remarks:

Save the file as vala-extension.vala and compile it to libvala-extension.so using:

valac -o libvala-extension.so --library vala-extension vala-extension.vala -X -shared -X -fPIC --vapidir . --pkg libpeas-1.0 --pkg gtk+-3.0 --pkg foo -X -I. -X -L. -X -lfoo

Time to write a plugin file. This is written in the KeyFile format. For all the options, see the reference. Note that all this information will be accessible through Peas.PluginInfo.

[Plugin]
Module=vala-extension.so
Name=Say Hello
Description=Displays "Hello World!" on click

Notice how in the Module value, the preceding "lib" is omitted.

Great! Save it to vala-extension.plugin and run the launcher. You should see a window with the button we just defined in it.

Python

Next, let's add a button to quit the application, this time in Python. I'm not going to show how Python and python-gobject work (wish I could! If I did it wrong, please let me know) here, so I'll keep it short.

from gi.repository import GObject
from gi.repository import Peas
from gi.repository import Gtk
from gi.repository import Foo

class PythonExtension(GObject.Object, Foo.Extension):
	window = GObject.Property(type=Foo.Window)
	button = GObject.Property(type=Gtk.Button)

	def do_activate(self):
		self.button = Gtk.Button(label="Quit")
		self.button.connect("clicked", Gtk.main_quit)

		self.window.get_buttons().add(self.button)
		self.button.show()

	def do_deactivate(self):
		self.window.get_buttons().remove(self.button)

Save it as python-extension.py.

The complementary plugin file (notice how ".py" is omitted):

[Plugin]
Module=python-extension
Loader=python3
Name=Quit button

Save it to a .plugin file like you're used to, and enjoy the quit button. Don't forget to set GI_TYPELIB_PATH to the current directory (export GI_TYPELIB_PATH=.).

Final words

Well, that's it! I hope this gave you a good impression of what libpeas can do for your application and how to achieve it. Of course, it can do way more. Plugins can bring extra datafiles and GSettings, for example, and libpeas-gtk provides widgets to manage plugins easily. If you want to know more about libpeas, a good starting point would be the official reference. Other sources I used to write this post are:

Thanks for reading! If you have any questions or other feedback, please let me know.


  1. Check out libgedit's Vala bindings here. ↩︎