目录
一、基本概念
- D‑Bus 与 gdbus
D‑Bus 是 Linux 系统中常用的进程间通信机制,而 gdbus 是 GLib 提供的 D‑Bus 接口实现,简化了服务与客户端的编写。利用 gdbus,你可以定义接口、注册对象、实现方法、发送信号等。 - 接口描述(Introspection)
gdbus 通过 XML 格式的 introspection 数据描述 D‑Bus 接口、方法、信号和属性。编写接口描述文件后,程序可以利用这些信息自动注册对象并实现调用处理函数。
二、服务端开发
gdbus 在服务端开发时主要提供两种方式:
1. 使用原始消息处理(手动注册对象)方式
这种方式则是直接使用 g_dbus_connection_register_object 接口,将一个对象(以及对应的接口 introspection 数据)注册到 GDBusConnection 上,然后在注册时提供一个处理回调。在回调中你需要手动解析和响应 DBus 消息。虽然这种方式灵活性更高,但相对而言需要编写更多低层代码来处理消息分发和错误处理。
2. 使用接口骨架(Skeleton)方式
这种方式通常借助 gdbus-codegen 基于接口的 introspection XML 生成相应的服务端 skeleton 代码。生成的 skeleton 类型(如 GDBusInterfaceSkeleton 或其派生类型)封装了对 DBus 消息的处理,你只需要重写或连接相应的信号(如方法调用、属性变化等)的回调函数,就能实现服务端逻辑。这种模式比较“面向对象”,能让开发者免去很多底层消息处理细节。
简单来说,通过接口骨架方式可以更方便地实现协议逻辑(推荐如果已有 introspection 数据),而原始消息处理方式则给了开发者更大的控制权,适合对底层细节有特殊需求的场景。
直接上代码,使用的GLib版本是:2.83.5。
方式1:
#include <stdio.h>
#include <locale.h>
#include "gio/gio.h"
#include "glib-unix.h"
/* 1. 定义接口描述 XML */
static const gchar introspection_xml[] =
"<node>"
" <interface name='com.example.GDBusExample'>"
" <method name='Hello'>"
" <arg type='s' name='greeting' direction='in'/>"
" <arg type='s' name='response' direction='out'/>"
" </method>"
" </interface>"
"</node>";
/* 2. 实现方法调用处理函数 */
static void
on_method_call(GDBusConnection *connection,
const gchar *sender,
const gchar *object_path,
const gchar *interface_name,
const gchar *method_name,
GVariant *parameters,
GDBusMethodInvocation *invocation,
gpointer user_data)
{
if (g_strcmp0(method_name, "Hello") == 0)
{
const gchar *greeting;
/* 从参数中获取传入的字符串 */
g_variant_get(parameters, "(&s)", &greeting);
g_print("收到客户端调用,参数:%s\n", greeting);
/* 构造返回信息 */
gchar *response = g_strdup_printf("Hello, %s!", greeting);
/* 返回结果 */
g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", response));
g_free(response);
}
}
/* 定义接口的虚表 */
static const GDBusInterfaceVTable interface_vtable = {
.method_call = on_method_call,
.get_property = NULL,
.set_property = NULL};
/* 3. 当成功连接到总线后调用,注册对象 */
static void
on_bus_acquired(GDBusConnection *connection,
const gchar *name,
gpointer user_data)
{
GError *error = NULL;
GDBusNodeInfo *introspection_data = (GDBusNodeInfo *)user_data;
guint registration_id = g_dbus_connection_register_object(
connection,
"/com/example/GDBusService", /* 对象路径 */
introspection_data->interfaces[0], /* 接口信息 */
&interface_vtable, /* 虚表,包含方法回调 */
NULL, /* user_data */
NULL, /* 销毁回调 */
&error);
if (registration_id == 0)
{
g_printerr("注册对象失败: %s\n", error->message);
g_error_free(error);
}
}
/* 成功获取服务名时调用 */
static void
on_name_acquired(GDBusConnection *connection,
const gchar *name,
gpointer user_data)
{
g_print("成功获取服务名:%s\n", name);
}
/* 当服务名获取失败或丢失时调用 */
static void
on_name_lost(GDBusConnection *connection,
const gchar *name,
gpointer user_data)
{
g_print("服务名丢失:%s\n", name);
}
// SIGINT 信号处理回调
static gboolean handle_sigint(gpointer user_data)
{
printf("捕获到 SIGINT 信号,正在退出...\n");
if (user_data != NULL)
{
g_main_loop_quit((GMainLoop *)user_data);
}
return G_SOURCE_REMOVE; // 处理完后移除信号处理器
}
int main(int argc, char *argv[])
{
GMainLoop *loop = NULL;
GDBusNodeInfo *introspection_data;
guint owner_id;
GError *error = NULL;
// 设置本地化环境,不然 g_print 输出中文时乱码
setlocale(LC_ALL, "");
/* 4. 解析 introspection XML 数据 */
introspection_data = g_dbus_node_info_new_for_xml(introspection_xml, &error);
if (!introspection_data)
{
g_printerr("解析 introspection XML 失败: %s\n", error->message);
g_error_free(error);
return 1;
}
/* 5. 获取服务名,连接到 session bus */
owner_id = g_bus_own_name(G_BUS_TYPE_SESSION,
"com.example.GDBusService", /* 服务名 */
G_BUS_NAME_OWNER_FLAGS_NONE,
on_bus_acquired, /* 成功连接到总线后注册对象 */
on_name_acquired, /* 成功获取服务名 */
on_name_lost, /* 获取服务名失败或丢失 */
introspection_data, /* user_data:传递 introspection 信息 */
NULL);
/* 6. 进入主循环,等待客户端调用 */
loop = g_main_loop_new(NULL, FALSE);
g_unix_signal_add(SIGINT, handle_sigint, loop);
g_print("服务正在运行...\n");
g_main_loop_run(loop);
g_print("服务停止\n");
/* 程序退出时进行清理 */
g_bus_unown_name(owner_id);
g_dbus_node_info_unref(introspection_data);
g_main_loop_unref(loop);
return 0;
}
方式2:
- 编写 XML 接口描述文件
首先,创建一个 XML 文件(例如 example_interface.xml ),用来描述你的接口及其方法。示例内容如下,和方式1代码里的xml字符串内容一样 :
<node>
<interface name="com.example.GDBusExample">
<method name="Hello">
<arg name="greeting" type="s" direction="in"/>
<arg name="response" type="s" direction="out"/>
</method>
</interface>
</node>
这个 XML 文件定义了一个接口 com.example.GDBusExample
,其中有一个 Hello
方法,接收一个字符串参数并返回一个字符串。
- 利用 gdbus-codegen 自动生成代码
使用 GLib 自带的工具 gdbus-codegen
来自动生成与 XML 对应的 C 代码。在终端中执行如下命令:
gdbus-codegen --interface-prefix com.example. --generate-c-code example_interface example_interface.xml
执行后会生成两个文件:example_interface.h、example_interface.c
这两个文件中包含了接口定义、Skeleton(服务端骨架)及辅助函数。Skeleton 结构体提供了注册、导出和方法处理(通过信号)等功能。
最后利用生成代码实现 D‑Bus 服务 :
#include <stdio.h>
#include <locale.h>
#include "gio/gio.h"
#include "glib-unix.h"
#include "example_interface.h"
/* 方法处理函数:处理客户端调用的 Hello 方法 */
static gboolean
on_handle_hello(GDBusExample *skeleton,
GDBusMethodInvocation *invocation,
const gchar *greeting,
gpointer user_data)
{
g_print("收到客户端调用,参数:%s\n", greeting);
gchar *response = g_strdup_printf("Hello, %s!", greeting);
/* 调用自动生成的 complete 函数返回结果 */
gdbus_example_complete_hello(skeleton, invocation, response);
g_free(response);
return TRUE;
}
/* 当成功连接到总线时的回调 */
static void
on_bus_acquired(GDBusConnection *connection,
const gchar *name,
gpointer user_data)
{
GError *error = NULL;
/* 创建 Skeleton 实例 */
GDBusExample *skeleton = gdbus_example_skeleton_new();
/* 连接方法处理信号 */
g_signal_connect(skeleton, "handle-hello", G_CALLBACK(on_handle_hello), NULL);
/* 将 Skeleton 导出到 D‑Bus 对象路径上 */
if (!g_dbus_interface_skeleton_export(G_DBUS_INTERFACE_SKELETON(skeleton),
connection,
"/com/example/GDBusService",
&error))
{
g_printerr("导出 Skeleton 失败: %s\n", error->message);
g_error_free(error);
}
else
{
g_print("对象导出成功,对象路径:/com/example/GDBusService\n");
}
}
// SIGINT 信号处理回调
static gboolean handle_sigint(gpointer user_data)
{
printf("捕获到 SIGINT 信号,正在退出...\n");
if (user_data != NULL)
{
g_main_loop_quit((GMainLoop *)user_data);
}
return G_SOURCE_REMOVE; // 处理完后移除信号处理器
}
int main(int argc, char *argv[])
{
guint owner_id;
setlocale(LC_ALL, "");
/* 通过 g_bus_own_name 申请一个 well‑known 名称,并在成功后导出对象 */
owner_id = g_bus_own_name(G_BUS_TYPE_SESSION,
"com.example.GDBusService", /* 服务名称 */
G_BUS_NAME_OWNER_FLAGS_NONE,
on_bus_acquired, /* 总线获取成功后的回调 */
NULL, /* 名称获取成功回调 */
NULL, /* 名称丢失回调 */
NULL, /* user_data */
NULL);
/* 启动 GLib 主循环,等待客户端调用 */
GMainLoop *loop = g_main_loop_new(NULL, FALSE);
g_unix_signal_add(SIGINT, handle_sigint, loop);
g_print("服务正在运行...\n");
g_main_loop_run(loop);
g_print("服务停止\n");
/* 退出前清理 */
g_bus_unown_name(owner_id);
g_main_loop_unref(loop);
return 0;
}
然后可以使用命令行工具测试该服务,例如:
gdbus call --session --dest com.example.GDBusService --object-path /com/example/GDBusService --method com.example.GDBusExample.Hello "World"
调用后返回如下内容:
('Hello, World!',)
服务端开发基本流程就是这样了。
三、客户端开发
该示例既展示了同步调用,也展示了异步调用的实现:
#include <gio/gio.h>
#include <stdio.h>
#include <locale.h>
/* 异步调用回调函数,注意第一个参数类型为 GObject* */
static void
on_async_call_finished(GObject *source_object,
GAsyncResult *res,
gpointer user_data)
{
GError *error = NULL;
/* 将 GObject 转换为 GDBusConnection */
GDBusConnection *connection = G_DBUS_CONNECTION(source_object);
GVariant *result = g_dbus_connection_call_finish(connection, res, &error);
if (!result)
{
g_printerr("异步调用失败: %s\n", error->message);
g_error_free(error);
}
else
{
const gchar *response;
g_variant_get(result, "(&s)", &response);
g_print("异步调用响应: %s\n", response);
g_variant_unref(result);
}
/* 调用完成后退出主循环 */
GMainLoop *loop = (GMainLoop *)user_data;
g_main_loop_quit(loop);
}
int main(int argc, char *argv[])
{
GError *error = NULL;
GDBusConnection *connection;
GVariant *result;
const gchar *response;
setlocale(LC_ALL, "");
/* 1. 连接到 session bus */
connection = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error);
if (!connection)
{
g_printerr("连接 bus 失败: %s\n", error->message);
g_error_free(error);
return 1;
}
/* 2. 同步调用 Hello 方法 */
result = g_dbus_connection_call_sync(
connection,
"com.example.GDBusService", /* 目标服务名 */
"/com/example/GDBusService", /* 对象路径 */
"com.example.GDBusExample", /* 接口名称 */
"Hello", /* 方法名称 */
g_variant_new("(s)", "World (sync)"), /* 输入参数 */
G_VARIANT_TYPE("(s)"), /* 期望返回值类型 */
G_DBUS_CALL_FLAGS_NONE,
-1, /* 默认超时时间 */
NULL,
&error);
if (!result)
{
g_printerr("同步调用失败: %s\n", error->message);
g_error_free(error);
g_object_unref(connection);
return 1;
}
/* 3. 解析同步调用的返回结果 */
g_variant_get(result, "(&s)", &response);
g_print("同步调用响应: %s\n", response);
g_variant_unref(result);
/* 4. 异步调用 Hello 方法 */
/* 为异步调用启动一个主循环 */
GMainLoop *loop = g_main_loop_new(NULL, FALSE);
g_dbus_connection_call(
connection,
"com.example.GDBusService", /* 目标服务名 */
"/com/example/GDBusService", /* 对象路径 */
"com.example.GDBusExample", /* 接口名称 */
"Hello", /* 方法名称 */
g_variant_new("(s)", "World (async)"), /* 输入参数 */
G_VARIANT_TYPE("(s)"), /* 期望返回值类型 */
G_DBUS_CALL_FLAGS_NONE,
-1, /* 默认超时时间 */
NULL, /* 取消通知 */
on_async_call_finished, /* 回调函数 */
loop /* 传递主循环作为 user_data,用于退出 */
);
g_print("等待异步调用结果...\n");
g_main_loop_run(loop);
g_main_loop_unref(loop);
g_object_unref(connection);
return 0;
}
编译后,先运行前面的服务端程序,再运行客户端,打印如下:
同步调用响应: Hello, World (sync)!
等待异步调用结果...
异步调用响应: Hello, World (async)!
为什么这里和使用 gdbus call
命令返回的信息不一样呢,因为 gdbus call
总是返回 GVariant 元组,我们这里用 g_variant_get(result, "(&s)", &response);
直接提取字符串,不会显示元组()。
如果想要 在 C 代码中保留元组格式,可以使用 g_variant_print()
进行完整输出,如下所示:
gchar *str_repr = g_variant_print(result, TRUE);
printf("完整返回值: %s\n", str_repr);
g_free(str_repr);