Plugin System Overview
Status: wired (#189 closed). Receiver-less calls (
loadPlugin,listPlugins,emitHook,invokeTool, …) andPluginApiinstance methods (api.registerHook,api.registerTool, …) dispatch throughcrates/perry-codegen/src/lower_call.rs::PERRY_PLUGIN_TABLEandPERRY_PLUGIN_INSTANCE_TABLE. TypeScript surface lives intypes/perry/plugin/index.d.ts. Host-side snippets below are compile-link verified by the doc-tests harness againstdocs/examples/plugins/host_snippets.ts; plugin-sideactivate(api)snippets againstdocs/examples/plugins/plugin_snippets.ts.
Perry supports native plugins as shared libraries (.dylib/.so). Plugins extend Perry applications with custom hooks, tools, services, and routes.
How It Works
- A plugin is a Perry-compiled shared library with
activate(api)anddeactivate()entry points - The host application loads plugins with
loadPlugin(path) - Plugins register hooks, tools, and services via the API handle
- The host dispatches events to plugins via
emitHook(name, data)
Host Application
↓ loadPlugin("./my-plugin.dylib")
↓ calls plugin_activate(api_handle)
Plugin
↓ api.registerHook("beforeSave", callback)
↓ api.registerTool("format", callback)
Host
↓ emitHook("beforeSave", data) → plugin callback runs
Quick Example
Plugin (compiled with --output-type dylib)
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}`)
}
perry my-plugin.ts --output-type dylib -o my-plugin.dylib
Host Application
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 plugins = listPlugins()
const hooks = listHooks()
const tools = listTools()
console.log(`loaded: ${pluginCount()} plugin(s), ${hooks.length} hook(s), ${tools.length} tool(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 ABI
Plugins must export these symbols:
perry_plugin_abi_version()— Returns ABI version (for compatibility checking)plugin_activate(api_handle)— Called when plugin is loadedplugin_deactivate()— Called when plugin is unloaded
Perry generates these automatically from your activate/deactivate exports.
Native Extensions
Perry also supports native extensions — packages that bundle platform-specific Rust/Swift/JNI code and compile directly into your binary. These are used for accessing platform APIs like the App Store review prompt or StoreKit in-app purchases.
See Native Extensions for details.
Next Steps
- Creating Plugins — Build a plugin step by step
- Hooks & Events — Hook modes, event bus, tools
- Native Extensions — Extensions with platform-native code
- App Store Review — Native review prompt (iOS/Android)