Shotwell Architecture Overview: Writing Plugins
NOTE: The following only applies to Shotwell 0.9 and up.
If you want to jump right into the plugin API, see the Valadocs for the plugin interfaces.
1. Introduction
Shotwell 0.9 introduces a plugin architecture so external contributors may extend Shotwell’s features and services without modifying the main code base itself. As this is an initial release of plugin support, the points of extension will be limited. A more general-purpose interface for plugins to insert themselves throughout the application may come later.
1.1. Terminology
Plugin can mean many things. For this document (and throughout the code) a plugin (or a Pluggable) is an object instance with a well-defined interface that is exercised by Shotwell (which is referred to here and in the code as the host).
An extension point is a Shotwell subsystem that discovers and loads plugins that have implemented a well-defined interface. It represents the plugins to the rest of the system (and the user) as though they were compiled directly into the application (rather than residing in a dynamically-loaded module). Examples of potential extension points are the Web publishing subsystem, slideshow transitions, photo editing tools, etc.
A module is a single dynamic library (.so or .la) stored in one of many well-known locations on the filesystem. At startup Shotwell searches for these library files and queries them for information about their contents. A module may contain multiple plugins.
An interface is a grouping of prototypes and constants (in general, a group of symbols) that are agreed upon by the plugin and the extension point. Shotwell has a simple versioning mechanism that allows an extension point and a plugin to negotiate interface versions. (In Vala-speak, the interface spoken of here is a namespace that holds one or more interfaces, constants, classes, etc.)
1.2. Roll-Your-Own vs. A Known Quantity
When the plugin interface was first being developed, we hoped to use the Peas library to manage all our plugin needs. The technology seemed quite promising and versatile. Peas allows for plugin writers to choose their language, and will even load an interpreter if a scripting language is used. It was all quite attractive.
Unfortunately, the interfaces to Peas (and its prerequisite, GObject Introspection are still fluid and the technology under development. Additionally, we here at Yorba strive for Shotwell to be available on the current version and the previous version of the major distros without the user manually upgrading prerequisite libraries. Because of the API differences between Peas 0.5 and 0.7, this seemed like a lot of work.
We may switch to Peas at some point in the future. The hope is that plugins written today for Shotwell will port easily to Peas (and hopefully will require little or no code change).
2. SPIT: Shotwell Pluggable Interfaces Technology
Development of plugins for Shotwell broke ground on 13 January 2011. The first order of business was devising a catchy acronym for the project. SPIT was landed upon after two-and-a-half minutes of intense thought over a plate of Chinese take-out. The SPIT interface first landed in trunk on 17 January 2011.
SPIT’s primary goal is to make it easy to write plugins for Shotwell. An empty module that provides no Pluggables and maintains a limited amount of state (mostly for debugging) can be written in 50 lines of Vala code. valac translates that into approximately 170 lines of C code, but valac’s generated code tends to be more verbose than hand-written C.
All modules implement the SPIT interface. SPIT is the entry point to all of the resources the module wishes to make available to Shotwell.
SPIT’s formal API can be browsed here.
NOTE: Shotwell does not automatically enable plugins when they’re first detected. If you’re writing a new plugin you must enable it in the Edit -> Preferences dialog. If you don’t see it listed then Shotwell did not find it in the filesystem or queried it and found something unsuitable. Run Shotwell like this so see debug output that may be of help:
$ SHOTWELL_LOG=1 SHOTWELL_LOG_FILE=:console: shotwell
2.1. The Entry Point
All SPIT modules have a single exported function, spit_entry_point. In Vala, this method looks like this:
Spit.Module? spit_entry_point(Spit.EntryPointParams *params)
and in C:
SpitModule* spit_entry_point (SpitEntryPointParams *params)
The parameters to the function are how SPIT interfaces are negotiated. When the shared library is first loaded into Shotwell, the first thing it does is call this entry point. The structure pointer that’s passed as a parameter is filled-in by Shotwell. It has the following fields:
int host_min_spit_interface
int host_max_spit_interface
int module_spit_interface
File module_file
The first two parameters (host_min_spit_interface and host_max_spit_interface) are the range of SPIT interfaces Shotwell understands. Today (22 March 2011) there is only one: 0 (zero). Thus, both parameters are this value. In the future, if the SPIT interface were to change, the current version number would be incremented to 1. That future version of Shotwell (provided it wanted to support plugins that understood both variations of SPIT) would pass 0 and 1 as the min and max versions.
The module’s entry point examines these values and decides whether or not it can support a version number in this range. Since the current version is zero, the choice is fairly limited. Assuming the module understood an interface version within the range, it sets module_spit_interface to the version it understands and returns a pointer to its Spit.Module (explained in the next section).
If for some reason the range was outside of zero (say, 1 and 2), the module would set module_spit_interface to Spit.UNSUPPORTED_INTERFACE (–1) and returns null.
If the module only understands a single interface version, it can use a simple helper function supplied by Shotwell: Spit.negotiate_interfaces.
The final value (module_file) is a GLib File object that represents the module file that Shotwell has loaded. From this object the plugin can glean the directory it’s installed in to locate other files it may require (icons, .ui files, etc.)
2.2. Spit.Module
If the negotiation goes well, the module’s entry point will return a Spit.Module. The module should create a new instance of this object without holding a reference to it. If the module holds a reference to the object, it needs to drop it before the module is unloaded. Shotwell does not inform the module when it’s being unloaded; the module should implement a g_module_unload () function for that reason. In any case, it’s easier if the module doesn’t hold the reference, so it’s not recommended.
The Spit.Module object provides basic information about the resources available in the module, as well as a pretty name, a version string, an a unique identifier (which should be a Java-style/CORBA name, i.e. org.yorba.shotwell.frotz, to prevent name collisions).
The key method here is get_pluggables (), which returns an array of objects implementing the Spit.Pluggable interface. (Since this interface is rather limited, they probably implement other interfaces as well.) Like Spit.Module, this array of interfaces represents the only instance of these objects; new instances should not be created with each invocation (particularly since this may be called multiple times when searching for Pluggables of a particular type.)
In most cases (but not all) these Pluggables will have a factory method that produces multiple instances of a class that can be used throughout Shotwell. This is due to the fact that the module can only provide one instance of the Pluggable objects to Shotwell. For some extension points, that may be fine; others may require new instances with each operation.
2.3. Spit.Pluggable
All plugins are Pluggables. Once Shotwell has access to the Pluggables via the Spit.Module, it can associate your plugin with the extension points it was designed to be used with.
Pluggable has a version negotiation method much like the entry point. This allows for the extension point to determine if the Pluggable understands the interface the extension point is expecting. Pluggable has this required method (in Vala):
int get_pluggable_interface(int min_host_interface, int max_host_interface);
and in C:
gint spit_pluggable_get_pluggable_interface (SpitPluggable* self, gint min_host_interface, gint max_host_interface)
Like the entry point, the host passes in a range of interface versions it understands. If the Pluggable speaks within this range, it returns its version, otherwise it returns Spit.UNSUPPORTED_INTERFACE. Spit.negotiate_interfaces can be used here as well. These version numbers are defined by each extension point, not SPIT. That is, the Publishing API has its own interface number, as does the Transitions API, and so on.
Additionally, each Pluggable identifies itself with an ID string (much like Spit.Module’s ID string) and a user-visible name. It also has a get_info() method which fills in a PluggableInfo struct with identifying information about the Pluggable — authors’ names, copyright, license, and so forth. The Pluggable can fill in as many or as few of these fields as it desires. They will be used to populate an About dialog for the plugin.
Finally, Pluggables may be activated and deactivated by the user. The Pluggable is informed of this via its activation () method. Note that this method will always be called at application startup to inform the Pluggable of its state. Also note that Shotwell currently does not unload a module if all its Pluggables are deactivated. This may change in the future.
2.4. Spit.HostInterface
In addition to the interfaces that the plugin and module must implement, the host (i.e. Shotwell) provides common services to each plugin via Spit.HostInterface. In the first version of SPIT, these are limited to getters and setters to persist configuration information and a method that returns the path to the module file itself (the same as what’s passed in with Spit.EntryPointParams).
This interface is not passed to any method mentioned in SPIT. The reason for this is twofold.
First, because each Pluggable instance must persist the entire time the module is loaded, it’s expected that the Pluggable interface for a particular extension point will include a factory method that creates instances for a particular purpose. (For example, rather than only offer a single Fade slideshow transition, the Pluggable is an effect Descriptor that can be used to generate multiple effects.) It’s expected that the Spit.HostInterface will be passed to this factory method so the instance can used it as needed.
Second, some extension points will want to offer additional host services to their Pluggables. They can do this by creating a host interface that requires (i.e. extends) Spit.HostInterface. Thus, the Pluggable will receive a single object that offers both host services.
3. Installing Your Plugin
Shotwell looks for plugins at startup in two locations. Note that those locations differ when Shotwell is run from its installed directory (typically /usr/bin or /usr/local/bin) or from the build directory.
If running from the installed directory, Shotwell looks for plugins in these locations and in this order:
~/.gnome2/shotwell/plugins
/usr/lib/shotwell/plugins (or, if installed in /usr/local, /usr/local/lib/shotwell/plugins)
If running from its build directory, Shotwell searches these paths in this order:
~/.gnome2/shotwell/plugins
(BUILDROOT)/plugins
Note that Shotwell searches these paths in this order. If it detects two or more plugins with the same ID (not the same user-visible name, but Java-style ID), it only uses the first found and ignores the rest. This means that plugins located in the user’s plugins directory have precedence over plugins installed in the system directories.
Second, Shotwell searches these directories recursively ignoring symbolic links. The current implementation is a depth-first search, although plugin installers shouldn’t rely on that.
4. Where to Start
Shotwell offers a few reference plugin modules to use as starting points for your own plugin. Feel free to build off of them. They’re all written in Vala.
In the samples/ directory is simple-plugin. This module is loaded and recognized by Shotwell, but it offers no Pluggables. This is more of a shell to start from. You can use the source file and the Makefile as a way to get started quickly with your own Pluggable.
Inside the plugins/ are the plugin modules that ship with Shotwell: shotwell-publishers, shotwell-publishers-extra, and shotwell-transitions. These implement functionality for the two current extension points, Web publishing and slideshow transitions. These are good places to look for examples on how to build your own.
If you feel your plugin is worthy of being included with Shotwell’s core plugins, please contact us on the Shotwell mailing list.
5. Extension Points
SPIT merely offers a common interface for modules to be loaded and offer Pluggables to Shotwell to use. A plugin author will want to implement Pluggables targeting a particular extension point. Three extension points are offered: