Thoughts on Whatnot

A blog about .

Go Plugins

The release of Go 1.8 inches steadily closer, bringing with it many interesting and useful features and improvements, including shorter compilation times, an even faster GC, and, my personal favorite, initial support for plugins.

Plugins, essentially Go’s version of C’s dlopen() and related functions, are an interesting one. The ability to dynamically load packages at run-time has been one of my most wanted features in Go since I first figured out how interfaces work. They come a bit late for my particular use case at the time, as I haven’t done anything with that project in about 4 or 5 years, but I still want the feature. To illustrate what makes plugins useful and why they mesh so well with Go, I’ll run through what exactly that use case was and how I had planned on integrating plugins into the project.

The project in question, which I never made public for various reasons, was a relatively simple comic viewer made with Go and GTK+. I designed it to be extensible, with a central framework that provided the GUI, and sub-packages that provided information about each comic strip. The package structure was somewhat inspired by the way the image package and its various sub-packages work, which I’ll elaborate on in a minute. I only ever added support for two comics, XKCD and Dilbert, but the structure was there for more if I wanted it. Unfortunately, it had a slight issue: If I wanted to add more comic strips, I had to recompile the entire program. This was kind of annoying.

The way the code was structured, I only had to make minimal modifications to the code of the main program in order to add more comics. Specifically, the program was split into two packages, main and comic. comic was a very simple package, with an API which looked something like

package comic

type I interface {
  Name() string
  URLOf(date time.Time) string
}

func Register(comic I)

func Get() []I

And that was about it. And then, each comic-specific package could do something like

package xkcd

import (
  "viewer/comic"
)

type xkcd struct {
  // Stuff.
}

func init() {
  comic.Register(&xkcd{})
}

And the main program would simply need an empty import statement for each comic. One simple import _ "xkcd", and the viewer would support XKCD, no more tweaks necessary. But could it be made even simpler to add comics?

Unfortunately, the options at the time were limited. While I did consider coming up with some kind of JSON-based system that would allow me to just load JSON files that somehow contained the necessary information to load a comic, I rejected it as being far too complicated. What I really wanted was plugins. And now, plugins are on the way, which means the above becomes vastly simpler to extend. As an example, let’s tweak the above system by adding plgin support:

package comic

type I interface {
  Name() string
  URLOf(date time.Time) string
}

The first tweak seems somewhat backwards. All I’ve done is remove everything from the comic package except for the interface definition. Bear with me, however, and it will make sense. Probably.

Next, change the comic-specific packages. For example, the xkcd package becomes something like

package main

import (
  "viewer/comic"
)

type xkcd struct {
  // Stuff.
}

func Init() comic.I

A change also has to be made to the compilation of the comic pacakges. A new buildmode was added as part of the plugin support, so rather than a simple go build, each comic-specific package has to be built with go build -buildmode=plugin. This will produce an .so file, which we’ll deal with in a second. You may also notice that the package has changed to main. This is a requirement of the plugin buildmode.

And, finally, a somewhat larger tweak to the main program:

package main

import (
  "plugin"
  "viewer/comic"
)

var comics map[string]comic.I

func loadComic(path string) error {
  p, err := plugin.Open(path)
  if err != nil {
    return err
  }

  i, err := p.Lookup("Init")
  if err != nil {
    return err
  }

  c := i.(func() comic.I)()
  comics[c.Name()] = c
  return nil
}

And that’s about it. The idea is that those .so files that are produced for each of the comic-specific packages could be placed in a known directory, such as $HOME/.viewer, or something. Then, when the program is launched, it would simply read that directory and call loadComic() on each .so file in the directory. To add a new comic, simply write a package that defines a func Init() comic.I, build it as a plugin, and drop it in that directory. Nice, clean, and simple.

Now, not everything’s quite so simple just yet, unfortunately. This initial support’s quite limited; in particular, it only works on Linux and Darwin. But it’s a step, and I look forward to being able, finally, to make easily extensible programs when Go 1.8 releases in February, even if the target platforms are somewhat limited for now.

If you would like to download the code framework for the viewer with plugin support, you may do so here.


Share

comments powered by Disqus