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.tsanddocs/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 youractivate()function - Generates
plugin_deactivate()calling yourdeactivate()function - Exports symbols with
-rdynamicfor 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
- Hooks & Events — Hook modes, event bus
- Overview — Plugin system overview