api

This module provides typical functionality required by shared library plugins that get loaded by the main plugin system.

Every plugin requires a pluginLoad() definition that gets called when the plugin is loaded.

import plugins/api

pluginLoad:
  echo "Loaded plugin"

If there is no action to be taken on load, an empty pluginLoad() is sufficient.

Optional definitions are:

  • pluginUnload() which gets called before the plugin is unloaded
  • pluginTick() which gets called every time syncPlugins() runs
  • pluginNotify() which gets called when PluginManager.notify() is called
  • pluginReady() which gets called when all plugins are loaded and system is ready
  • pluginDepends() which should be used when a plugin depends on other plugins being loaded

Plugins can choose to use any combination of these optional definitions depending on the use case.

In addition, plugins can define custom callbacks by using the {.pluginCallback.} pragma.

Custom callbacks can be invoked with the callCommand() proc. The callback name and params should be populated correctly in the CmdData.params. This approach is useful if the callback needs to be invoked as a string like if it were provided by user input.

In addition, the call() and callPlugin() procs are also available to invoke custom callbacks directly without having to pass the callback name in CmdData. Lastly, the getPlugin() and getCallback() procs allow invoking callbacks like regular code.

Return values if any can be populated by the callback in CmdData.returned which, along with CmdData.params, are string types so a pointer type CmdData.pparams and CmdData.preturned are also available for other types of data. In addition, callbacks should set CmdData.failed to true if the callback has failed in order to notify the caller.

The newCmdData(), getCommandResult() and getCommandIntResult() procs are available to simplify invoking callbacks and getting return values.

Callbacks should ensure they check input params and returned values for validity.

The getManagerData() and getPluginData() procs enable storage of global and plugin local data so that it is accessible from any plugin.

The following additional procs are available:

  • notify(xxx) - invoke pluginNotify() across all plugins with param xxx
  • getVersion() - get git version hash of main application
  • getVersionBanner() - get git and Nim compiler version of main application
  • quit() - stop and unload the plugin system

The following plugin system specific procs are available:

  • plist() - list all loaded plugins
  • pload([xxx]) - (re)load specific or all plugins
  • punload([xxx]) - unload specific or all plugins
  • ppause() - pause the plugin monitor - will not reload plugins on changes
  • presume() - resume plugin monitor
  • pstop() - stop and unload all plugins

All these procs are also available from the main application.

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 ""

Macros

macro pluginCallback(body): untyped
Use this pragma to define callback procs in plugins
import plugins/api

proc name(plugin: Plugin, cmd: CmdData) {.pluginCallback.} =
  discard

Templates

template pluginLoad(body: untyped) {...}{.dirty.}

Use this template to specify the code to run when plugin is loaded

Note that all custom callbacks defined with {.pluginCallback.} should precede the pluginLoad() call.

import plugins/api

pluginLoad:
  echo "Loaded plugin"
template pluginLoad() {...}{.dirty.}

Use this template if there is no code to be run on plugin load. pluginLoad() is required or the plugin or does not get loaded

Note that all custom callbacks defined with {.pluginCallback.} should precede the pluginLoad() call.

import plugins/api

pluginLoad()
template pluginUnload(body: untyped) {...}{.dirty.}
Use this template to specify the code to run before plugin is loaded
import plugins/api

pluginUnload:
  echo "Unloaded plugin"
template pluginTick(body: untyped) {...}{.dirty.}
Use this template to specify the code to run on every tick - when syncPlugins() is called in main loop
import plugins/api

pluginTick:
  echo "Tick plugin"
template pluginNotify(body: untyped) {...}{.dirty.}
Use this template to specify the code to run when a notify event is called
import plugins/api

pluginNotify:
  echo "Notify plugin: " & $cmd.params
template pluginReady(body: untyped) {...}{.dirty.}
Use this template to specify the code to run when all plugins are loaded and system is ready
import plugins/api

pluginReady:
  echo "All plugins ready"
template pluginDepends(deps)
Use this template to specify which plugins this plugin depends on. System will ensure that those plugins get loaded before this one
import plugins/api

pluginDepends(@["plg1", "plg2"])