D-Bus 完整教程 / 04 - D-Bus 内省机制
第 04 章:D-Bus 内省机制
4.1 什么是内省(Introspection)?
内省是 D-Bus 的 接口发现机制。通过内省,客户端可以在运行时查询服务端提供了哪些对象、接口、方法、属性和信号,而无需提前知道接口的定义。
这类似于:
- Web API 中的 OpenAPI / Swagger 文档
- COM 中的
ITypeInfo - Java 的反射(Reflection)
内省的价值
| 场景 | 作用 |
|---|---|
| 开发调试 | 工具(d-feet、busctl)自动发现可用接口 |
| 动态绑定 | 脚本语言(Python)在运行时调用任意方法 |
| 文档生成 | 从 XML 自动生成 API 文档 |
| 代码生成 | gdbus-codegen 从 XML 生成 C 绑定代码 |
| 版本检查 | 验证服务端是否支持特定方法 |
4.2 Introspectable 接口
每个 D-Bus 对象都隐式实现了 org.freedesktop.DBus.Introspectable 接口,该接口只有一个方法:
org.freedesktop.DBus.Introspectable.Introspect() → (s: xml_data)
调用示例
# 使用 busctl 内省
busctl introspect org.freedesktop.login1 \
/org/freedesktop/login1 \
org.freedesktop.login1.Manager
# 使用 gdbus 内省(获取原始 XML)
gdbus introspect --session \
--dest org.freedesktop.DBus \
--object-path /org/freedesktop/DBus
# 使用 dbus-send 获取原始 XML
dbus-send --session --dest=org.freedesktop.DBus \
--type=method_call --print-reply \
/org/freedesktop/DBus \
org.freedesktop.DBus.Introspectable.Introspect
内省 XML 输出示例
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.freedesktop.DBus.Introspectable">
<method name="Introspect">
<arg name="xml_data" type="s" direction="out"/>
</method>
</interface>
<interface name="org.freedesktop.DBus.Peer">
<method name="Ping"/>
<method name="GetMachineId">
<arg name="machine_uuid" type="s" direction="out"/>
</method>
</interface>
<interface name="org.freedesktop.DBus.Properties">
<method name="Get">
<arg name="interface_name" type="s" direction="in"/>
<arg name="property_name" type="s" direction="in"/>
<arg name="value" type="v" direction="out"/>
</method>
<method name="Set">
<arg name="interface_name" type="s" direction="in"/>
<arg name="property_name" type="s" direction="in"/>
<arg name="value" type="v" direction="in"/>
</method>
<method name="GetAll">
<arg name="interface_name" type="s" direction="in"/>
<arg name="props" type="a{sv}" direction="out"/>
</method>
<signal name="PropertiesChanged">
<arg name="interface_name" type="s"/>
<arg name="changed_properties" type="a{sv}"/>
<arg name="invalidated_properties" type="as"/>
</signal>
</interface>
</node>
4.3 XML 元素详解
4.3.1 <node> — 对象节点
<!-- 顶级节点 -->
<node>
<!-- 该对象上的接口 -->
</node>
<!-- 子节点声明(可选) -->
<node name="child_object"/>
4.3.2 <interface> — 接口
<interface name="org.example.MyInterface">
<!-- 方法、属性、信号 -->
</interface>
接口命名规则:
- 使用反向域名格式(如
org.example.MyService) - 接口名称 ≠ 总线名称 ≠ 对象路径
- 一个对象可以实现多个接口
4.3.3 <method> — 方法
<method name="DoSomething">
<!-- 输入参数 -->
<arg name="input_string" type="s" direction="in"/>
<arg name="input_number" type="i" direction="in"/>
<!-- 输出参数 -->
<arg name="result" type="a{sv}" direction="out"/>
</method>
<!-- 无参数方法 -->
<method name="Ping"/>
| 属性 | 说明 |
|---|---|
name | 方法名称 |
type | 参数的 D-Bus 类型签名 |
direction | in(输入)或 out(输出) |
name(arg) | 参数名称(可选,仅供文档) |
4.3.4 <signal> — 信号
<signal name="StateChanged">
<arg name="new_state" type="s"/>
<arg name="old_state" type="s"/>
<arg name="reason" type="u"/>
</signal>
信号参数没有 direction 属性——信号总是从发送方到接收方。
4.3.5 <property> — 属性
<!-- 可读写属性 -->
<property name="Volume" type="i" access="readwrite"/>
<!-- 只读属性 -->
<property name="Name" type="s" access="read"/>
<!-- 只写属性(罕见) -->
<property name="Secret" type="s" access="write"/>
access 值 | 含义 |
|---|---|
read | 只读(可 Get,可 GetAll) |
readwrite | 可读写(可 Get / Set) |
write | 只写(只能 Set) |
4.3.6 <annotation> — 注解
注解为工具提供额外元信息:
<method name="Connect">
<arg name="device" type="s" direction="in"/>
<annotation name="org.freedesktop.DBus.Deprecated" value="true"/>
<annotation name="org.gtk.Ephemeral" value="true"/>
<annotation name="org.freedesktop.DBus.Method.NoReply" value="true"/>
</method>
常用注解:
| 注解名 | 说明 |
|---|---|
org.freedesktop.DBus.Deprecated | 标记为已弃用 |
org.freedesktop.DBus.Method.NoReply | Fire-and-forget 方法 |
org.freedesktop.DBus.Method.Async | 异步实现标记 |
org.freedesktop.DBus.Property.EmitsChangedSignal | 属性变化信号策略 |
org.gtk.Ephemeral | GNOME 短生命周期方法 |
4.4 标准接口
D-Bus 规范定义了四个标准接口,所有对象都应实现:
4.4.1 org.freedesktop.DBus.Introspectable
Introspect() → (s: xml_data)
4.4.2 org.freedesktop.DBus.Properties
Get(s: interface_name, s: property_name) → (v: value)
Set(s: interface_name, s: property_name, v: value)
GetAll(s: interface_name) → (a{sv}: props)
信号:
PropertiesChanged(s: interface_name, a{sv}: changed, as: invalidated)
4.4.3 org.freedesktop.DBus.Peer
Ping()
GetMachineId() → (s: machine_uuid)
4.4.4 org.freedesktop.DBus.ObjectManager
GetManagedObjects() → (a{oa{sa{sv}}}: objects)
信号:
InterfacesAdded(o: object_path, a{sa{sv}}: interfaces)
InterfacesRemoved(o: object_path, as: interfaces)
4.5 子节点遍历
内省的结果可以包含子节点声明:
<node>
<interface name="org.freedesktop.login1.Manager">
<!-- 方法和属性 -->
</interface>
<node name="seat"/>
<node name="session"/>
<node name="user"/>
</node>
使用 busctl tree 可以递归遍历:
# 查看完整对象树
busctl tree org.freedesktop.login1
# 输出:
# /org/freedesktop/login1
# ├─/org/freedesktop/login1/seat
# │ └─/org/freedesktop/login1/seat/seat0
# ├─/org/freedesktop/login1/session
# │ ├─/org/freedesktop/login1/session/c1
# │ └─/org/freedesktop/login1/session/c2
# └─/org/freedesktop/login1/user
# └─/org/freedesktop/login1/user/_1000
注意:子节点声明仅为导航便利,并不意味着子节点的接口由父对象实现。每个节点独立实现自己的接口。
4.6 编程实现内省
Python 示例
#!/usr/bin/env python3
"""手动调用 Introspect 方法并解析 XML"""
import dbus
import xml.etree.ElementTree as ET
def introspect(bus_name, object_path):
bus = dbus.SessionBus()
obj = bus.get_object(bus_name, object_path)
iface = dbus.Interface(obj, 'org.freedesktop.DBus.Introspectable')
xml_str = iface.Introspect()
root = ET.fromstring(xml_str)
print(f"对象路径: {object_path}")
print(f"子节点: {[n.get('name') for n in root.findall('node')]}")
print()
for iface_elem in root.findall('interface'):
iface_name = iface_elem.get('name')
methods = [m.get('name') for m in iface_elem.findall('method')]
signals = [s.get('name') for s in iface_elem.findall('signal')]
props = [p.get('name') for p in iface_elem.findall('property')]
print(f"接口: {iface_name}")
if methods:
print(f" 方法: {', '.join(methods)}")
if signals:
print(f" 信号: {', '.join(signals)}")
if props:
print(f" 属性: {', '.join(props)}")
print()
# 使用
introspect('org.freedesktop.DBus', '/org/freedesktop/DBus')
GDBus (C) 示例
/* 使用 GDBusProxy 进行内省 */
#include <gio/gio.h>
static void introspect_object(GDBusConnection *conn, const char *bus_name, const char *object_path) {
GError *error = NULL;
GDBusProxy *proxy = g_dbus_proxy_new_for_bus_sync(
G_BUS_TYPE_SESSION,
G_DBUS_PROXY_FLAGS_NONE,
NULL, /* introspection data */
bus_name,
object_path,
"org.freedesktop.DBus.Introspectable",
NULL, /* cancellable */
&error
);
if (error) {
g_printerr("错误: %s\n", error->message);
g_error_free(error);
return;
}
GVariant *result = g_dbus_proxy_call_sync(
proxy, "Introspect", NULL,
G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error
);
if (result) {
const char *xml_data;
g_variant_get(result, "(&s)", &xml_data);
g_print("内省 XML:\n%s\n", xml_data);
g_variant_unref(result);
}
g_object_unref(proxy);
}
int main(void) {
GMainLoop *loop = g_main_loop_new(NULL, FALSE);
GDBusConnection *conn = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, NULL);
introspect_object(conn, "org.freedesktop.DBus", "/org/freedesktop/DBus");
g_object_unref(conn);
g_main_loop_unref(loop);
return 0;
}
编译:
gcc -o introspect introspect.c $(pkg-config --cflags --libs gio-2.0)
4.7 从 XML 生成代码
gdbus-codegen
# 创建接口描述文件
cat > org.example.Calculator.xml << 'EOF'
<?xml version="1.0"?>
<node xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd">
<interface name="org.example.Calculator">
<method name="Add">
<arg name="a" type="d" direction="in"/>
<arg name="b" type="d" direction="in"/>
<arg name="result" type="d" direction="out"/>
</method>
<method name="Subtract">
<arg name="a" type="d" direction="in"/>
<arg name="b" type="d" direction="in"/>
<arg name="result" type="d" direction="out"/>
</method>
<signal name="CalculationPerformed">
<arg name="operation" type="s"/>
<arg name="result" type="d"/>
</signal>
<property name="History" type="as" access="read"/>
</interface>
</node>
EOF
# 生成客户端代理代码
gdbus-codegen \
--generate-c-code calculator-client \
--c-namespace Example \
--interface-prefix org.example \
org.example.Calculator.xml
# 生成服务端骨架代码
gdbus-codegen \
--generate-c-code calculator-server \
--c-namespace Example \
--interface-prefix org.example \
--c-generate-object-manager \
org.example.Calculator.xml
这将生成以下文件:
calculator-client.h/calculator-client.c— 客户端代理(Proxy)calculator-server.h/calculator-server.c— 服务端骨架(Skeleton)
4.8 内省的局限性
| 局限 | 说明 |
|---|---|
| 无参数文档 | XML 中的 name 属性仅供文档,无类型约束 |
| 无默认值 | 不支持参数默认值 |
| 无继承 | 接口之间没有继承关系 |
| 无版本号 | 接口没有版本概念 |
| 可选实现 | 服务端可能不实现 Introspectable |
4.9 最佳实践
- 始终实现 Introspectable:这是 D-Bus 规范的要求
- 使用标准注解:如
EmitsChangedSignal,让客户端知道属性变化策略 - 子节点声明:为动态创建的对象使用子节点声明
- 保持 XML 同步:接口描述 XML 应与实际实现保持一致
- 利用代码生成:避免手写绑定代码
本章小结
| 概念 | 说明 |
|---|---|
| 内省 | 运行时查询对象接口的方法 |
| Introspectable | 所有对象必须实现的标准接口 |
| XML 描述 | 接口的标准格式,包含方法/信号/属性 |
<method> | 描述方法及其参数类型和方向 |
<signal> | 描述信号及其参数类型 |
<property> | 描述属性及其类型和访问权限 |
<annotation> | 工具元信息(弃用标记、行为提示) |