New plugin API
Ankit Vani
a at nevitus.org
Thu Aug 8 15:37:23 EDT 2013
Hi everyone.
I have been working on the plugin API for over two weeks now, and I feel I am in
a good place with where it stands now to talk about it and ask for suggestions.
There are things that I'm still not quite sure of, and things that could be done
better. I would appreciate suggestions to make this implementation of the
plugins API better.
Repo: http://hg.pidgin.im/soc/2013/ankitkv/gobjectification
Branch: soc.2013.gobjectification.plugins
Files of interest: libpurple/plugins.[ch]
---
INTRODUCTION
============
One of the goals of my Google Summer of Code proposals was the introduction of
GObject-based plugins. For this, I decided to start with a new API, that would
provide the ability to register new types in the GObject type system, and would
provide higher flexibility without sacrificing simplicity for libpurple and
plugin authors. Due to the problems in the current plugin API, such as the
difficulty in writing a loaded loader or a loaded protocol due to the
differentiation between protocols, loaders, and standard plugins, I decided not
to just add GObjectification to the current plugin API. Free language loaders
was also an interesting prospect of this change.
As it turns out, Gary (grim) had already written a very promising plugin library
that pretty much did exactly what we want, GPlugin
(https://bitbucket.org/rw_grim/gplugin). It is robust, lets you register new
types into the GObject type system, and can support plugins written in different
programming languages via loaders (although I believe only native C is fully
functional as of yet). Evaluating all the requirements for the new plugin API
with Ethan, it seemed that if I was to not use GPlugin, I would have ended up
with an API whose functionality resembled that of GPlugin anyway, but instead
would've taken longer to write. So rather than spending time reinventing the
wheel, we decided to use GPlugin as the backbone for the new purple plugin API.
BRIEF OVERVIEW OF GPLUGIN
=========================
A brief introduction to the basic data structures of GPlugin:
1. GPluginPlugin
This is an abstract type that represents a plugin of any type in GPlugin.
2. GPluginNativePlugin
It inherits GPluginPlugin and implements the GTypePlugin interface of the
GObject library. It represents a native C plugin. It allows registering new
types and interfaces into the type system.
3. GPluginPluginLoader
It is an abstract type that represents a plugin loader. The plugin loader is
responsible for loading plugins of a particular language.
4. GPluginNativePluginLoader
It is the loader for native C plugins. This is where the actual module
loading, unloading, and querying happens for native plugins.
5. GPluginPluginInfo
This type holds the information about a plugin, such as the id, name, author,
website etc. This is a GObject instead of a struct, so that applications such
as libpurple can inherit it and add their own fields in the plugin's info.
The gplugin_plugin_manager_* API provides the calls to add search paths, finding
plugins, and loading/unloading of plugins. You don't really need to care much
about loaders as GPlugin pretty much handles them transparently, unless you're
writing a loader in which case you can register your loader using
gplugin_plugin_manager_register_loader().
Here is an example of a very basic plugin that you can write using GPlugin:
https://bitbucket.org/rw_grim/gplugin/src/f64b9822d56dd25da027c26e47319f6817dffb0b/tests/plugins/basic-plugin.c?at=default
A plugin requires three functions:
1. gplugin_plugin_query() returns a new plugin info object that specifies things
like id, name, author etc. for the plugin.
2. gplugin_plugin_load(plugin) runs code on load and returns TRUE to indicate a
successful load, or FALSE to cancel the load.
3. gplugin_plugin_unload(plugin) runs code on unload and returns TRUE if unload
succeeded, otherwise FALSE.
GPLUGIN WITH LIBPURPLE
======================
GPlugin has been added as an external dependency for libpurple. If
--disable-plugins parameter is provided to configure, GPlugin is not required
and all dynamic loading magic is disabled. Of course, protocols can still be
linked statically in this case.
Since GPluginPlugin represents a plugin, one way to integrate GPlugin with
libpurple would be using GPluginPlugin instead of PurplePlugin throughout the
codebase. We would use the GPlugin API directly, and for purple-related plugin
stuff, we'd use a purple API. However, there are a number of problems with this
approach:
1. When --disable-plugins is provided, we want to disable the entire dynamic
loading mechanism, and GPlugin should no longer be required. This is a
problem when there are calls to the GPlugin API and types such as
GPluginPlugin and GPluginPluginInfo all around the codebase.
2. It would be really confusing for developers and plugin authors, when some of
the things needed a GPlugin API and some things needed a purple API.
Moreover, at times you would need to cast purple stuff to GPlugin stuff,
which gets ugly soon enough.
3. Memory management becomes a problem with needing to remember to unref
returned GPlugin objects and lists all the time, since the rest of the
GObjectified libpurple does not ref and unref unless necessary. Forget an
unref once somewhere, and you'd have spend time and energy hunting down the
leak.
4. It becomes difficult to document everything, because not everything is in
libpurple. And the documentation in libpurple would at times have to assume
familiarity with the documentation of GPlugin.
Thus, the solution I chose was to keep everything simple and purple, and only
deal with GPlugin in plugins.[ch], where the purple plugin API is implemented.
Basically, the purple plugin API wraps the GPlugin stuff wherever needed.
When plugins support is enabled, gplugin.h is included and all GPlugin calls are
enabled.
In this case, PurplePluginInfo inherits GPluginPluginInfo to hold additional
purple information for a plugin like the UI requirement, actions, whether the
plugin is loadable, preference frame callback etc.
PurplePlugin is a typedef for GPluginPlugin, and represents a plugin of any
type.
When plugins support is disabled, GPlugin is not linked, gplugin.h is not
included, and all GPlugin stuff is disabled by an #ifdef PURPLE_PLUGINS guard.
In this case, PurplePluginInfo inherits GObject, and isn't really going to be
instantiated anywhere.
PurplePlugin is a typedef for GObject, and again, isn't going to be instantiatd
anywhere (protocols are no longer represented by their plugins, I get to that in
the last section).
A GPlugin plugin requires the three functions I mentioned in the previous
section. However, due to the different semantics of loading and unloading when a
plugin is to be linked dynamically or statically, a macro that does the
appropriate work is introduced.
This macro is:
PURPLE_PLUGIN_INIT(plugin-name, query-func, load-func, unload-func)
When linking dynamically, this expands to the gplugin_plugin_[query,load,unload]
functions.
When linking statically, this expands to pluginname_plugin_[load,unload]
functions, called by static_proto_load() and
static_proto_unload().
PURPLE PLUGIN API
=================
One of the main changes in the new plugin API is that there is no distinction of
plugins based on whether they're protocols, loaders or standard plugins. You
could write a standard plugin that provides a new protocol, or multiple
protocols, or a loader, or both, etc (not saying that you should do them crazy
stuffs). But this actually simplifies a lot of things, while giving plugin
authors more flexibility.
I will discuss the aspects that have changed in this API, and leave out the
obvious stuff.
---
Plugin info
-----------
Object hierarchy:
GObject
+----GPluginPluginInfo
+----PurplePluginInfo
+----FinchPluginInfo
+----PidginPluginInfo
PurplePluginInfo inherits GPluginPluginInfo and holds information about a
plugin. An instance of PurplePluginInfo has to be returned from the plugin's
query function, by providing the appropriate values. See the example at the end
of this section.
A plugin info instance is created by calling
purple_plugin_info_new(prop1-name, prop1-value, prop2-name, prop2-value, ...);
Valid properties are:
1. "id" (string): The ID of the plugin.
2. "name" (string): The name of the plugin.
3. "version" (string): Version of the plugin.
4. "category" (string): Primary category of the plugin.
5. "summary" (string): Summary of the plugin.
6. "description" (string): Description of the plugin.
7. "author" (string): Author of the plugin.
8. "website" (string): Website of the plugin.
9. "icon" (string): Path to a plugin's icon.
10. "license" (string): The plugin's license.
11. "abi_version" (guint32): The required ABI version for the plugin.
12. "dependencies" (GSList): List of plugin IDs required by the plugin.
13. "preferences_frame" (PurplePluginPrefFrameCallback): Callback that returns
a preferences frame for the plugin.
All properties except "id" and "abi_version" are optional, and may not be
specified.
A "flags" property is also present. I talk about it in a subsequent subsection.
- Since we also have a GSoC project that is working on a new plugins window, I
have temporarily added a property "category", which could organize plugins in
the plugins window.
- "icon", which is a GPluginPluginInfo property may also be helpful in some way
in the new plugins window.
- I'm not sure if we need a "license" property, which is also a
GPluginPluginInfo property.
Please let me know if I should remove any of these properties, since every
property has an appropriate get function in the API, for example
purple_plugin_info_get_name(), purple_plugin_info_get_author() etc.
An additional property "ui_requirement" is also present, but it should not be
set manually.
It is set if you create a plugin info instance using one of these functions
instead of purple_plugin_info_new():
1. finch_plugin_info_new(): (in gntplugin) Creates a finch plugin, with UI
requirement as finch. A property
"finch_preferences_frame" can be set to a callback
that creates a GNT frame.
1. pidgin_plugin_info_new(): (in gtkplugin) Creates a pidgin plugin, with UI
requirement as pidgin. A property
"pidgin_config_frame" can be set to a callback that
creates a GTK frame.
The PurplePluginInfo instance for a plugin can be retrieved by calling
purple_plugin_get_info(plugin).
Plugin requirements
-------------------
A plugin that does not meet all the requirements is not loadable. To check if a
plugin is loadable, call purple_plugin_is_loadable(plugin).
If it is not loadable, purple_plugin_get_error(plugin) should give you the
reason why not.
A plugin is deemed to not be loadable if:
1. It does not have an ID.
2. The UI requirement for the plugin does not match.
3. The ABI version requirement for the plugin does not match.
This reason can be displayed by the UI, but attempts to load the plugin will
fail.
A plugin is also not loadable if it does not return an info instance. However,
purple_plugin_get_error() will return NULL in this case because there would be
nowhere to store the error.
Internal and load-on-query plugins
----------------------------------
A plugin can also specify a bitwise combination of two values as a "flags"
property. These values are:
1. GPLUGIN_PLUGIN_INFO_FLAGS_INTERNAL: Internal plugin.
2. GPLUGIN_PLUGIN_INFO_FLAGS_LOAD_ON_QUERY: Auto-load on query.
- Internal plugins are plugins that should not be shown in plugin lists of the
UI.
- Load-on-query plugins are automatically loaded when purple_plugins_refresh()
is called. Such plugins are not saved by purple_plugins_save_loaded(). Once
unloaded, a load-on-query plugin is not auto-loaded until the next restart.
Protocol plugins are to be both internal and load-on-query.
Actions
-------
Till now, the actions field was a callback, that returned a GList of actions
that had to be instantiated and added to the list.
Now, actions can be added using
purple_plugin_add_action(plugin, label, callback) in the plugin's load function,
without plugin authors needing to create a PurplePluginAction instance and
adding it to a list.
Use purple_plugin_get_actions(plugin) to get a list of PurplePluginAction
instances of that plugin. Unlike before, this list is owned by the plugin info
and should not be freed.
Registering new types
---------------------
GPlugin provides these functions to register a new type with the GObject type
system:
1. GType gplugin_native_plugin_register_type(GPluginNativePlugin *plugin,
GType parent, const gchar *name,
const GTypeInfo *info,
GTypeFlags flags);
2. void gplugin_native_plugin_add_interface(GPluginNativePlugin *plugin,
GType instance_type,
GType interface_type,
const GInterfaceInfo *interface_info);
3. GType gplugin_native_plugin_register_enum(GPluginNativePlugin *plugin,
const gchar *name,
const GEnumValue *values);
4. GType gplugin_native_plugin_register_flags(GPluginNativePlugin *plugin,
const gchar *name,
const GFlagsValue *values);
You can use these functions directly, however, for simplicity, uniformity, and
to avoid plugin authors from having to cast PurplePlugin to GPluginNativePlugin,
I have added these functions in the purple API to wrap the functions 1 and 2
above:
1. GType purple_plugin_register_type(PurplePlugin *plugin, GType parent,
const gchar *name, const GTypeInfo *info,
GTypeFlags flags);
2. void purple_plugin_add_interface(PurplePlugin *plugin, GType instance_type,
GType interface_type,
const GInterfaceInfo *interface_info);
When more languages are supported, these functions can provide a transparent
interface to register new types.
Removed API
-----------
- IPC has not been implemented yet. I'm not sure if we need it - do we? Would
signals suffice?
- purple_plugins_find_with_name() and purple_plugins_find_with_basename() have
no equivalents in the new API. Plugins can still be looked up by id or
filename.
A basic plugin example
----------------------
/*
* This example does pretty much the same thing as the helloworld plugin in
* the libpurple plugins, without the comments.
*/
#include "plugins.h"
static void
plugin_action_test_cb(PurplePluginAction *action)
{
purple_notify_message(action->plugin, PURPLE_NOTIFY_MSG_INFO,
"Plugin Actions Test",
"This is a plugin actions test :)", NULL, NULL, NULL);
}
static PurplePluginInfo *
plugin_query(void)
{
return purple_plugin_info_new(
"id", "core-example",
"name", "Example plugin",
"version", "1.0",
"summary", "An example plugin",
"description", "An example plugin",
"abi_version", PURPLE_ABI_VERSION,
NULL
);
}
static gboolean
plugin_load(PurplePlugin *plugin)
{
purple_plugin_add_action(plugin, "Action Test", plugin_action_test_cb);
purple_notify_message(plugin, PURPLE_NOTIFY_MSG_INFO, "Hello World!",
"This is the example plugin :)", NULL, NULL, NULL);
return TRUE;
}
static gboolean
plugin_unload(PurplePlugin *plugin)
{
return TRUE;
}
PURPLE_PLUGIN_INIT(example, plugin_query, plugin_load, plugin_unload);
---
Please take a look at plugins.h and plugins.c in my repository to get the full
picture.
PROTOCOLS (TEMPORARY FIX)
=========================
Since a plugin cannot represent a protocol anymore (as a prpl), a protocol has
to have its own representation that can be added and removed from the global
protocols list by plugins. This is temporarily done by PurplePluginProtocolInfo,
with three extra fields (id, name, actions). The way protocols are defined will
probably be going through some changes as well after the plugin stuff is all
done. But for now, this is the solution to get working protocols with the new
plugin API without modifying the protocol interface (much) yet.
For now, for example, this simplifies this:
PurplePlugin *prpl;
PurplePluginProtocolInfo *prpl_info;
prpl = purple_connection_get_prpl(gc);
prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(prpl);
to this:
PurplePluginProtocolInfo *prpl_info;
prpl_info = purple_connection_get_protocol_info(gc);
To add a new protocol, a plugin has to call purple_protocols_add(prpl_info).
To remove it, call purple_protocols_remove(prpl_info).
Removing a protocol disconnects all connected accounts using that particular
protocol, and frees its user splits, protocol options and actions.
---
I will now start refactoring existing prpls and plugins to use the new plugin
API. However, I want to ensure everything is as per what the community wants
before I go all out doing so. Please suggest changes and improvements.
I will post an update about any major changes done henceforth.
- Ankit
More information about the Devel
mailing list