# 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 { ... }]` | 防御性调用 |