# RPC(`@rpc` + `rpc`)
对于运行时之间直接、类型安全的调用,Deft 使用 `@rpc` 暴露函数,使用
`rpc` 调用它。与 `pub/*`(广播、即发即忘)不同,RPC 是一对一的,并返回
一个可以 `await` 的 future。
RPC 是 `pub/*` 在运行时层面的对等物:它位于 `Runtime` 上(与发布/订阅代理
和配置注册表并列),并且在每个宿主中都可用——TUI、独立的 `deft`
脚本二进制、集群工作进程,以及通过 `runtime/spawn` 派生的具名子运行时。
## 模型
1. 运行时用 `@rpc true` 标记函数(外加可选的 `@doc`)。
2. 当包含 `@rpc` 闭包的命名空间被加载时(`tui/run`、AppThread 启动或
独立文件加载),运行时会扫描它,并把方法注册到自己的 RpcRegistry 中。
3. 任何其他运行时都可通过 `rpc` 调用已注册的方法,它返回一个 Future,
其解析结果为该方法的返回值。
4. 调用方用 `await` 解析该 future。
# 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]
## 标识:运行时名称
`[rpc target method ...]` 通过运行时名称来定位目标——即创建运行时所用的
`id`(`RuntimeConfig.id`)。在 TUI 中,每个 AppThread 的运行时以其应用命名
(自动编号的应用名,同时也是面板标签),因此你传给 `tui/find-panel` 的
同一个字符串就是 RPC 目标:
[rpc "editor" "cmd-open-file" $path] # the "editor" runtime [rpc "diff" "load-diff" $l $r] # the "diff" runtime
运行时名称在所有活动运行时中唯一,因此即使同一个应用存在多个实例
("editor"、"editor2"……),`(runtime-name, method-name)` 也能唯一标识
一个方法。对于临时运行时,名称就是传给 `runtime/spawn` 或 `createChild`
的值。
## 暴露方法(`@rpc`)
在应用命名空间中的任意函数上添加 `@rpc true`。运行时会在加载后自动识别它。
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 }
运行时提取:
- 方法名(来自 `def` 名称)。
- 描述(来自 `@doc`)。
- 参数名和类型标注(来自闭包签名)。
这些信息作为元数据进入 RpcRegistry,调用方可以通过 `rpc/methods` 查看。
## 调用方法(`rpc`)
`[rpc runtime-name method-name args...]` 返回一个 Future。用 `await` 获取
返回值,或忽略它以即发即忘。
# Await the result set opened [await [rpc "editor" "cmd-open-file" $path]] # Fire-and-forget rpc "editor" "cmd-goto-line" 42
方法名是一个字符串——拼写错误会在调用时以被拒绝的 future 失败
("method not registered for that runtime")。参数类型不匹配会在目标
运行时失败,并在调用方的 future 中产生运行时类型错误。
## 列出可用方法
`[rpc/methods]` 列出共享注册表中所有已注册的方法。
`[rpc/methods runtime-name]` 列出一个运行时暴露的方法。
[rpc/methods] # => @{ %{:runtime "editor" :name "cmd-open-file" :description "Open a file..."} ... } [rpc/methods "editor"] # => @{ @{:name "cmd-open-file" ...} @{:name "cmd-save" ...} }
这正是 RPC 发现界面背后的机制(例如 Alt+X 命令面板中可用的跨运行时操作)。
## RpcRegistry
该注册表位于运行时层(`src/host/runtime/rpc_registry.zig`),与
发布/订阅代理相对应。每个 Runtime 拥有一个实例,子运行时通过
`shared_rpc_registry` 共享根运行时的实例,因此每个面板和脚本看到的都是
一张全局方法表。
条目以所属运行时名称 + 方法名称为键。当运行时关闭时(AppThread 退出、
子运行时被销毁),其条目会通过 `unregisterByRuntime` 清除,因此注册表
绝不会持有过期的 `*Runtime`。限制:`MAX_METHODS = 256`、`MAX_PARAMS = 8`。
## 返回值与 Future
`rpc` 在调用方的堆上分配一个 Future。目标运行时在自己的驱动线程上运行该方法
(AppThread 的 appWorker,或运行时的循环线程);结果通过线程安全队列送回,
经 `deft/stringify` 解析后用于解析该 Future。
try { set result [await [rpc "editor" "cmd-get-buffer-text"]] echo "got back $($result(len)) chars" } catch e { echo "RPC failed: $e~message" }
如果目标运行时抛出异常,该 Future 会以错误值被拒绝。
## 模式
### 编辑器交接
editor、git、find、files 和 log 应用都使用 RPC 请求编辑器打开文件。由于
面板标签等于运行时名称,无需通过 `tui/find-panel` 间接定位——直接寻址
编辑器即可:
rpc "editor" "cmd-open-file" $path
生产环境用法参见 `packages/git/src/git.dft`、`packages/files/src/files.dft`、
`packages/find/src/find.dft`、`packages/log/src/log.dft`。
### 双向协调
diff 和 editor 应用都暴露 RPC 方法并相互调用:
# Diff runtime exposes def load-diff {|left right| @rpc true ... } # Editor runtime calls rpc "diff" "load-diff" $left-path $right-path
### 防御性调用
如果目标运行时可能不存在,请用 `try` 包裹调用:
try { [rpc "editor" "cmd-goto-line" ($ln - 1)] } catch _e { nil }
### 独立运行 / 多运行时
RPC 在 TUI 之外的工作方式相同。派生具名子运行时的脚本可以直接调用其
`@rpc` 方法:
def child [runtime/spawn "worker" "apps/worker/worker.dft"] set r [await [rpc "worker" "process" $job]]
## 与 `pub/*` 对比
| 方面 | `rpc` | `pub/*` |
|---|---|---|
| 耦合 | 紧——调用方知道目标运行时 + 方法 | 松——广播,任意订阅者 |
| 方向 | 一对一 | 一对多 |
| 宿主 | 所有宿主 | 所有宿主 |
| 返回值 | 解析为值的 Future | 无(即发即忘) |
| 类型安全 | 部分(依据 `@rpc` + 闭包签名校验) | 无 |
| 运行时可发现 | 是(`rpc/methods`) | 仅凭约定 |
对于运行时之间的紧密协调(编辑器交接、diff↔editor、父↔工作进程)使用
RPC。对于广播事件(焦点变化、配置更新、文件系统变化)使用发布/订阅。
## 局限
- 字符串方法名。 拼写错误在运行时失败,而不是编译时。
- 每个 RPC 方法最多 8 个参数(注册表限制)。
- 每个注册表最多 256 个方法。
- 不支持流式传输。 对于增量数据,请使用一系列 `pub/publish` 调用或
带 `@stream` 的 `deftask`。
## 快速参考
| 形式 | 用途 |
|---|---|
| `@rpc true`(写在 def 上) | 暴露一个方法 |
| `[rpc runtime-name method-name args...]` | 调用方法(返回 Future) |
| `[rpc/methods]` | 列出所有已注册的方法 |
| `[rpc/methods runtime-name]` | 列出一个运行时的方法 |
| `[await [rpc ...]]` | 阻塞等待结果 |
| `[try { rpc ... } catch e { ... }]` | 防御性调用 |