# RPC (`@rpc` + `rpc`)
For direct, type-safe calls between runtimes, Deft uses **`@rpc`** to
expose a function and **`rpc`** to invoke it. Unlike `pub/*` (broadcast,
fire-and-forget), RPC is one-to-one and returns a future you can `await`.
RPC is a runtime-level peer of `pub/*`: it lives on the `Runtime` (alongside
the pub/sub broker and the config registry) and is available in **every
host** — the TUI, the standalone `deft` scripting binary, cluster workers,
and named child runtimes spawned via `runtime/spawn`.
## The Model
1. A runtime marks functions with `@rpc true` (plus optional `@doc`).
2. When a namespace containing `@rpc` closures is loaded (`tui/run`,
AppThread launch, or a standalone file load), the runtime scans it and
registers the methods in its **RpcRegistry**.
3. Any other runtime invokes a registered method via `rpc`, which returns a
Future resolving to the method's return value.
4. The caller resolves the future with `await`.
# Producer: packages/editor/src/editor.dft def cmd-open-file {|path^string| @rpc true @doc "Open a file in this editor panel." [load-file $path] }
# Consumer: packages/git/src/git.dft await [rpc "editor" "cmd-open-file" $full-path]
## Identity: the runtime name
`[rpc target method ...]` addresses the target by **runtime name** — the
`id` the runtime was created with (`RuntimeConfig.id`). In the TUI, each
AppThread's runtime is named after its app (the auto-numbered app name,
which is also the panel tag), so the same string you would pass to
`tui/find-panel` is the RPC target:
[rpc "editor" "cmd-open-file" $path] # the "editor" runtime [rpc "diff" "load-diff" $l $r] # the "diff" runtime
Runtime names are unique across live runtimes, so `(runtime-name,
method-name)` uniquely identifies a method even with multiple instances of
the same app ("editor", "editor2", ...). For ad-hoc runtimes, the name is
whatever was passed to `runtime/spawn` or `createChild`.
## Exposing a Method (`@rpc`)
Add `@rpc true` to any function in your app's namespace. The runtime
picks it up automatically after load.
def load-diff {|left-path right-path| @rpc true @doc "Load a side-by-side diff into this panel." @params %{ :left-path "string" :right-path "string" } # ... load and display the diff ... :ok }
The runtime extracts:
- The method name (from the `def` name).
- The description (from `@doc`).
- The parameter names and type hints (from the closure signature).
These go into the RpcRegistry as metadata that callers can introspect via
`rpc/methods`.
## Calling a Method (`rpc`)
`[rpc runtime-name method-name args...]` returns a Future. `await` it for
the return value, or ignore it for fire-and-forget.
# Await the result set opened [await [rpc "editor" "cmd-open-file" $path]] # Fire-and-forget rpc "editor" "cmd-goto-line" 42
The method name is a string — typos fail at call time with a rejected
future ("method not registered for that runtime"). Parameter type
mismatches fail at the target runtime with a runtime type error in the
caller's future.
## Listing Available Methods
`[rpc/methods]` lists all registered methods across the shared registry.
`[rpc/methods runtime-name]` lists methods exposed by one runtime.
[rpc/methods] # => @{ %{:runtime "editor" :name "cmd-open-file" :description "Open a file..."} ... } [rpc/methods "editor"] # => @{ @{:name "cmd-open-file" ...} @{:name "cmd-save" ...} }
This is what powers RPC discovery UIs (e.g. the Alt+X command palette of
available cross-runtime actions).
## The RpcRegistry
The registry lives at the **runtime layer** (`src/host/runtime/rpc_registry.zig`),
mirroring the pub/sub broker. Each Runtime owns one instance, and child
runtimes share the root's via `shared_rpc_registry` so every panel and
script sees one global method table.
Entries are keyed by **owning runtime name + method name**. When a runtime
shuts down (an AppThread exits, a child is destroyed), its entries are
evicted via `unregisterByRuntime` so the registry never holds a stale
`*Runtime`. Limits: `MAX_METHODS = 256`, `MAX_PARAMS = 8`.
## Return Values and Futures
`rpc` allocates a Future on the caller's heap. The target runtime runs the
method on its own driver thread (an AppThread's appWorker, or the runtime's
loop thread); the result is shipped back via a thread-safe queue, parsed
from `deft/stringify`, and used to resolve the Future.
try { set result [await [rpc "editor" "cmd-get-buffer-text"]] echo "got back $($result(len)) chars" } catch e { echo "RPC failed: $e~message" }
If the target runtime throws, the Future rejects with the error value.
## Patterns
### Editor hand-off
Editor, git, find, files, and log apps all use RPC to ask the editor to
open files. Because the panel tag equals the runtime name, no `tui/find-panel`
indirection is needed — address the editor directly:
rpc "editor" "cmd-open-file" $path
See `packages/git/src/git.dft`, `packages/files/src/files.dft`,
`packages/find/src/find.dft`, `packages/log/src/log.dft` for production
usage.
### Bidirectional coordination
Diff and editor apps both expose RPC methods and call each other:
# Diff runtime exposes def load-diff {|left right| @rpc true ... } # Editor runtime calls rpc "diff" "load-diff" $left-path $right-path
### Defensive calling
Wrap calls in `try` if the target runtime might not exist:
try { [rpc "editor" "cmd-goto-line" ($ln - 1)] } catch _e { nil }
### Standalone / multi-runtime
RPC works the same outside the TUI. A script that spawns a named child
runtime can call its `@rpc` methods directly:
def child [runtime/spawn "worker" "apps/worker/worker.dft"] set r [await [rpc "worker" "process" $job]]
## Compared to `pub/*`
| Aspect | `rpc` | `pub/*` |
|---|---|---|
| Coupling | Tight — caller knows target runtime + method | Loose — broadcast, any subscribers |
| Direction | One-to-one | One-to-many |
| Hosts | All hosts | All hosts |
| Return value | Future resolving to value | None (fire-and-forget) |
| Type safety | Some (validated against `@rpc` + closure sig) | None |
| Discoverable at runtime | Yes (`rpc/methods`) | By convention only |
Use RPC for tight runtime-to-runtime coordination (editor hand-off,
diff↔editor, parent↔worker). Use pub/sub for broadcast events (focus
changes, config updates, file system changes).
## Limitations
- **String method names.** Typos fail at runtime, not compile time.
- **Max 8 parameters per RPC method** (registry limit).
- **Max 256 methods per registry.**
- **No streaming.** For incremental data, use a series of `pub/publish`
calls or `deftask` with `@stream`.
## Quick Reference
| Form | Purpose |
|---|---|
| `@rpc true` (on a def) | Expose a method |
| `[rpc runtime-name method-name args...]` | Invoke a method (returns Future) |
| `[rpc/methods]` | List all registered methods |
| `[rpc/methods runtime-name]` | List methods for one runtime |
| `[await [rpc ...]]` | Block for the result |
| `[try { rpc ... } catch e { ... }]` | Defensive calling |