探索用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以下的标准中;理论上大家各自写的模块的头文件应该申明这个模块拥有的信号和槽,方便其他人使用,但现在还缺少申明信号和槽的宏,目前只能在模块头文件写注释来告知别人,比较蠢。
后续在对他进行优化,现在已经足够我使用了。