eigenclass logo
MAIN  Index  Search  Changes  PageRank  Login

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.

update.png 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.