http://qs321.pair.com?node_id=366279

I used perl's flexible object system in the past to create programs that support plug-ins. This can be a very powerful feature, depending on how your program is meant to be used.

Recently, this subject came to my mind again when I participated in the Movable Type plugin contest. So I thought, why not let everybody know what can be done with perl to write pluggable programs? I wrote this tutorial, which is meant for people with some understanding of perl and packages, and I'd like to know what you think before I suggest it to the gods.

Well, here goes:

Writing Plugable Programs With Perl

Why plug-ins?

A plugin is just a way for people to extend your software without you having to care, or even know about it. It allows you to write an infrastructure over which people will have the freedom to do what they need. A good, well known example of plugins use is in web browsers (such as Mozilla) that just can't display every kind of content that exists in the web, so they allow plugins to do this for them.

The example program: complain.pl

In this tutorial we will attempt to solve a problem that we all faced one time or another, after spending a lot of time reading posts: How can we prove our superiority to the highest extent possible (find problems in the code) while wasting as little time and effort as possible?

The example program, complain.pl, will do this for you. It'll do more than that. It'll handle complain-fasion. You know, today you complain about 'use strict', tommorow it's efficiency, who knows what next? So for this purpose, finding a new reason to complain will only involve adding a new plugin to complain.pl - a plugin that somebody must have written already in a lively comunity such as Perl Monks!

so, let's introduce our program:

################################## # complain.pl - find what's wrong with others' code fast! # input should come from STDIN. # options: --plugin <name> - use the named plugin # --verbose - display some stuff use strict; use warings; use Linux; use Mozilla; # OK, ommit this one :-) # General tool stuff use Getopt::Long; my @plugins; my $verbose; GetOptions('plugin=s' => \@plugins, 'verbose' => \$verbose); # Get input program. my $snippet = eval {local $/; return < >}; print $snippet if $verbose;

What is a perl plug-in?

Essentialy, a plugin is just a perl package that complies to certain rules declared by the author of the pluggable program. These rules could be as simple as: "Sit in the plugins/ directory", "Put a line describing yourself in /etc/myprog.conf" or quite complex.

For our program, we'll decide that plugins reside in the plugins/ directory, and that's it. We'll also take a shortcut and assume that this directory in below the current directory. Doing the right thing here is left as an excersize...

Loading a plugin at runtime

Perl has a way of loading packages selectively on runtime: the 'require' keyword. unlike the familiar 'use', which is unconditional, happens before the program starts, and dies if it can't happen, require allows you much more flexibility. So you could write:

foreach (@plugin_list) { require $_ or warn "Plugin $_ failed to load"; }

And this is what we'll do in our programs:

# Load plugins @plugins = grep { eval{require "plugins/$_.pl"} ? 1 : (warn("Plug-in $_ failed to lo +ad.\n"), 0) } @plugins;

So we required the plugins and filtered out those who for some reason misbehaved. Which brings us to an important point: You can't let a plugin fail your entire program. That's why we used 'eval' here.

What require really does

require() is very similar to use(). They both let you include a module in this form:
require Acme::Current; # or use Acme::Current;
Except for the differences I already mentioned, there's one more important difference, in how the two are called:
use Module LIST; require EXPR;
As you see, require takes an expression. So we can put in, for example, require "plugins/$_.pl". If you use the form require Some::Module, then Some::Module is a bareword that is translated to a string.

Declaring the plugin interface

A plugin is only usable if the main program knows how to call it. So it is important to declare the way your plugin is used, so the plugin author could implement the required behaveiour.

For example, say we want our program to allow the plugins to affect it's initialization process. We'll declare that the plugins should implement an 'init' subroutine, and do this in our main program:

foreach (@plugin_list) { $_->init; }

This is sometimes called 'program events' that the plugin should react to.

For our program, we'll just execute the 'evaluate' method:

# Do the stuff foreach (@plugins) { $_->evaluate($snippet); }

You'll notice that there's a problem here: What if a plugin does not implement a certain feature? This will certainly die with "Can't call bal bla bla..." For 'evaluate' this isn't much of a problem, since if a plugin does not implement it, we'll drop him from our command line options. But say we wanted to let plugins declare their version on init time?

'can' to the rescue

You don't want to allow one misbehaving plugin to ruin your entire program, so we'll use a mechanism built into perl that allows us to check if a package can handle a certain method or not:

# Initialization: foreach (@plugins) { $_->init if UNIVERSAL::can($_, 'init'); }

The UNIVERSAL package is the 'top object' in perl, but it's also useful for dealing with simple packages, like we did here. the 'can' method of UNIVERSAL is what did the checking for us.

Now it keeps quiet.

Now we know what we expect our plugins to do and maybe we've even written some doccumentaion for others. Now let's write one. The 'bench' plugin will tell us exactly how slow a program is.

############################# # plugins/bench.pl package bench; use Benchmark; sub evaluate { my $pkg = shift; my $snippet = shift; print timestr(timeit(400000, $snippet)), "\n"; } 1;

You can try it now:

$ ./complain.pl -p bench < snippet.txt

Comunication between the plugin and the program

That print statement above must have caused you to scream to high heavens. A plugin should not decide on where and how to output its results!

OK, we need a way for the plugin to communicate the results back to the program. In our program, we'll keep a list of results, and declare the addOpinion subroutine for use by our plugin.

But we need to make our plugin be able to find this method. So what we'll do, we'll create a 'context object' - an object that is passed to the plugin and tells it useful stuff about the using program.

We'll add this simple package to the top of complain.pl:

package Context; my %versions = (); # The returned hash will store for each plugin the complaints it regis +ters. sub new { my $class = shift; return bless {}, $class; } # This will be optional. sub declareVersion { my $self = shift; my ($plugin, $version) = @_; $versions{$plugin} = $version; } # That's for our program sub getVersion { shift; $versions{shift()} } # This is how the plugin will tell us what he thinks, as many # times as it wants to. sub addOpinion { my $self = shift; my ($plugin, $opinion) = @_; $self->{$plugin} = [] unless exists $self->{$plugin}; push @{$self->{$plugin}}, $opinion; }

Now we'll just change the way we issue program events, so that a context object will be passed to the program. This is the new code, that we shall put right after the modules are loaded:

# Start a new context; my $ctx = Context->new; # Initialization: foreach (@plugins) { $_->init($ctx) if UNIVERSAL::can($_, 'init'); } # Do the stuff foreach (@plugins) { $_->evaluate($ctx, $snippet); }

And we'll fix the 'bench' plug-in to take advantage of the new and improved API :)

sub evaluate { my $pkg = shift; my ($ctx, $snippet) = @_; $ctx->addOpinion($pkg, timestr(timeit(400000, $snippet))); }

Wrapping it up

Now all we have to do is use the opinions expressed by our complaining plug-ins to generate a report. We'll add this final part to our program, in which we use the context object to check what happened:

# Simple output. while (my ($plugin, $opinions) = each %$ctx) { my $ver = undef; my $verstr = ($ver = Context->getVersion($plugin)) ? "(Version $ver) " : ''; print "The plugin $plugin ${verstr}says:\n"; print join '\n', map { "\t$_\n" } @$opinions; }

Just for fun: A plugin that checks if you used strict

This is the most fasionable complaint. Let's write a simple checker for this:
############################### # plugins/grammar.pl package grammar; sub init { my $pkg = shift; my $ctx = shift; $ctx->declareVersion($pkg, '0.1'); } sub evaluate { my $pkg = shift; my ($ctx, $snippet) = @_; if ($snippet =~ /use\s+strict;/) { $ctx->addOpinion($pkg, "Uses strict. OK"); } else { $ctx->addOpinion($pkg, "Does not use strict; Heathen swine!!") +; } } 1;

Note that this plug-in makes use of declareVersion, which is very considerate.

And that's all there's to it

Of course, this is just the most basic program I could think of. Better implementations will probably do a lot of things different, like creating a plugin object that plugins must inherit from, or do other fancy object-oriented stuff. But this is not the place for it.

You might want to take a look at the plugin interface of MovableType.

The distinction between use() and require() is documented in perldoc -f use and perldoc -f require. Thanks chromatic and tilly for your comments on this.

If anybody knows other good examples, please share.


perl -e'$b=unpack"b*",pack"H*","59dfce2d6b1664d3b26cd9969503";\ for(;$a<length$b;$a+=9){print+pack"b8",substr$b,$a,8;}'
My public key