Plugins in your Ruby application
I remember spending some time implementing a plugin framework in C++ some ~10 years ago. Things have changed greatly since, and when I had to do the same in Ruby some days ago for my Ruby script for the wmii window manager it took me but a few minutes.
Read Michael Granger's comment and take a look at PluginFactory if you don't mind the extra dependency.
Goals
This is yet another task that becomes so easy in Ruby it seems to fade out, so to say, the same way many design patterns are trivial and hardly deserve to be named anymore.
There are at least two things a plugin framework should do:
- allow the user to drop plugins into some directory and have them loaded
- register the functionality exposed by each plugin so as to use it either automatically or on demand
The first part is but a simple iteration
Dir["#{PLUGIN_DIR}/*.rb"].each{|x| load x }
Registering
As for the second, there are many ways to do it, but it definitely looks better when plugin registration is automagical and the plugin definition seems purely declarative. Several ideas come to mind, as there are quite a few hooks we can use for that.
Class#inherited
Class#inherited is the most obvious choice (I feel); it'd look like this
class PluginBase def self.inherited(child) # register child somewhere # just to give a complete example: PluginBase.registered_plugins << child end @registered_plugins = [] class << self; attr_reader :registered_plugins end end
Then the plugin itself would be like
### # this would go in PLUGIN_DIR/whatever.rb # it'd be loaded as shown above class MyPlugin < PluginBase def foo; 1 end end
and it could be used in the main code with
PluginBase.registered_plugins # => [MyPlugin]
You can use singleton methods in PluginBase to add as much syntax sugar as you want in the "plugin definition":
module PluginSugar def def_field(*names) class_eval do names.each do |name| define_method(name) do |*args| case args.size when 0: instance_variable_get("@#{name}") else instance_variable_set("@#{name}", *args) end end end end end end class PluginBase class << self extend PluginSugar def_field :author, :version end end class FooPlugin < PluginBase author "J. Random <random@example.com>" version "0.0.0" def foo; 1 end end p = PluginBase.registered_plugins.last p.author # => "J. Random <random@example.com>" p.version # => "0.0.0"
Capturing plugin instances
Instead of registering classes derived from your PluginBase class, you can get a reference to instances of it (this is what I ended up using):
class Plugin @registered_plugins = {} class << self attr_reader :registered_plugins private :new end def self.define(name, &block) p = new p.instance_eval(&block) Plugin.registered_plugins[name] = p end extend PluginSugar def_field :author, :version end ### this under PLUGIN_DIR/ Plugin.define "foo" do author "Tsukishiro M." version "1.0.0" # stuff def do_it(x) # becomes a singleton method x * 2 end end #### Plugin.registered_plugins.keys # => ["foo"] plugin = Plugin.registered_plugins["foo"] # => #<Plugin:0xb7de9934 @author="Tsukishiro M.", @version="1.0.0"> plugin.author # => "Tsukishiro M." plugin.do_it "foo " # => "foo foo "
An actual (if toyish) example
This is a part of the standard plugin included in ruby-wmii using the second technique:
Plugin.define "standard" do bar_applet("cpuinfo", 800) do |wmii, bar| Thread.new do loop do cpuinfo = IO.readlines("/proc/cpuinfo")[6].split[-1].sub(/\..*$/,'') bar.data = cpuinfo.chomp + " Mhz" sleep 5 end end end binding("retag", "MODKEY-Shift-t") do |wmii,| wmii.wmiimenu(wmii.views_intellisort){|new_tag| wmii.retag_curr_client(new_tag) } end ('a'..'z').each do |key| binding("letter-jump-#{key}", "MODKEY2-#{key}") do |wmii,| unless wmii.curr_view[0,1] == key wmii.view wmii.views_intellisort.find{|x| x[0,1] == key } end end end binding("move-prev", "MODKEY-Control-UP", "MODKEY-comma") do |wmii,| wmii.view wmii.views[wmii.curr_view_index-1] || wmii.views[-1] end binding("move-next", "MODKEY-Control-DOWN", "MODKEY-period") do |wmii,| wmii.view wmii.views[wmii.curr_view_index+1] || wmii.views[0] end end
GemPlugin - Tom (2006-07-06 (Thr) 12:35:21)
Does this approach have advantages over Zed Shaw's GemPlugin?
mfp 2006-07-06 (Thr) 13:08:22
That *is* the way GemPlugin is implemented:
def Base.inherited(klass)
name = "/" + klass.to_s.downcase
Manager.instance.register(@@category, name, klass)
@@category = nil
end
Of course, GemPlugin provides additional functionality: it doesn't just #load the files, but rather uses RubyGems to discover installed packages dependent on the specified ones (e.g. all the packages that depend on mongrel but not on rails) and loads them. And it does more than the above trivial examples regarding registration.
You can use GemPlugin if:
- you don't mind the extra dependency and the additional work to create the plugins
- you want your plugins to be installed as RubyGems packages --- and the fact that just dropping some .rb files under a given dir doesn't work anymore is OK for you
- your plugins are complex and carry some data
You can keep the raw thing if you don't need more...
assente 23-07-2006 (Dom) 04:27:41
very interesting, is also a way to unload plugins at runtime?
mfp 2006-07-26 (Wed) 15:05:40
You cannot un-require() a file, but what you can do is remove the classes/modules defined there, with something like
MyApp::Plugins.module_eval{ remove_const :MyPluginClass }
assuming the plugins are in the MyApp::Plugins namespace.
You can't use that to undo changes to other classes, including the core ones, though. Take a look at _why's sandbox for a way to isolate code that would be most useful for this kind of things...
EdvardM 2006-11-23 (Thr) 07:30:45
Excellent post! it is quite easy to add dependency handling (must be run before/after given plugin) and extension points. If you are interested, see my additions by visiting http://majakari.net/articles/2006/11/23/adding-plugins-to-your-ruby-application
PluginFactory - Michael Granger (2006-07-01 (Sat) 00:53:32)
A friend of mine and I found ourselves using a similar technique to the one you describe everywhere, so we wrote a mixin that will add pluggable functionality to a class so you can do:
class MyProject::Document include PluginFactory
# Define the directory/ies to search for plugins def self.derivativeDirs; ["myproject/document"]; end
... end
Then if you create a class that derives from MyProject::Document, and put it in a file called 'pdf.rb' in myproject/document/, you can instantiate it like this:
MyProject::Document.create( 'pdf', *other_constructor_args )
I use it all the time now, and it makes adding pluggable systems to your code very easy.
It can be found at: http://deveiate.org/projects/PluginFactory or via the RAA.
mfp 2006-07-02 (Sun) 03:29:21
Thanks for the note, I'm adding a ref to it. PluginFactory wouldn't make sense in my case (I want ruby-wmii to be standalone), but it should be useful to more people.
Exposed commands? - Danno (2006-06-30 (Fri) 05:21:43)
How are the different commands that the plugin can call exposed to the class?
mfp 2006-06-30 (Fri) 07:48:38
Do you mean methods like "author" or "version" in the above examples? In the first example they are just singleton methods of the PluginBase class; in the second, merely instance methods defined in Plugin and hence available to its instances. In both cases, you can add instance methods to PluginBase and Plugin, respectively, and they'll be available to the actual plugin instances. You can also pass them a handle to some "core" object, like the wmii variables in the last example.
If you mean the functionality offered by the plugin, you could either
- establish a convention (e.g. "plugin classes must define foo, which is used to foobar"), possibly enforced by some additional code
- provide a way to declare the functionality offered by the plugin.
- ...
The last snippet, taken from ruby-wmii, is an example of the latter: the plugin acts as a sort of namespace where key bindings and applets can be defined, and they're then used in the application with something like
use_bar_applet "cpuinfo"
use_bar_applet "mpd", 10
#use_bar_applet "battery-monitor
('a'..'z').each{|k| use_binding "letter-jump-#{k}" }
use_binding "move-prev"
use_binding "move-next", "MODKEY-n" # override key sequence
which is in the configuration managed by the user (no need for it if plugins are always used when found, of course).
Danno 2006-06-30 (Fri) 10:40:18
Yeah, I was curious about the latter.
bar_applet and binding.
Keyword(s):[blog] [ruby] [frontpage] [subpar] [plugin] [technique]
References:[ruby-wmii 0.3.0: extensibility via plugins, easier upgrades, new applets (MPD, battery monitor)...] [Extending the wmii WM with more plugins; thanks, Nathan]