强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 类型签名
directionin(输入)或 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.NoReplyFire-and-forget 方法
org.freedesktop.DBus.Method.Async异步实现标记
org.freedesktop.DBus.Property.EmitsChangedSignal属性变化信号策略
org.gtk.EphemeralGNOME 短生命周期方法

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 最佳实践

  1. 始终实现 Introspectable:这是 D-Bus 规范的要求
  2. 使用标准注解:如 EmitsChangedSignal,让客户端知道属性变化策略
  3. 子节点声明:为动态创建的对象使用子节点声明
  4. 保持 XML 同步:接口描述 XML 应与实际实现保持一致
  5. 利用代码生成:避免手写绑定代码

本章小结

概念说明
内省运行时查询对象接口的方法
Introspectable所有对象必须实现的标准接口
XML 描述接口的标准格式,包含方法/信号/属性
<method>描述方法及其参数类型和方向
<signal>描述信号及其参数类型
<property>描述属性及其类型和访问权限
<annotation>工具元信息(弃用标记、行为提示)

扩展阅读