You are viewing ahefner

Previous Entry | Next Entry

Piddling Plugins

oscar

The Shuffletron music player, in various branches, has accumulated some neat features (particularly last.fm scrobbling in Brit Butler's branch) that deserve merging, and ought to be cleanly separated from the core of the program. Leslie Polzer sent me a novel implementation early on which used generic functions for extensibility, adding/removing methods via the MOP as plugins load/unload. Clever as that was (and I'm impressed how little code is required, rereading the patch now), I wasn't comfortable with it, and the lack of a pressing need for a plugin interface let me put it off for a good long while.

Building extensibility around generic functions seemed the right thing to do though, and a slightly different idea, of writing plugins in the style of mixins and calling CHANGE-CLASS to enable them at runtime, stuck in the back of my head until (with some prodding) I was motivated to try it out. It's hardly a new idea (both Gsharp and McCLIM contain implementations of similar ideas, as does the AMOP book, just to name a few examples), and a minimal implementation doesn't take much code at all:

(defvar *configurations* (make-hash-table :test 'equal))

(defun configuration (plugins)
  (or (gethash plugins *configurations*)
      (setf (gethash plugins *configurations*)
            (make-instance 'standard-class
                           :name (format nil "MY-APPLICATION~{/~A~}" plugins)
                           :direct-superclasses (cons (find-class 'my-application)
                                                      (mapcar #'find-class plugins))))))

(defun reconfigure (application plugins &rest initargs)
  (apply #'change-class application (configuration plugins) initargs))

(defun active-plugins (instance)
  (mapcar #'class-name (rest (sb-mop:class-direct-superclasses (class-of instance)))))

(defun enable-plugin (application plugin &rest initargs)
  (apply #'reconfigure application (adjoin plugin (active-plugins application)) initargs))

(defun disable-plugin (application plugin)
  (reconfigure application (remove plugin (active-plugins application))))

(defun make-application (&rest initargs)
  (apply 'make-instance (configuration '()) initargs))

This isn't an ideal implementation, and there's a limit to how good it's going to get when CLOS doesn't fully support anonymous classes. However, a more serious attempt should work on arbitrary classes, provide a place to hang init/shutdown code for plugins, and the ability to list which plugins are enabled within an instance.

Piddling Plugins

Piddling-plugins adds these features using only slightly more code than above, along with some superfluous macro magic for writing defun-style definitions that are extensible by plugins. I've made light use of it in a branch of Shuffletron, confirming to myself that it's a good fit.

The code is tiny and self-explanatory, so I'll just post the examples from the README file for fun.

Examples

Imagine we have written a music player, looking something like this deliberately simplified code:

(defclass music-player () ())

(defun run-music-player ()
  ;; You need to set or bind *application* to your application instance
  ;; if you use defun-extensible. It's a good idea even if you don't.
  (let ((*application* (make-instance 'music-player)))
    (init-audio)
    (init-library *application*)
    (loop (execute-command (read-line)))))

Functions extensible by plugins can be defined using DEFUN-EXTENSIBLE. This is just syntactic sugar for defining a generic function specialized on the application object, with a wrapper that passes in the value of *APPLICATION*.

(defun-extensible execute-command (command)
  ...)

(defun-extensible play-song (song)
  ...)

(defun-extensible song-finished (song)
  ...)

Plugins extend the behavior of the application by defining methods on the extensible functions (or rather the generic functions defined behind the scenes, which are prefixed by "EXTENDING-"):

(defclass scrobbler ()
  ((auth-token :accessor auth-token)))

(defmethod plugin-enabled (app (plugin-name (eql 'scrobbler)) &key &allow-other-keys)
  (setf (auth-token app) (get-auth-token))
  (format t "~&Scrobbler enabled.~%"))

(defmethod plugin-disabled (app (plugin-name (eql 'scrobbler)))
  (format t "~&Scrobbler disabled.~%"))

(defmethod extending-song-finished :after ((plugin scrobbler) song)
  (scrobble song (auth-token plugin)))

(defclass status-bar () ())

(defmethod extending-play-song :after ((plugin status-bar) song)
  (redraw-status-bar))

(defmethod extending-execute-command :after ((plugin status-bar) command-line)
  (declare (ignore command-line))
  (redraw-status-bar))

To enable a plugin:

(enable-plugin *application* NAME [INITARGS...])

To disable a plugin:

(disable-plugin *application* NAME)

To set precisely which plugins are enabled:

(reconfigure *application* LIST-OF-PLUGINS [INITARGS...])

References

Comments

( 3 comments — Leave a comment )
denisignjat
Jan. 26th, 2012 09:28 am (UTC)
Hi,

Sorry for posting here, I wasn't able to find any issue tracker to report it (github tracker for shuffletron is disabled :S).

How to compile latest shuffletron? I tried to load it via quicklisp, but failed requiring mixalot-flac.

Also, I tried 0.0.4 version and every time it asks me about library path and I point it to folder with a bunch of mp3 files, it ends with 'No playable files found in '. Any ideas?

Thanks,
Denis

ahefner
Jan. 26th, 2012 09:44 am (UTC)
I'll assume you're using some flavor of Linux (otherwise I don't think 0.0.4 would even build). There's a bug in older versions (including 0.0.04) that might cause the library scan to fail on certain filesystems. Any of the 0.0.5-pre-whatever tags should work, minus Ogg and Flac support.

You'll need libflac (and the development headers) installed. On Debian Linux, these are in the libflac8 and libflac-dev packages. I haven't built it on other operating systems since adding Ogg/Flac support, so I won't be too surprised if there's an issue or two waiting to be fixed.

Can you paste here the error that occurs while compiling mixalot-flac?





denisignjat
Jan. 26th, 2012 01:29 pm (UTC)
Ahh, sorry, forgot to mention how I tried to compile 0.0.5 :) On other hand, that issue with 'No playable' was caused with 0.0.4 binary I downloaded from project homepage.

I'm using fedora 16 with sbcl 1.0.55. First I tried it with quicklisp and got this:

...
distribution for more information.
* (ql:quickload "shuffletron")

debugger invoked on a QUICKLISP-CLIENT::SYSTEM-NOT-FOUND in thread
#
[Error: Irreparable invalid markup ('<thread [...] "initial>') in entry. Owner must fix manually. Raw contents below.]

Ahh, sorry, forgot to mention how I tried to compile 0.0.5 :) On other hand, that issue with 'No playable' was caused with 0.0.4 binary I downloaded from project homepage.

I'm using fedora 16 with sbcl 1.0.55. First I tried it with quicklisp and got this:

...
distribution for more information.
* (ql:quickload "shuffletron")

debugger invoked on a QUICKLISP-CLIENT::SYSTEM-NOT-FOUND in thread
#<THREAD "initial thread" RUNNING {AB2CE41}>:
System "mixalot-flac" not found
...

Then I got mixalot from github and:

(asdf:operate 'asdf:load-op 'mixalot)

Compiled everything. But I don't know how to compile shuffletron. I'm getting:

Component :MIXALOT-FLAC not found, required by #<SYSTEM "shuffletron">


Please excuse for these, probably basic things. I'm not lisp user (I'm using clojure mostly) and hardly getting into asdf. Is it possible to add Makefile to mixalot tree; we all somehow get used to make/"make install" combo :)

Thanks
( 3 comments — Leave a comment )