# D-Bus `dbus/*` 是一个纯 Zig 实现的 D-Bus 库,同时具备客户端和服务端 两种角色:连接会话总线或系统总线、调用方法、导出对象、发送和 订阅信号。一次 `[dbus/connect]` 就是一个连接(到总线守护进程的 一个套接字)。它在所属的 VM 线程上通过事件循环进行分派,因此 导出的处理函数和订阅闭包都作为 Deft 代码内联运行,方法调用返回 可与 `[await]` 集成的 future。 没有 libdbus 依赖,也无需编写总线配置 —— 默认连接会话总线,使用 `%{ :system true }` 则连接系统总线。 ## 连接与名称 D-Bus 将连接与名称区分开来: - 每个连接都会由总线分配一个唯一名称(`:1.5`、`:1.42`……), 可通过 `[dbus/unique-name $conn]` 读取。唯一名称属于单个连接, 重连后会改变 —— 它们适合用于定位特定对端,而不是稳定的服务。 - 众所周知名称(well-known name,如 `org.freedesktop.Notifications`、 `org.deft.myapp`)是总线层面的别名,同一时刻至多由一个连接 拥有。调用方寻址的是名称而非进程,总线会把每次调用路由到 当前拥有该名称的连接。 总线上可以有任意多个连接,其中任意多个都可以调用某个众所周知 名称 —— 但只有一个能成为它的主所有者。正是这条单一所有者规则 让人意外:两个守护进程无法同时服务 `org.freedesktop.Notifications`。 先到者拥有它;后到者要么排队等待,要么被拒绝,而每次 `Notify` 调用都会继续发给在位者。 连接断开时(`dbus/disconnect`、GC 终结器或进程退出),它会失去 自己的名称,此时总线会提升下一个排队的请求者。 ## 请求名称
set conn [dbus/connect]
set rc   [await [dbus/register-name $conn "org.deft.myapp" 7]]
`[dbus/register-name $conn "name" ?flags]` 返回一个 future,解析为 数值型的 `RequestName` 结果: | 结果 | 含义 | |---|---| | `1` | 主所有者 —— 调用将路由到这里 | | `2` | 在队列中 —— 已存在另一个所有者 | | `3` | 已存在且未入队 —— 已存在另一个所有者且设置了 `DO_NOT_QUEUE` | | `4` | 已经是所有者 | 可选标志是一个位掩码: | 标志 | 值 | 含义 | |---|---|---| | `ALLOW_REPLACEMENT` | `1` | 带 `REPLACE_EXISTING` 的另一个请求者可以从我们这里取走该名称 | | `REPLACE_EXISTING` | `2` | 尝试从当前所有者手中取走该名称 | | `DO_NOT_QUEUE` | `4` | 若名称无法获取,以结果 `3` 失败而非排队 | 替换是相互的。 只有当当前所有者以 `ALLOW_REPLACEMENT` 请求 该名称时,`REPLACE_EXISTING` 才会成功。这是 D-Bus 有意设计的安全 属性(名称劫持问题在 CVE-2008-4311 中修复):未选择加入的服务 无法被外部顶替,无论新来者传入什么标志。夺走其名称的唯一方式是 让它断开连接或退出。 有用的组合: | 标志 | 行为 | |---|---| | `0` | 经典服务:先到者胜,后到者静默排队 | | `4` | 快速失败:名称被占用时返回 `3` 而非排队 | | `6` | 若允许则接管,否则报告 `3` 且不静默排队 | | `7` | 同 `6`,并允许未来的守护进程替换我们 | “已拥有”与“被占用”之间的区别,对任何消费共享名称的一方都很 重要。不使用 `DO_NOT_QUEUE` 时,请求可能以结果 `2` 成功,然后 永远等待在一个永不退出的所有者后面 —— 连接看起来健康,却收不到 任何东西。 ### 完整示例:通知守护进程 在 GNOME 会话中,GNOME Shell 的通知服务拥有 `org.freedesktop.Notifications`,并且未请求 `ALLOW_REPLACEMENT`。因此以标志 `6` 或 `7` 请求的 deft 守护进程 会得到结果 `3`,而不是接管 —— `notify-send` 仍然发给 GNOME。 deft 守护进程可以通过直接调用总线来报告谁持有该名称:
[await [dbus/call $conn "org.freedesktop.DBus" "/org/freedesktop/DBus" \
       "org.freedesktop.DBus" "GetNameOwner" "s" "org.freedesktop.Notifications"]]
[await [dbus/call $conn "org.freedesktop.DBus" "/org/freedesktop/DBus" \
       "org.freedesktop.DBus" "GetConnectionUnixProcessID" "s" $owner]]
要移交该名称,请停止在位者(例如杀掉进程,或屏蔽其 D-Bus 激活), 然后重新请求;定期重试的守护进程会在名称变为空闲的那一刻接管。 ## Deft 实例之间的自定义名称 众所周知名称不过是总线命名空间中的字符串,因此 deft 进程可以定义 自己的服务 —— `org.deft.<app>`、`com.example.<service>.<instance>`, 只要合适即可。这是不同的 OS 进程之间以及与外部程序互操作的路径。 服务端注册名称并导出接口:
# server.dft — run in its own deft process
set conn [dbus/connect]
set rc   [await [dbus/register-name $conn "org.deft.notify.1" 7]]

[dbus/export $conn "/org/deft/notify" "org.deft.Notify" %{
    "Send" {|text| [handle-notification $text]}
}]
[vwait]
客户端调用它:
# client.dft
set conn [dbus/connect]
[await [dbus/call $conn "org.deft.notify.1" "/org/deft/notify" \
       "org.deft.Notify" "Send" "s" "hello"]]
当多个实例必须共存时,给每个实例取不同的名称(`org.deft.worker.1`、 `org.deft.worker.2`……);当它们共享一个名称时,用标志 `6`/`7` 控制替换。实例可以通过向总线查询已有名称来发现彼此:
set names [await [dbus/call $conn "org.freedesktop.DBus" "/org/freedesktop/DBus" \
           "org.freedesktop.DBus" "ListNames"]]
若想用扇出取代请求/响应,可发送信号并按匹配规则订阅:
# publisher
[dbus/emit $conn "/org/deft/notify" "org.deft.Notify" "Posted" "s" $text]

# subscriber — closure receives %{ :sender :path :interface :member :args }
[dbus/subscribe $conn "type='signal',interface='org.deft.Notify',member='Posted'"
    {|ev| [on-posted $ev~args]}]
## 同进程内的替代方案 如果所谓“实例”是同一进程内的运行时 —— TUI 面板、由 `runtime/spawn` 派生的子运行时、集群工作进程 —— 那你几乎不会想用 D-Bus。这些 运行时已经共享平台层: - `pub/*` 是贯穿运行时层级的共享代理,采用普通值投递 —— 见 发布/订阅。 - `@rpc` + `rpc` 提供按运行时名称寻址的一对一类型化调用 —— 见 RPC。 - `config/*` 是共享的反应式键/值注册表。 当对端是独立的 OS 进程,或者外部客户端(桌面 shell、`notify-send`、 系统服务)必须通过众所周知名称访问你时,D-Bus 才是正确的工具。 ## 生命周期与内省 - 名称会一直持有到连接关闭。`dbus/disconnect`、资源的 GC 终结器 以及进程退出都会释放连接拥有的所有名称。 - `[dbus/info $conn]` 报告 `%{ :state :unique-name :pending :subs :exports }`。 - `[dbus/unique-name $conn]` 在 `HELLO` 完成后返回该连接的 `:1.x` 名称。 - 总线对象(`org.freedesktop.DBus`,路径 `/org/freedesktop/DBus`) 会应答 `ListNames`、`GetNameOwner`、`NameHasOwner` 和 `GetConnectionUnixProcessID` —— 足以在无额外依赖的情况下构建 服务发现与诊断。 - `NameOwnerChanged` 是总线信号:订阅它(为关心的名称加 `arg0` 匹配),即可在服务出现、消失或易主的那一刻获知。 ## 快速参考 | 形式 | 用途 | |---|---| | `[dbus/connect ?opts]` | 连接会话总线(默认)或系统总线(`:system true`) | | `[dbus/call $c dest path iface method ?sig @args]` | 方法调用;`await` 其 future | | `[dbus/send $c dest path iface method ?sig @args]` | 发出即忘的调用 | | `[dbus/register-name $c "name" ?flags]` | 请求众所周知名称;`await` 结果码 | | `[dbus/export $c "path" "iface" %{ "Method" $fn }]` | 提供方法(服务端角色) | | `[dbus/emit $c "path" "iface" "Signal" ?sig @args]` | 广播信号 | | `[dbus/subscribe $c "match rule" $closure]` | 在匹配的信号上运行 `$closure` | | `[dbus/disconnect $c]` | 关闭;释放拥有的名称 |