plugins

This module provides a plugin system that can be used in applications that require the ability to distribute functionality across external shared libraries. It provides the following functionality:

  • Build plugin source file into shared library
  • Monitor source files for changes and rebuild (hot code reloading)
  • Load/unload/reload shared library
  • Provide standard and custom callback framework
  • Allow shipping in binary-only mode (no source or hot code reloading) if required

The library requires --threads:on since the monitoring function runs in a separate thread. It also needs --gc:boehm to ensure that memory is handled correctly across multiple threads and plugins. This is true both for the main application as well as the individual plugins. To build in binary mode, the -d:binary flag should be used.

The boehm garbage collector or libgc can be installed using the package manager on most Linux distros and on OSX. For Windows, prebuilt 32-bit and 64-bit binaries are available here.

This module should be imported in the main application. Plugins should import the plugins/api module which provides typical functionality required by the shared library plugins.

The main procs of interest are:

  • initPlugins() which initializes the system and loads all plugins
  • syncPlugins() which should be called in the main application loop and performs all load/unload/reload functionality plus execution of any callbacks invoked
  • stopPlugins() which stops the system and unloads cleanly

Many of the procs and callbacks accessible from plugins can also be called from the main application if required. Refer to the documentation for the plugins/api module for more details.

Types

CmdData = ref object
  params*: seq[string]         ## Sequence of string params including callback
                     ## name to send callback
  pparams*: seq[pointer]       ## Sequence of pointer params to send objects to
                       ## the callback
  failed*: bool                ## Whether callback succeeded or failed
  returned*: seq[string]       ## Sequence of strings returned by callback
  preturned*: seq[pointer]     ## Sequence of pointers returned by callback
  
Object to send params and receive returned values to callbacks
Plugin = ref object
  manager*: PluginManager      ## Access plugin manager from plugin
  name*: string                ## Name of the plugin
  path: string
  handle: LibHandle
  depends: seq[string]         ## Plugins this plugin depends on
  dependents: HashSet[string]  ## Plugins that depend on this plugin
  pluginData: pointer          ## Pointer to store any type T within plugin to make
                    ## data accessible across all callbacks within plugin
                    ## Used by `getPluginData()` and `freePluginData()`
  onDepends: proc (plugin: Plugin; cmd: CmdData)
  onLoad: proc (plugin: Plugin; cmd: CmdData)
  onUnload: proc (plugin: Plugin; cmd: CmdData)
  onTick: proc (plugin: Plugin; cmd: CmdData)
  onNotify: proc (plugin: Plugin; cmd: CmdData)
  onReady: proc (plugin: Plugin; cmd: CmdData)
  cindex: HashSet[string]
  callbacks: Table[string, proc (plugin: Plugin; cmd: CmdData)]
Plugin state information is stored in this object - each plugin has an instance of this
Run = enum
  executing, stopped, paused
States of the plugin system - can be changed using the ppause, presume and pstop global callbacks
PluginManager = ref object
  run*: Run                    ## State of system
  ready*: bool                 ## True when all plugins are loaded
  cli*: seq[string]            ## Commands to run when system is ready
  tick: int
  pmonitor: ptr PluginMonitor
  plugins: OrderedTable[string, Plugin]
  pluginData: Table[string, pointer]
  callbacks: Table[string, pointer]
Manager of all loaded plugins and callbacks

Procs

proc newShared[T](): ptr T
Allocate memory of type T in shared memory
proc freeShared[T](s: var ptr T)
Free shared memory of type T
proc getManagerData[T](plugin: Plugin): T

Use this proc to store any type T in the plugin manager. Data will persist across plugin unload/reload and can be used to store information that requires such persistence.

Only first call allocates memory. Subsequent calls returns the object already allocated before.

Ensure freeManagerData() is called to free this memory.

import plugins/api

type
  PlgData = object
    intField: int

pluginLoad:
  var pData = getManagerData[PlgData](plugin)
  pData.intField = 5

pluginTick:
  var pData = getManagerData[PlgData](plugin)
  pData.intField += 1
proc freeManagerData[T](plugin: Plugin)
Use this proc to free memory allocated in the plugin manager with getManagerData()
import plugins/api

type
  PlgData = object
    intField: int

proc reloadAll(plugin: Plugin, cmd: CmdData) {.pluginCallback.} =
  freeManagerData[PlgData](plugin)
  var plgData = getManagerData[PlugData](plugin)
proc getPluginData[T](plugin: Plugin): T

Use this proc to store any type T within the plugin. Data will be accessible across plugin callbacks but will be invalid after plugin unload.

Only first call allocates memory. Subsequent calls returns the object already allocated before.

Ensure freePluginData() is called to free this memory before plugin unload.

import plugins/api

type
  PlgData = object
    intField: int

pluginLoad:
  var pData = getPluginData[PlgData](plugin)
  pData.intField = 5

pluginTick:
  var pData = getManagerData[PlgData](plugin)
  pData.intField += 1
proc freePluginData[T](plugin: Plugin)
Use this proc to free memory allocated within the plugin with getPluginData()
import plugins/api

type
  PlgData = object
    intField: int

pluginUnload:
  freePluginData[PlgData](plugin)
proc splitCmd(command: string): tuple[name, val: string] {...}{.raises: [], tags: [].}
Split "xxx yyy zzz" into "xxx" and "yyy zzz"
proc newCmdData(command: string): CmdData {...}{.raises: [], tags: [].}
Create new CmdData with command split using os.parseCmdLine() and stored in CmdData.params for processing by receiving callback
proc getVersion(): string {...}{.raises: [], tags: [].}
Get the Git version hash - repo state at compile time
proc getVersionBanner(): string {...}{.raises: [ValueError], tags: [].}
Get the version banner which includes Git hash if any and compiler version used to build main application
proc quit(manager: PluginManager) {...}{.raises: [], tags: [].}
Stop the plugin manager
proc notify(manager: PluginManager; msg: string) {...}{.raises: [Exception, KeyError],
    tags: [RootEffect].}
Invoke pluginNotify() across all plugins with msg as argument
proc plist(manager: PluginManager): seq[string] {...}{.raises: [], tags: [].}
Return a list of all loaded plugins
proc pload(manager: PluginManager; cmd: CmdData) {...}{.raises: [], tags: [].}
Reload all plugins if CmdData.params is empty, else (re)load the plugin(s) specified
proc punload(manager: PluginManager; cmd: CmdData) {...}{.
    raises: [KeyError, Exception, ValueError], tags: [RootEffect].}
Unload all plugins if CmdData.params is empty, else unload the plugin(s) specified
proc presume(manager: PluginManager) {...}{.raises: [Exception, KeyError],
                                    tags: [RootEffect].}
Resume plugin monitor - monitor plugin files and recompile and reload if changed
proc ppause(manager: PluginManager) {...}{.raises: [Exception, KeyError],
                                   tags: [RootEffect].}

Pause the plugin monitor - plugin files are not monitored for changes

This helps during development of plugins if source files need to be edited for an extended period of time and saving incomplete/broken code to disk should not lead to recompile and reload.

proc pstop(manager: PluginManager) {...}{.raises: [Exception, KeyError], tags: [RootEffect].}
Stop the plugin monitor thread - loaded plugins will stay loaded but the monitor thread will exit and no longer monitor plugins for changes
proc getPlugin(manager: PluginManager; name: string): Plugin {...}{.raises: [KeyError],
    tags: [].}
Get plugin by name - if no such plugin, result.isNil will be true
proc getCallback(manager: PluginManager; pname, callback: string): proc (
    plugin: Plugin; cmd: CmdData) {...}{.raises: [KeyError], tags: [].}
Get custom callback by plugin name and callback name
import plugins

var
  cmd = newCmdData("callbackparam")
  plugin = getPlugin("plugin1")
  callback = getCallback(manager, "plugin1", "callbackname")
if not callback.isNil:
  callback(plugin, cmd)
proc call(manager: PluginManager; callback: string; cmd: CmdData) {...}{.
    raises: [KeyError, Exception, ValueError], tags: [RootEffect].}

Invoke custom callback across all plugins by callback name

CmdData.params should only include the parameters to pass to the callback

import plugins

var
  cmd = newCmdData("callbackparam")
call(manager, "callbackname", cmd)
proc callPlugin(manager: PluginManager; pname, callback: string; cmd: CmdData) {...}{.
    raises: [KeyError, Exception], tags: [RootEffect].}

Invoke custom callbacks by plugin name and callback name

CmdData.params should only include the parameters to pass to the callback

import plugins

var
  cmd = newCmdData("callbackparam")
callPlugin(manager, "plugin1", "callbackname", cmd)
proc callCommand(manager: PluginManager; cmd: CmdData) {...}{.
    raises: [Exception, KeyError, ValueError], tags: [RootEffect].}

Invoke custom callbacks in a command line format

CmdData.params should include the callback name and will be searched across all loaded plugins.

import plugins

var
  cmd = newCmdData("callbackname callbackparam")
callCommand(manager, cmd)
proc getCommandResult(manager: PluginManager; command: string): seq[string] {...}{.
    raises: [Exception, KeyError, ValueError], tags: [RootEffect].}

Shortcut for running a callback defined in another plugin and getting all string values returned

CmdData.params should include the callback name and will be searched across all loaded plugins.

import plugins/api

proc somecallback(plugin: Plugin, cmd: CmdData) {.pluginCallback.} =
  var
    ret = getCommandResult(plugin, "othercallback param1 param2")

# Assume this callback is in another plugin
proc othercallback(plugin: Plugin, cmd: CmdData) {.pluginCallback.} =
  for param in cmd.params:
    cmd.returned.add param & "return"
proc getCommandIntResult(manager: PluginManager; command: string; default = 0): seq[int] {...}{.
    raises: [Exception, KeyError, ValueError], tags: [RootEffect].}

Shortcut for running a callback defined in another plugin and getting all integer values returned

If no value is returned, return the default value specified.

CmdData.params should include the callback name and will be searched across all loaded plugins.

import plugins/api

proc somecallback(plugin: Plugin, cmd: CmdData) {.pluginCallback.} =
  var
    ret = getCommandResult(plugin, "othercallback 1 2")

# Assume this callback is in another plugin
proc othercallback(plugin: Plugin, cmd: CmdData) {.pluginCallback.} =
  for param in cmd.params:
    try:
      cmd.returned.add $(parseInt(param) * 2)
    except:
      cmd.returned.add ""
proc initPlugins(paths: seq[string]; cmds: seq[string] = @[]): PluginManager {...}{.raises: [
    Exception, ResourceExhaustedError, IOError, ValueError, OSError, Defect],
    tags: [RootEffect, TimeEffect, ReadDirEffect, ReadIOEffect, ExecIOEffect].}

Loads all plugins in specified paths

cmds is a list of commands to execute after all plugins are successfully loaded and system is ready

Returns plugin manager that tracks all loaded plugins and associated data

proc stopPlugins(manager: PluginManager) {...}{.raises: [KeyError, Exception, ValueError],
                                        tags: [RootEffect].}
Stops all plugins in the specified manager and frees all associated data
proc syncPlugins(manager: PluginManager) {...}{.raises: [KeyError, Exception, ValueError], tags: [
    ReadDirEffect, RootEffect, WriteDirEffect, TimeEffect, ReadIOEffect,
    WriteIOEffect].}

Give plugin system time to process all events

This should be called in the main application loop