Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Creating Plugins

Status: wired (#189 closed). See Plugin System Overview — Status for the full surface. Snippets below are compile-link verified by the doc-tests harness against docs/examples/plugins/plugin_snippets.ts and docs/examples/plugins/host_snippets.ts.

Build Perry plugins as shared libraries that extend host applications.

Step 1: Write the Plugin

let count = 0

export function activate(api: PluginApi) {
    api.setMetadata("counter", "1.0.0", "Counts hook invocations")

    api.registerHook("onRequest", (data) => {
        count++
        console.log(`Request #${count}`)
        return data
    })

    api.registerTool("getCount", "returns request count", () => count)
}

export function deactivate() {
    console.log(`Total requests processed: ${count}`)
}

Step 2: Compile as Shared Library

perry counter-plugin.ts --output-type dylib -o counter-plugin.dylib

The --output-type dylib flag tells Perry to produce a .dylib (macOS) or .so (Linux) instead of an executable.

Perry automatically:

  • Generates perry_plugin_abi_version() returning the current ABI version
  • Generates plugin_activate(api_handle) calling your activate() function
  • Generates plugin_deactivate() calling your deactivate() function
  • Exports symbols with -rdynamic for the host to find

Step 3: Load from Host

import {
    loadPlugin, unloadPlugin,
    emitHook, emitEvent, invokeTool,
    setPluginConfig,
    discoverPlugins, listPlugins, listHooks, listTools,
    pluginCount, initPlugins,
} from "perry/plugin"

const id = loadPlugin("./counter-plugin.dylib")
console.log(`load returned: ${id !== 0 ? "ok" : "fail"}`)

const found = discoverPlugins("./plugins/")
console.log(`discovered ${found.length} plugin(s)`)

const result = emitHook("beforeSave", { content: "hello world" })

const greeting = invokeTool("greet", { name: "Perry" })
const formatted = invokeTool("formatCode", {
    code: "const x=1",
    language: "typescript",
})

Plugin API Reference

The api: PluginApi passed to activate() provides:

Metadata

api.setMetadata(name: string, version: string, description: string): void

Hooks

api.registerHook(name: string, handler: (ctx: unknown) => unknown): void
api.registerHookEx(name: string, handler: (ctx: unknown) => unknown, priority: number, mode: number): void

registerHook defaults to priority 10 / mode 0 (filter). Use registerHookEx for explicit priority and mode (0=filter, 1=action, 2=waterfall). Lower priority numbers run first.

Tools

api.registerTool(name: string, description: string, handler: (args: unknown) => unknown): void

Tools are invoked by name from the host.

Configuration

const value = api.getConfig(key: string)  // Read host-provided config

Events

api.on(event: string, handler: (data: unknown) => void): void  // Listen for events
api.emit(event: string, data: unknown): void                    // Emit to other plugins

Unregistering (Selective Cleanup)

The host purges all of a plugin’s registrations when the plugin is unloaded, so explicit unregister calls are only needed for long-lived plugins that re-configure themselves at runtime, or for stopping services / event listeners cleanly before re-registering.

api.unregisterHook(name: string, handler: (ctx: unknown) => unknown): void
api.unregisterTool(name: string): void
api.unregisterService(name: string): void   // invokes the service's stopFn first
api.unregisterRoute(path: string): void
api.off(event: string, handler: (data: unknown) => void): void

unregisterHook / off do a closure-identity compare: pass the exact same closure reference that was registered. unregisterService invokes the service’s stopFn before removing the entry, matching the lifecycle contract of registerService. All five calls are no-ops if the caller did not register the resource or no entry matches.

// Recommended pattern: keep a module-scoped reference to handlers so
// `deactivate()` can selectively unregister them. The host also purges
// all of a plugin's registrations on unload, but explicit unregister
// calls are the right way to stop services / event handlers cleanly
// when a plugin re-configures itself at runtime. Shown here as a
// regular function (not exported) so it doesn't collide with the
// `activate` / `deactivate` exports from `counter-plugin` above.
const _onDataUpdated = (data: any) => {
    console.log(`${data.source} updated ${data.records} records`)
}

function _stopWorker() {
    console.log("worker stopped")
}

function _startWorker() {
    console.log("worker started")
}

function cleanupExample(api: PluginApi) {
    api.on("dataUpdated", _onDataUpdated)
    api.registerService("worker", _startWorker, _stopWorker)

    // Later, in your deactivate() export:
    api.off("dataUpdated", _onDataUpdated)
    api.unregisterService("worker")   // invokes _stopWorker before removal
}

Next Steps