用C模拟QT的信号槽机制

探索用C模拟QT的信号槽机制

前言

初学QT的时候,一直觉得信号槽机制很好用,在写固件的时候总想尝试自己写一下,恰好最近比较空,因此乘此机会探索一下。

设计思路

我的想法很简单,信号相当于一个要执行的函数链表,定义信号的时候就是在定义链表头,定义槽的时候就是在定义一个链表节点,当我用connect连接槽时,就是在将我的槽函数注册到信号对应的链表上,当要发射信号的时候就是在遍历链表,将已注册的槽函数全部执行一遍,思路虽然简单,但还是有一些东西需要思考一下,例如如何创建不同参数类型的信号和槽。最后我使用了5个宏来实现这些操作。这里我还是直接上代码吧。

/**
 * @file signal_slot.h
 * @author salalei
 * @brief 用C模拟QT的信号槽机制
 * @version V1.0.0
 * @date 2021-08-25
 * 
 * @copyright Copyright (c) 2021
 * 
 */
#ifndef __SIGNAL_SLOT_H__
#define __SIGNAL_SLOT_H__

#ifdef __cplusplus
extern "C" {
#endif

#include <stdint.h>

/**
 * @brief 槽函数列表
 */
struct slot_list_node
{
    struct slot_list_node *prev;
    struct slot_list_node *next;
    void *data;
};

/**
 * @brief 定义一个信号
 * 
 * @param name 该信号的名称
 * @param ... 这个信号所携带的参数的类型
 */
#define SIGNAL(name, ...)                                   \
    typedef void (*name##_signal_t)(void *, ##__VA_ARGS__); \
    struct slot_list_node name##_signal_head = {            \
        .prev = &name##_signal_head,                        \
        .next = &name##_signal_head,                        \
        .data = NULL,                                       \
    }

/**
 * @brief 发射一个信号
 * 
 * @param name 需要发射的信号名称
 * @param priv 指向用户需要发送的数据的指针
 * @param ... 该信号所携带的参数
 */
#define EMIT(name, priv, ...)                                                                \
    do                                                                                       \
    {                                                                                        \
        name##_signal_t slot;                                                                \
        struct slot_list_node *node;                                                         \
        for (node = name##_signal_head.next; node != &name##_signal_head; node = node->next) \
        {                                                                                    \
            slot = (name##_signal_t)node->data;                                              \
            slot((priv), ##__VA_ARGS__);                                                     \
        }                                                                                    \
    } while (0)

/**
 * @brief 定义一个槽
 * 
 * @param name 该槽函数的名称
 * ... 这个槽所携带的参数的类型
 */
#define SLOT(name, ...)                        \
    void name(void *, ##__VA_ARGS__);          \
    struct slot_list_node name##_slot_node = { \
        .prev = &name##_slot_node,             \
        .next = &name##_slot_node,             \
        .data = name,                          \
    }

/**
 * @brief 将指定信号与指定槽连接
 * 
 * @param signal_name 需要连接的信号名称(该信号必须在连接前用SIGNAL去申明)
 * @param slot_name 需要连接的槽名称(该信号必须在连接前用SLOT去申明)
 */
#define CONNECT(signal_name, slot_name)                                \
    do                                                                 \
    {                                                                  \
        extern struct slot_list_node signal_name##_signal_head;        \
        slot_name##_slot_node.next = &signal_name##_signal_head;       \
        slot_name##_slot_node.prev = signal_name##_signal_head.prev;   \
        signal_name##_signal_head.prev->next = &slot_name##_slot_node; \
        signal_name##_signal_head.prev = &slot_name##_slot_node;       \
    } while (0)

/**
 * @brief 将指定信号与指定槽断开
 * 
 * @param signal_name 需要连接的信号名称(该信号必须在连接前用SIGNAL去申明)
 * @param slot_name 需要连接的槽名称(该信号必须在连接前用SLOT去申明)
 */
#define DISCONNECT(signal_name, slot_name)                             \
    do                                                                 \
    {                                                                  \
        slot_name##_slot_node.prev->next = slot_name##_slot_node.next; \
        slot_name##_slot_node.next->prev = slot_name##_slot_node.prev; \
        slot_name##_slot_node.prev = &slot_name##_slot_node;           \
        slot_name##_slot_node.next = &slot_name##_slot_node;           \
    } while (0)

#ifdef __cplusplus
}
#endif

#endif

代码量比我预期中的少很多。这里我为了能够定义携带不同参数的信号和槽,我将函数的名称和参数拆开分别输入,而不是以一个函数整体,这样的话就能够使用__VA_ARGS__这个预定义宏来实现不同参数的信号槽申明。然后在CONNECT部分我采用尾插法,让先连接的槽函数先被遍历执行。以下为用模拟的信号槽写的一个demo。

#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include "signal_slot.h"

/**
 * @brief 定义2个信号
 */
SIGNAL(test1_signal, bool);
SIGNAL(test2_signal, uint8_t *, size_t);

/**
 * @brief 定义3个槽
 */
SLOT(test1_slot, bool);
SLOT(test2_slot, bool);
SLOT(test3_slot, uint8_t *, size_t);

/**
 * @brief 槽函数1
 * 
 * @param priv 信号传入的私有数据
 * @param state 
 */
void test1_slot(void *priv, bool state)
{
    printf("slot1 recv state:%s\n", state ? "true" : "false");
}

/**
 * @brief 槽函数2
 * 
 * @param priv 信号传入的私有数据
 * @param state 
 */
void test2_slot(void *priv, bool state)
{
    printf("slot2 recv state:%s\n", state ? "true" : "false");
}

/**
 * @brief 槽函数2
 * 
 * @param priv 信号传入的私有数据
 * @param state 
 */
void test3_slot(void *priv, uint8_t *buf, size_t size)
{
    while (size--)
    {
        putchar(*buf++);
    }
}

int main(int argc, char **argv)
{
    //将槽1、2连接到信号1,槽3连接到信号2
    CONNECT(test1_signal, test1_slot);
    CONNECT(test1_signal, test2_slot);
    CONNECT(test2_signal, test3_slot);

    //发射信号1
    EMIT(test1_signal, NULL, 0);
    //发射信号2
    EMIT(test2_signal, NULL, "Signal slot\n", 12);

    return 0;
}

以下是运行结果
在这里插入图片描述

总结

说一下优点:不需要动态内存;可申明不同长度不同类型参数的槽和信号;不需要外部申明只要知道对应的信号名和槽名,即可实现模块间的通信(因为链头的外部申明放在了CONNECT的宏中)。
缺点也很明显:只能实现一个信号连接多个槽,不能实现一个槽连接多个信号;__VA_ARGS__这个预定义宏是C99才有的东西,因此不能在用在C99以下的标准中;理论上大家各自写的模块的头文件应该申明这个模块拥有的信号和槽,方便其他人使用,但现在还缺少申明信号和槽的宏,目前只能在模块头文件写注释来告知别人,比较蠢。

后续在对他进行优化,现在已经足够我使用了。

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Qt信号机制作为Qt框架的重要特性,具有很多优点,如松耦合、类型安全、跨线程通信等。然而,即便如此,它也存在一些缺点。 首先,信号机制的语法相对复杂,需要开发者熟悉和理解一定的概念和规则。虽然Qt提供了一些工具和文档来帮助开发者学习和使用信号机制,但对刚接触Qt的开发者来说,仍然需要花费一定的时间和精力去理解它。 其次,信号机制的性能相对较低。每个信号之间的连接都需要一定的开销,如果使用过度,可能导致程序变慢。尤其是在高频繁触发的场景下,如实时图形界面更新,信号机制可能成为瓶颈,需要额外的优化和处理。 另外,信号机制对于多线程的支持不够友好。Qt提供了一些机制来处理多线程下的信号通信,如Qt::QueuedConnection和Qt::BlockingQueuedConnection,但开发者需要小心地处理线程间的同步和互斥问题,以避免潜在的死锁和竞态条件。 此外,信号机制在特定情况下可能导致代码的可维护性降低。当一个信号可以连接到多个时,开发者需要追踪和管理这些连接,以及处理可能的循环依赖和内存泄漏问题。这要求开发者对信号的连接和断开有很好的理解,并编写清晰可读的代码。 综上所述,虽然Qt信号机制是一个强大而灵活的工具,但在使用时需要注意以上一些缺点,以确保代码的性能和可维护性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值