GDBus入门教程

发布于:2025-03-08 ⋅ 阅读:(206) ⋅ 点赞:(0)

目录

一、基本概念

二、服务端开发

三、客户端开发


一、基本概念

  • 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);

网站公告

今日签到

点亮在社区的每一天
去签到