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

Plugin System Overview

Status: wired (#189 closed). Receiver-less calls (loadPlugin, listPlugins, emitHook, invokeTool, …) and PluginApi instance methods (api.registerHook, api.registerTool, …) dispatch through crates/perry-codegen/src/lower_call.rs::PERRY_PLUGIN_TABLE and PERRY_PLUGIN_INSTANCE_TABLE. TypeScript surface lives in types/perry/plugin/index.d.ts. Host-side snippets below are compile-link verified by the doc-tests harness against docs/examples/plugins/host_snippets.ts; plugin-side activate(api) snippets against docs/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

  1. A plugin is a Perry-compiled shared library with activate(api) and deactivate() entry points
  2. The host application loads plugins with loadPlugin(path)
  3. Plugins register hooks, tools, and services via the API handle
  4. 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 loaded
  • plugin_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