plugger

package module
v3.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 4, 2024 License: Apache-2.0 Imports: 9 Imported by: 39

README

plugger

Go Reference GitHub Go Report Card Coverage

plugger/v3 is a minimalist Go plugin manager featuring type-safe handling of functions and interfaces (“symbols”) exposed by plugins. Type safety is checked at compile time, thanks to Go Generics. Plugins usually are realized as packages exposing certain well-defined functions or interfaces by registering these. Plugin packages can be statically linked to or dynamically loaded by a Go application binary.

Applications then can retrieve, for instance, a list of the exposed plugin functions (“symbols”) of a specific type and then call all of these exposed plugin functions one after another – and without having to explicitly maintain a dedicated list of package functions to call in code. As practice shows, such lists quickly tend to get forgotten when adding new plugins.

plugger/v3 ensures a well-defined order of the symbols of the same type, where the symbols are either sorted lexicographically based on plugin names or optionally using ”placement hints”. This supports such use cases where some of the plugins might actually build upon the results from plugins that were invoked earlier.

Another use case is an application retrieving the exposed symbol for only a particular single named plugin and invoking only this particular plugin.

Finally, plugger/v3 is safe for concurrent use (as opposed to v0/v2 that are not).

Installation

To add plugger/v3 to your Go module as a dependency:

go get github.com/thediveo/go-plugger/v3@latest

Usage

Just three steps...

Define Exposed Symbol Type

First, define a type for the symbol you want to expose by your plugins; this must be either a function or interface (but not a pure type-constraining interface). This type will then be used plugger to manage different exposed symbol types in separate so-called "groups".

type pluginFn func() string

Define this type only in one place and then import it into your plugins as well as in the places where you need to work with the exposed symbol(s). Using a dedicated package just for the exposed symbol type might at first look like overkill but is your friend against import cycles.

Registering Exposed Symbols

Second, in your plugins, register (expose) the respective pluginFn implementations by fetching the group object for your specific symbol type and then calling Register on it.

import "github.com/thediveo/go-plugger/v3"

func init() {
    plugger.Group[pluginFn]().Register(MyPluginFn)
}

func MyPluginFn() string { return "foo" }

Please note that plugger/v3 defaults to deriving the plugin name from the package name where Register is called.

Calling Exposed Symbols

Finally, when you want to invoke the registered symbols, grab the group object for your specific symbol type and then range over the group's exposed symbols.

import (
    "github.com/thediveo/go-plugger/v3"
    // ...
    // don't forget to underline-import your (static) plugins!
)

func main() {
    pluginFnGroup := plugger.Group[pluginFn]()
    for _, pluginFn := range pluginFnGroup.Symbols() {
        fmt.Println(pluginFn())
    }
}

Dynamically Loading Plugins

Please see also example/dynplug for a working example.

  1. make sure your plugin has a main package with an empty main function.
  2. build your plugin shared object using go build -tags plugger_dynamic -buildmode=plugin ...
    • Please don't forget to specify the plugger_dynamic build tag/constraint; otherwise, trying to automatically discover and load plugins using dyn.Discover will panic with a notice to enable the plugger_dynamic build tag.
  3. in you application, call dyn.Discover to discover plugins in a specific directory (and sub directories) and to load them.

Migrating from v0/v2 to v3

In plugger/v3, groups now correspond with exactly one symbol type, whereas v0/v2 allowed to register multiple symbols for the same plugin in the same group. In v3, simply use multiple and now type-safe groups as needed, one for each type of exposed symbol.

As one benefit, exposed symbols are now inherently nameless from the perspective of the plugin manager, so no more need to deal with them. And another benefit is that groups are also nameless too, but instead they are now (symbol) typed.

In v3, exposed symbols are simply registered using their corresponding type-safe and name-less group, and with the only options available being WithName(name) and WithPlacement(hint).

// v3:
plugger.Group[fooFn]().Register(foo)
// before, v0:
//   plugger.RegisterPlugin(&plugger.PluginSpec{
//      Group:   "group",
//      Name:    "plug1",
//      Symbols: []plugger.Symbol{foo},
//   })
// before, v2:
// plugger.Register(plugger.WithName("plug1"), 
//     plugger.WithGroup("group"), plugger.WithSymbol(foo))

In Unit Tests

Sometimes, unit tests need a well-defined isolated plugin group configuration. For this, PluginGroup[T] objects as returned by Group[T]() can now be backed up and restored using PluginGroup[T].Backup() and PluginGroup[T].Restore(). Additionally, PluginGroup[T].Clear() resets a plugin group to its initial empty state.

VSCode Tasks

The included go-plugger.code-workspace defines the following tasks:

  • View Go module documentation task: installs pkgsite, if not done already so, then starts pkgsite and opens VSCode's integrated ("simple") browser to show the go-plugger/v2 documentation.

  • Build workspace task: builds all, including the shared library test plugin.

  • Run all tests with coverage task: does what it says on the tin and runs all tests with coverage.

Aux Tasks
  • pksite service: auxilliary task to run pkgsite as a background service using scripts/pkgsite.sh. The script leverages browser-sync and nodemon to hot reload the Go module documentation on changes; many thanks to @mdaverde's Build your Golang package docs locally for paving the way. scripts/pkgsite.sh adds automatic installation of pkgsite, as well as the browser-sync and nodemon npm packages for the local user.
  • view pkgsite: auxilliary task to open the VSCode-integrated "simple" browser and pass it the local URL to open in order to show the module documentation rendered by pkgsite. This requires a detour via a task input with ID "pkgsite".

Make Targets

  • make: lists all targets.
  • make coverage: runs all tests with coverage and then updates the coverage badge in README.md.
  • make pkgsite: installs x/pkgsite, as well as the browser-sync and nodemon npm packages first, if not already done so. Then runs the pkgsite and hot reloads it whenever the documentation changes.
  • make report: installs @gojp/goreportcard if not yet done so and then runs it on the code base.
  • make test: runs all tests (including dynamic plugins).

plugger is Copyright 2019-2022 Harald Albrecht, and licensed under the Apache License, Version 2.0.

Documentation

Overview

Package plugger v3 implements a minimalist plugin manager featuring type-safe handling of functions and interfaces (“symbols”) exposed by plugins. Type safety is checked at compile time, thanks to Go Generics. Plugins usually are realized as packages exposing certain well-defined functions or interfaces by registering these. Plugin packages can be statically linked to or dynamically loaded by a Go application binary.

Applications then can retrieve, for instance, a list of the exposed plugin functions (“symbols”) of a specific type using Group[T]().Symbols() and then call all of these exposed plugin functions one after another – and without having to explicitly maintain a dedicated list of package functions to call in code. As practice shows, such lists quickly tend to get forgotten when adding new plugins.

Plugger v3 ensures a well-defined order of the symbols of the same type, where the symbols are either sorted lexicographically based on plugin names or optionally using ”placement hints”. This supports such use cases where some of the plugins might actually build upon the results from plugins that were invoked earlier.

Plugger v3 is safe for concurrent use (as opposed to v0/v2 that are not).

Usage

Exposed plugin symbols are organized in PluginGroup objects, based on their particular type. The first step thus is to define a dedicated type for an exposed plugin symbol, such as a function or interface:

type PluginFn func(string) string

A good practice is to define the exported symbol types in a dedicated and otherwise empty package. This not only avoids import cycles but also ensures that always the same symbol type is used for looking up the corresponding PluginGroup object when working with symbols.

The PluginGroup for a specific type is retrieved by calling Group for the specific type:

group := plugger.Group[PluginFn]()

Calling Group multiple times for the same type always returns the same PluginGroup instance. There's no need for global variables referencing plugin group objects and using them should be avoided.

Next, plugins register their exposed symbols by retrieving the symbol's group first and then calling the plugger.PluginGroup.Register receiver on the group object.

func init() {
    plugger.Group[pluginFn]().Register(MyPluginFn)
}

func MyPluginFn() string { return "foo" }

Please note that plugger defaults to deriving the plugin name from the package name where plugger.PluginGroup.Register is called. The plugin name can also be explicitly specifyed by using WithPlugin in a registration.

Finally, when an application wants to invoke the registered symbols, it needs to grab the group object for the specific symbol type as before and then range over the group's exposed plugger.PluginGroup.Symbols.

import (
    // don't forget to underline-import your (static) plugins!
)

func main() {
    pluginFnGroup := plugger.Group[pluginFn]()
    for _, pluginFn := range pluginFnGroup.Symbols() {
        fmt.Println(pluginFn())
    }
}

Dynamically loading Plugins

Specify the build tag/constraint “plugger_dynamic” and use github.com/thediveo/go-plugger/v3/dyn.Discover to discover and load plugin shared objects.

Upgrading from v0/v2

Plugger v3 simplifies the API while at the same time introducing type-safety for the exposed symbols. In v3, a given PluginGroup always contains only symbols of the same particular type, but never multiple different symbol types. In consequence, the overhead of naming exposed symbols in order to differentiate them could be removed; this v1/v2 feature wasn't really used anyway.

// v3:
plugger.Group[fooFn]().Register(foo)
// before, v0:
//   plugger.RegisterPlugin(&plugger.PluginSpec{
//      Group:   "group",
//      Name:    "plug1",
//      Symbols: []plugger.Symbol{foo},
//   })
// before, v2:
// plugger.Register(plugger.WithName("plug1"),
//     plugger.WithGroup("group"), plugger.WithSymbol(foo))

In Unit Tests

Sometimes, unit tests need a well-defined isolated plugin group configuration. For this, PluginGroup objects returned by Group() can now be backed up and restored using PluginGroup.Backup and PluginGroup.Restore. Additionally, PluginGroup.Clear resets a plugin group to its initial empty state.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func WithPlacement

func WithPlacement(placement string) func(symbolSetter)

WithPlacement registers an exposed symbol with the given (plugin) placement hint in plugger.PluginGroup.Register.

func WithPlugin

func WithPlugin(name string) func(symbolSetter)

WithPlugin registers an exposed symbol with the given plugin name in plugger.PluginGroup.Register.

Types

type GroupStash added in v3.1.0

type GroupStash[T any] struct {
	// contains filtered or unexported fields
}

GroupStash is a “backup” of a PluginGroup. It can be used especially in unit tests where a PluginGroup needs to be modified to a particular known configuration for a test, and the group's original configuration restored after the test.

type PluginGroup

type PluginGroup[T any] struct {
	// contains filtered or unexported fields
}

PluginGroup represents the exposed plugin symbols for a particular symbol type, with the exposed symbols ordered by plugin name, or alternatively, by plugin placement.

func Group

func Group[T any]() *PluginGroup[T]

Group returns the *PluginGroup object for the given exposed symbol type T. Calling Group multiple times for the same exposed symbol type T always returns the same PluginGroup object.

func (*PluginGroup[T]) Backup added in v3.1.0

func (g *PluginGroup[T]) Backup() GroupStash[T]

Save returns a copy of this plugin group's current plugin configuration, for later restoration using the Restore method.

func (*PluginGroup[T]) Clear added in v3.1.0

func (g *PluginGroup[T]) Clear()

Clears this plugin group's configuration (such as in unit tests).

func (*PluginGroup[T]) PluginSymbol

func (g *PluginGroup[T]) PluginSymbol(name string) T

PluginSymbol returns the exposed symbol of the plugin identified by its name, or the zero symbol value if no such named plugin exists in this symbol group.

func (*PluginGroup[T]) Plugins

func (g *PluginGroup[T]) Plugins() []string

Plugins returns the names of all plugins exposing symbols in this plugin group. The returned list is always ordered, based on the plugin names and placement hints.

func (*PluginGroup[T]) PluginsSymbols

func (g *PluginGroup[T]) PluginsSymbols() []Symbol[T]

PluginsSymbols returns all exposed symbols together with the names of the plugins exposing them. This is always a clean and ordered copy of the Symbol objects.

func (*PluginGroup[T]) Register

func (g *PluginGroup[T]) Register(symbol T, opts ...RegisterOption)

Register a plugin-exposed symbol, with optional additional registration information.

func (*PluginGroup[T]) Restore added in v3.1.0

func (g *PluginGroup[T]) Restore(s GroupStash[T])

Restore a plugin group's former plugin configuration from a backup previously created by the Backup method.

func (*PluginGroup[T]) String

func (g *PluginGroup[T]) String() string

String renders a textual representation of a particular Group, showing the managed symbol type as well as the plugin-exposed symbols registered in this group.

func (*PluginGroup[T]) Symbols

func (g *PluginGroup[T]) Symbols() []T

Symbols returns all symbols (functions or interfaces) exposed by the plugins in this Group. This is always a clean and ordered copy of the list of exposed symbols.

type RegisterOption

type RegisterOption func(symbolSetter)

RegisterOption allows optional registration information to be passed to the Register method of plugin groups.

type Symbol

type Symbol[T any] struct {
	S         T      // exposed function or interface symbol.
	Plugin    string // name of plugin exposing the symbol S.
	Placement string // optional placement hint, or "".
}

Symbol is a function or interface exposed by a (named) plugin. The interface must not be a constraint interface used to express type constraints.

The placement hint indicates where in an ordered list of the plugin symbols this plugin should be placed:

  • "<": place at the beginning;
  • ">": place at the end;
  • "<foo": place before the plugin named "foo", if there is no such plugin named "foo", then the placement gets ignored;
  • ">foo": place after the plugin named "foo", if there is no such plugin named "foo", then the placement gets ignored.

func (Symbol[T]) Validate

func (s Symbol[T]) Validate()

Validate an exported plugin symbol and panic if the symbol is anything other than a function or interface.

While Go 1 has gained type constraints (in form of constraint interfaces) for use with Generics, there currently is no way to express constraints that forbid certain types, instead of allowing only a specific set. Thus, we need to validate at runtime that the symbol's type T actually is either a function type or an interface type. However, we cannot simply query the type of the symbol as this in the case of T being an interface would return the implementing value's T*. We thus need to construct a dummy composite type containing T that reflect accepts and then get that contained T's type via reflect. This then will be the correct interface T (instead of the underlying implementing value's T*). The Go compiler already ensured that the value satisfies the interface type T.

Directories

Path Synopsis
Package dyn discovers and loads .so Go plugins from the filesystem, so these plugins then can register themselves with the plugger plugin mechanism.
Package dyn discovers and loads .so Go plugins from the filesystem, so these plugins then can register themselves with the plugger plugin mechanism.
example
barplug
Package barplug is an example plugin registering its exposed DoIt function symbol.
Package barplug is an example plugin registering its exposed DoIt function symbol.
dynplug
Package dynplug is an example plugin registering its exposed DoIt function symbol; it intended to be loaded dynamically.
Package dynplug is an example plugin registering its exposed DoIt function symbol; it intended to be loaded dynamically.
fooplug
Package fooplug is an example plugin registering its exposed DoIt function symbol.
Package fooplug is an example plugin registering its exposed DoIt function symbol.
plugin
Package plugin defines the exposed example plugin API.
Package plugin defines the exposed example plugin API.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL