Vala 语言入门教程 / 10 - D-Bus 集成
第 10 章:D-Bus 集成
D-Bus 是 Linux 桌面的核心 IPC(进程间通信)机制。GNOME 的几乎所有系统服务都通过 D-Bus 暴露接口。
10.1 D-Bus 概述
10.1.1 什么是 D-Bus
D-Bus 是一个消息总线系统,允许应用程序之间进行通信:
┌─────────────────────────────────────────────────┐
│ D-Bus 总线 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 系统总线 │ │ 会话总线 │ │ 启动服务 │ │
│ │(System) │ │(Session) │ │(Activation)│ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
└───────┼──────────────┼──────────────┼────────────┘
│ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│Network │ │ 文件管理 │ │ 通知守护 │
│Manager │ │ 器 │ │ 进程 │
│(system) │ │(session)│ │(session)│
└─────────┘ └─────────┘ └─────────┘
10.1.2 两条总线
| 总线 | 用途 | 地址 |
|---|---|---|
| 系统总线(System Bus) | 系统级服务(网络、蓝牙、电源) | unix:path=/var/run/dbus/system_bus_socket |
| 会话总线(Session Bus) | 用户级服务(通知、文件管理) | 随登录自动生成 |
10.1.3 D-Bus 通信模型
Client(客户端) Service(服务端)
│ │
│─── 方法调用 ───────────→│
│ │
│←── 方法返回 ────────────│
│ │
│←── 信号(订阅)─────────│
│ │
│─── 属性读取 ───────────→│
│←── 属性值 ──────────────│
10.2 查看系统 D-Bus 服务
10.2.1 使用 busctl 命令
# 列出所有总线上的服务
busctl list
# 列出会话总线上的服务
busctl --user list
# 查看某个服务的对象树
busctl tree org.freedesktop.login1
# 查看接口详情
busctl introspect org.freedesktop.login1 /org/freedesktop/login1
# 调用方法
busctl call org.freedesktop.login1 /org/freedesktop/login1 \
org.freedesktop.login1.Manager GetSession s ""
# 监听信号
busctl monitor org.freedesktop.login1
10.2.2 使用 D-Feet(图形化工具)
# 安装 D-Feet
sudo apt install d-feet
# 启动
d-feet
D-Feet 提供图形界面浏览 D-Bus 服务、调用方法、查看属性。
10.3 Vala 中的 D-Bus 基础
10.3.1 使用 D-Bus 代理(客户端)
void main () {
try {
// 连接到会话总线
var connection = GLib.Bus.get_sync (GLib.BusType.SESSION, null);
// 创建代理
var proxy = new GLib.DBusProxy.sync (
connection,
GLib.DBusProxyFlags.NONE,
null, // InterfaceInfo
"org.freedesktop.DBus", // Bus name
"/org/freedesktop/DBus", // Object path
"org.freedesktop.DBus", // Interface
null // Cancellable
);
// 调用方法
var result = proxy.call_sync (
"ListNames", // Method name
null, // Parameters
GLib.DBusCallFlags.NONE,
-1, // Timeout
null // Cancellable
);
// 解析结果
if (result != null) {
var names = result.get_child_value (0);
uint length = names.n_children ();
print ("总线上有 %u 个服务:\n", length);
for (uint i = 0; i < length && i < 20; i++) {
string? name = names.get_child_value (i).get_string ();
print (" %s\n", name);
}
}
} catch (GLib.Error e) {
printerr ("D-Bus 错误: %s\n", e.message);
}
}
10.3.2 使用总线拥有者(BusName)
void main () {
// 获取总线连接
var connection = GLib.Bus.get_sync (GLib.BusType.SESSION, null);
// 检查服务是否存在
try {
var result = connection.call_sync (
"org.freedesktop.DBus",
"/org/freedesktop/DBus",
"org.freedesktop.DBus",
"NameHasOwner",
new GLib.Variant ("(s)", "org.freedesktop.portal.Desktop"),
new GLib.VariantType ("(b)"),
GLib.DBusCallFlags.NONE,
-1,
null
);
bool has_owner = result.get_child_value (0).get_boolean ();
print ("portal 服务存在: %s\n", has_owner.to_string ());
} catch (GLib.Error e) {
printerr ("错误: %s\n", e.message);
}
}
10.4 D-Bus 服务端实现
10.4.1 定义 D-Bus 接口
使用 Vala 的 [DBus] 注解可以方便地定义 D-Bus 服务:
// 定义 D-Bus 接口
[DBus (name = "com.example.Calculator")]
public class CalculatorService : Object {
private int last_result = 0;
// D-Bus 方法
public int add (int a, int b) {
last_result = a + b;
print ("计算: %d + %d = %d\n", a, b, last_result);
return last_result;
}
public int subtract (int a, int b) {
last_result = a - b;
return last_result;
}
public int multiply (int a, int b) {
last_result = a * b;
return last_result;
}
public double divide (int a, int b) throws GLib.Error {
if (b == 0) {
throw new GLib.IOError.FAILED ("除数不能为零");
}
last_result = a / b;
return (double) a / b;
}
// D-Bus 属性
public int last_result {
get { return last_result; }
}
// D-Bus 信号
public signal void calculation_done (int result);
}
void main () {
var loop = new GLib.MainLoop ();
// 注册 D-Bus 服务
GLib.Bus.own_name (
GLib.BusType.SESSION,
"com.example.Calculator",
GLib.BusNameOwnerFlags.NONE,
(connection, name) => {
// 总线名已获取
try {
connection.register_object (
"/com/example/Calculator",
new CalculatorService ()
);
print ("D-Bus 服务已注册: %s\n", name);
} catch (GLib.Error e) {
printerr ("注册失败: %s\n", e.message);
}
},
() => {
// 总线名获取成功
print ("总线名已获取\n");
},
(connection, name) => {
// 总线名丢失
printerr ("总线名丢失: %s\n", name);
loop.quit ();
}
);
print ("计算器服务运行中...\n");
print ("测试: busctl --user call com.example.Calculator /com/example/Calculator com.example.Calculator add ii 3 5\n");
loop.run ();
}
10.4.2 异步 D-Bus 服务
[DBus (name = "com.example.FileService")]
public class FileService : Object {
// 异步 D-Bus 方法
public async string read_file (string path) throws GLib.Error {
string content;
size_t length;
GLib.FileUtils.get_contents (path, out content, out length);
return content;
}
public async void write_file (string path, string content)
throws GLib.Error
{
GLib.FileUtils.set_contents (path, content);
}
// 同步方法
public bool file_exists (string path) {
return GLib.FileUtils.test (path, GLib.FileTest.EXISTS);
}
public signal void file_changed (string path, string event_type);
}
void main () {
var loop = new GLib.MainLoop ();
GLib.Bus.own_name (
GLib.BusType.SESSION,
"com.example.FileService",
GLib.BusNameOwnerFlags.NONE,
(connection, name) => {
try {
var service = new FileService ();
connection.register_object (
"/com/example/FileService",
service
);
// 模拟文件变化信号
GLib.Timeout.add (5000, () => {
service.file_changed ("/tmp/test.txt", "modified");
return GLib.Source.CONTINUE;
});
print ("文件服务已注册\n");
} catch (GLib.Error e) {
printerr ("注册失败: %s\n", e.message);
}
},
() => {},
(connection, name) => { loop.quit (); }
);
loop.run ();
}
10.5 D-Bus 客户端调用
10.5.1 使用 GDBusProxy
void main () {
try {
// 创建代理
var proxy = new GLib.DBusProxy.for_bus_sync (
GLib.BusType.SESSION,
GLib.DBusProxyFlags.NONE,
null,
"com.example.Calculator",
"/com/example/Calculator",
"com.example.Calculator",
null
);
// 调用方法
var result = proxy.call_sync (
"Add",
new GLib.Variant ("(ii)", 10, 20),
GLib.DBusCallFlags.NONE,
-1,
null
);
int sum = result.get_child_value (0).get_int32 ();
print ("10 + 20 = %d\n", sum);
// 读取属性
var props = new GLib.DBusProxy.for_bus_sync (
GLib.BusType.SESSION,
GLib.DBusProxyFlags.NONE,
null,
"com.example.Calculator",
"/com/example/Calculator",
"org.freedesktop.DBus.Properties",
null
);
var prop_result = props.call_sync (
"Get",
new GLib.Variant ("(ss)",
"com.example.Calculator",
"LastResult"
),
GLib.DBusCallFlags.NONE,
-1,
null
);
int last = (int) prop_result.get_child_value (0)
.get_variant ()
.get_int32 ();
print ("上次结果: %d\n", last);
} catch (GLib.Error e) {
printerr ("调用失败: %s\n", e.message);
}
}
10.5.2 监听 D-Bus 信号
void main () {
var loop = new GLib.MainLoop ();
try {
var connection = GLib.Bus.get_sync (GLib.BusType.SESSION, null);
// 订阅信号
uint subscription_id = connection.signal_subscribe (
"com.example.Calculator", // sender
"com.example.Calculator", // interface
"CalculationDone", // signal name
"/com/example/Calculator", // object path
null, // arg0
GLib.DBusSignalFlags.NONE,
(conn, sender, object_path, interface_name,
signal_name, parameters) => {
int result = parameters.get_child_value (0).get_int32 ();
print ("收到信号: 计算完成,结果=%d\n", result);
}
);
print ("正在监听 D-Bus 信号...\n");
print ("按 Ctrl+C 退出\n");
loop.run ();
} catch (GLib.Error e) {
printerr ("错误: %s\n", e.message);
}
}
10.6 GObject Introspection 和 D-Bus
10.6.1 调用系统 D-Bus 服务
void main () {
// 调用系统通知服务
try {
var proxy = new GLib.DBusProxy.for_bus_sync (
GLib.BusType.SESSION,
GLib.DBusProxyFlags.NONE,
null,
"org.freedesktop.Notifications",
"/org/freedesktop/Notifications",
"org.freedesktop.Notifications",
null
);
// 发送通知
var result = proxy.call_sync (
"Notify",
new GLib.Variant ("(susssasa{sv}i)",
"ValaApp", // app_name
0, // replaces_id
"dialog-information", // icon
"Vala 通知", // summary
"这是一条来自 Vala 的通知!", // body
new GLib.Variant ("as", new string[0]), // actions
new GLib.Variant ("a{sv}", new GLib.Variant[0]), // hints
5000 // timeout (ms)
),
GLib.DBusCallFlags.NONE,
-1,
null
);
uint32 notification_id = result.get_child_value (0).get_uint32 ();
print ("通知已发送,ID: %u\n", notification_id);
} catch (GLib.Error e) {
printerr ("通知发送失败: %s\n", e.message);
}
}
10.6.2 查询系统信息
void main () {
// 查询系统登录信息
try {
var proxy = new GLib.DBusProxy.for_bus_sync (
GLib.BusType.SYSTEM,
GLib.DBusProxyFlags.NONE,
null,
"org.freedesktop.login1",
"/org/freedesktop/login1",
"org.freedesktop.login1.Manager",
null
);
// 获取当前会话
var result = proxy.call_sync (
"GetSession",
new GLib.Variant ("(s)", ""),
GLib.DBusCallFlags.NONE,
-1,
null
);
if (result != null) {
string session_path = result.get_child_value (0)
.get_obj_path ();
print ("当前会话路径: %s\n", session_path);
}
} catch (GLib.Error e) {
printerr ("查询失败: %s\n", e.message);
}
}
10.7 属性和接口定义
10.7.1 完整的 D-Bus 服务定义
// 定义接口 XML(可选,Vala 也可以自动推断)
/*
<node>
<interface name="com.example.ConfigService">
<method name="GetConfig">
<arg name="key" direction="in" type="s"/>
<arg name="value" direction="out" type="s"/>
</method>
<method name="SetConfig">
<arg name="key" direction="in" type="s"/>
<arg name="value" direction="in" type="s"/>
</method>
<property name="Version" type="s" access="read"/>
<signal name="ConfigChanged">
<arg name="key" type="s"/>
<arg name="value" type="s"/>
</signal>
</interface>
</node>
*/
[DBus (name = "com.example.ConfigService")]
public class ConfigService : Object {
private GLib.HashTable<string, string> config;
construct {
config = new GLib.HashTable<string, string> (str_hash, str_equal);
config["app.name"] = "MyApp";
config["app.version"] = "1.0.0";
config["app.debug"] = "true";
}
// D-Bus 方法
public string get_config (string key) throws GLib.Error {
if (!config.contains (key)) {
throw new GLib.IOError.NOT_FOUND (
"配置键不存在: %s".printf (key)
);
}
return config[key];
}
public void set_config (string key, string value) {
config[key] = value;
config_changed (key, value);
print ("配置更新: %s = %s\n", key, value);
}
// D-Bus 属性
public string version {
owned get { return "1.0.0"; }
}
// D-Bus 信号
public signal void config_changed (string key, string value);
}
void main () {
var loop = new GLib.MainLoop ();
GLib.Bus.own_name (
GLib.BusType.SESSION,
"com.example.ConfigService",
GLib.BusNameOwnerFlags.NONE,
(connection, name) => {
try {
connection.register_object (
"/com/example/ConfigService",
new ConfigService ()
);
print ("配置服务已启动\n");
} catch (GLib.Error e) {
printerr ("注册失败: %s\n", e.message);
}
},
() => {},
(connection, name) => { loop.quit (); }
);
loop.run ();
}
10.8 系统总线 vs 会话总线
| 特性 | 系统总线 | 会话总线 |
|---|---|---|
| 访问权限 | 需要 root 或 PolicyKit | 用户级 |
| 地址 | /var/run/dbus/system_bus_socket | 自动生成 |
| 用途 | 系统服务 | 用户应用 |
| 安全 | 高(受限访问) | 中(用户会话内) |
| 示例服务 | NetworkManager, systemd | Notifications, Portal |
10.8.1 使用 PolicyKit 进行权限检查
[DBus (name = "com.example.SystemService")]
public class SystemService : Object {
public void shutdown () throws GLib.Error {
// 在实际应用中,这里应该检查 PolicyKit 权限
print ("系统关机请求\n");
// 使用 systemd 关机
try {
var proxy = new GLib.DBusProxy.for_bus_sync (
GLib.BusType.SYSTEM,
GLib.DBusProxyFlags.NONE,
null,
"org.freedesktop.login1",
"/org/freedesktop/login1",
"org.freedesktop.login1.Manager",
null
);
proxy.call_sync (
"PowerOff",
new GLib.Variant ("(b)", false),
GLib.DBusCallFlags.NONE,
-1,
null
);
} catch (GLib.Error e) {
throw new GLib.IOError.FAILED (
"关机失败: %s".printf (e.message)
);
}
}
public string status {
owned get { return "running"; }
}
}
void main () {
var loop = new GLib.MainLoop ();
GLib.Bus.own_name (
GLib.BusType.SYSTEM,
"com.example.SystemService",
GLib.BusNameOwnerFlags.NONE,
(connection, name) => {
try {
connection.register_object (
"/com/example/SystemService",
new SystemService ()
);
print ("系统服务已注册\n");
} catch (GLib.Error e) {
printerr ("注册失败: %s\n", e.message);
}
},
() => {},
(connection, name) => { loop.quit (); }
);
loop.run ();
}
10.9 业务场景:桌面通知服务
[DBus (name = "org.freedesktop.Notifications")]
public class NotificationService : Object {
private uint32 next_id = 1;
private GLib.HashTable<uint32, NotificationInfo> notifications;
construct {
notifications = new GLib.HashTable<uint32, NotificationInfo> (
direct_hash, direct_equal
);
}
public uint32 notify (
string app_name,
uint32 replaces_id,
string app_icon,
string summary,
string body,
string[] actions,
GLib.HashTable<string, GLib.Variant> hints,
int32 expire_timeout
) throws GLib.Error {
uint32 id = replaces_id > 0 ? replaces_id : next_id++;
if (replaces_id == 0) next_id++;
var info = new NotificationInfo ();
info.app_name = app_name;
info.summary = summary;
info.body = body;
info.timestamp = new GLib.DateTime.now_local ().to_string ();
notifications[id] = info;
print ("┌─────────────────────────────────┐\n");
print ("│ 📢 %s\n", summary);
print ("│ %s\n", body);
print ("│ 来自: %s | ID: %u\n", app_name, id);
print ("└─────────────────────────────────┘\n");
notification_received (id, app_name, summary, body);
// 自动过期
if (expire_timeout > 0) {
GLib.Timeout.add_once ((uint) expire_timeout, () => {
close_notification (id);
});
}
return id;
}
public void close_notification (uint32 id) throws GLib.Error {
if (notifications.contains (id)) {
notifications.remove (id);
notification_closed (id, 1); // 1 = dismissed
print ("通知已关闭: %u\n", id);
}
}
public string[] get_capabilities () {
return {
"body",
"body-markup",
"body-hyperlinks",
"persistence"
};
}
public void get_server_information (
out string name,
out string vendor,
out string version,
out string spec_version
) {
name = "ValaNotificationService";
vendor = "Example";
version = "1.0.0";
spec_version = "1.2";
}
public signal void notification_received (
uint32 id, string app_name,
string summary, string body
);
public signal void notification_closed (uint32 id, uint32 reason);
}
public class NotificationInfo : Object {
public string app_name;
public string summary;
public string body;
public string timestamp;
}
void main () {
var loop = new GLib.MainLoop ();
GLib.Bus.own_name (
GLib.BusType.SESSION,
"org.freedesktop.Notifications",
GLib.BusNameOwnerFlags.REPLACE,
(connection, name) => {
try {
var service = new NotificationService ();
connection.register_object (
"/org/freedesktop/Notifications",
service
);
print ("通知服务已启动\n");
print ("测试: busctl --user call org.freedesktop.Notifications /org/freedesktop/Notifications org.freedesktop.Notifications Notify susssasa{sv}i \"Test\" 0 \"\" \"测试标题\" \"测试内容\" 0 0 5000\n");
} catch (GLib.Error e) {
printerr ("注册失败: %s\n", e.message);
}
},
() => {},
(connection, name) => { loop.quit (); }
);
loop.run ();
}
10.10 注意事项
⚠️ D-Bus 常见陷阱
- 总线名冲突:确保你的服务名唯一,使用反向域名格式
- 线程安全:D-Bus 回调可能在不同线程
- 超时处理:设置合理的调用超时
- 错误传播:D-Bus 错误会包装在
GLib.Error中 - 类型匹配:参数类型必须与接口定义完全匹配
- 资源清理:使用
GLib.Bus.unown_name()释放总线名
10.11 扩展阅读
| 资源 | 链接 |
|---|---|
| D-Bus 规范 | https://dbus.freedesktop.org/doc/dbus-specification.html |
| GDBus 文档 | https://docs.gtk.org/gio/DBus.html |
| D-Bus 最佳实践 | https://dbus.freedesktop.org/doc/dbus-api-design.html |
| busctl 手册 | man busctl |
| D-Feet | https://wiki.gnome.org/Apps/DFeet |
| GNOME D-Bus 教程 | https://developer.gnome.org/documentation/tutorials/d-bus.html |
10.12 总结
| 要点 | 说明 |
|---|---|
| D-Bus | Linux IPC 标准,两条总线(系统/会话) |
| 服务端 | [DBus] 注解 + Bus.own_name |
| 客户端 | DBusProxy 或直接 connection.call_sync |
| 信号 | connection.signal_subscribe |
| 属性 | 通过 org.freedesktop.DBus.Properties |
| 安全 | 系统总线需要 PolicyKit |
下一章我们将学习 Docker 构建与部署。→ 第 11 章:Docker 构建与部署