C语言实现信号槽的项目实践

 更新时间:2024年04月02日 10:20:37   作者:明月清风旧  
信号槽是观察者模式的一种实现,一个信号就是一个能够被观察的事件,本文主要介绍了C语言实现信号槽的项目实践模具有一定的参考价值,感兴趣的可以了解一下

一.前言

前段时间使用qt做了个项目,了解到信号槽这种机制,在解耦合和程序设计的方便程度上体现出极度的优雅与从容.

现在回到单片机程序开发上来,感受到之前的单片机程序写的还不够优雅,使用函数指针能够实现解耦合,但需要设置回调函数,而且不同的模块设计的回调函数不尽相同,最大的痛点是实现异步调用非常复杂,与优雅二字基本是毫无关联了.

网上查阅了一遍,发现别人设计的信号槽要么只支持同步调用,要么实现过程复杂,代码量巨大,要么代码量巨大还只支持同步调用,要么居然还要自己去实现信号,信号不是给个函数声明就行了吗?还有的是闭源代码,好像是需要购买?

介于以上原因,本人打算自己实现信号槽机制,应用于单片机编程.实现的完整代码不到300行,支持同步异步混合调用,一对多多对一多对多等.当然,本文程序也有局限性,就是只支持单片机.因为只测试了单片机.

二.平台及运行环境

CPUSTM32F407
内存128K+64K
操作系统rt-thread
编译器keil
moc脚本python

对单片机的配置还是有一定的要求,内存太小的话能支持的信号和槽的数量就会有限制,使用体验上就会捉襟见肘;由于信号槽调用比回调函数速度慢,在信号槽数量多的情况下如果CPU算力不够可能会影响业务逻辑.本文代码不会限制信号和槽的数量,只要内存够.
由于异步调用需要操作系统支持,本文程序基于rt-thread,如需支持其他操作系统,需要做相应修改

三.代码实现

1.定义相关数据类型

在 signal.h 文件中声明数据类型如下:

// 声明信号和发送宏
#define signal void
#define emit

// 槽列表
typedef struct _slot_list{
  struct _slot_list *next;// 下一个槽
  void *fun;// 槽函数
  void *mb;// 槽函数运行的线程
  void *obj;// 槽函数所在类
}slot_list;

// 信号
typedef struct{
  const char *name;// 信号名
  const char *file;// 信号所在文件
  void *signal_;// 信号
  slot_list *head;// 槽列表
}signal_def;

// 消息
typedef struct{
  void *fun;// 要执行的函数
  void *src;// 消息源
  int param_num;// 参数个数
  uint32_t param[0];// 参数列表
}slot_msg_def;

// 异步调用
typedef struct{
  void *mb;
  int run;
}slot_run_def;

// 线程
typedef void * sig_thread;

2.信号定义相关

为了使系统知道程序中有哪些信号,还需要在信号的定义上下功夫,既要使程序能正常执行,又需要使用户在编程使又只需要声明信号,而不需要去实现.本文程序仿照qt中的做法,使用moc脚本来自动生成信号的实现.
在 signal.h 中声明宏:

// 这个宏用于声明信号,声明了之后系统就能找到这个信号
// 这个宏用户编程时不需要使用
#define signal_export(name_) \
  const static char __sig_##name_##_name[] SECTION(".rodata.sigstr") = #name_; \
  const static char __sig_##name_##_file[] SECTION(".rodata.sigstr") = __FILE__; \
  RT_USED static signal_def _signal_##name_ SECTION("signalstruct")= \
  {\
    .name=__sig_##name_##_name,\
    .file=__sig_##name_##_file,\
    .signal_=name_,\
    .head=0,\
  };

信号声明示例:
用户编程时只需要在需要使用信号的头文件中,使用 signal 关键字声明信号原型即可,moc脚本会自动生成信号的实现,保证c语言程序结构正常.如下:

#include "signal.h"

signal pmcu_test_signal(int *a,int b);

信号实现示例:
在此展示一下moc脚本生成的信号实现长什么样,这部分用户使用过程中无需关心:

#include "stdlib.h"
#include "signal.h"

void pmcu_test_signal(int *a,int b)
{
  uint32_t *param=malloc(sizeof(uint32_t)*2);
  param[0]=(uint32_t)a;
  param[1]=(uint32_t)b;
  _signal_emit(pmcu_test_signal,param,2);
  free(param);
}
signal_export(pmcu_test_signal);

3.相关函数实现

signal.c 文件实现如下:
其中 param_check(s) 是断言 参数s是否为0的宏

#include "signal.h"
#include "board.h"
#include "stdlib.h"
#include "stdio.h"

typedef struct{
  void *mutex;
}self_def;

static self_def g_self;

// 在系统启动时初始化信号量系统
int signal_init(void)
{
  self_def *s=&g_self;
  if(s->mutex==0)
  {
    s->mutex=rt_mutex_create("signal_",RT_IPC_FLAG_FIFO);
  }
  return 0;
}

// 4字节拷贝
static void cpy4byte(uint32_t *dst,uint32_t *src,int num_4byte)
{
  for(int i=0;i<num_4byte;i++)
  {
    dst[i]=src[i];
  }
}

// 调用这个宏执行槽函数
#define SLOT_FUN_RUN(fun,param) \
  ((slot_fun_def)(fun))(param[0],param[1],param[2],\
    param[3],param[4],param[5],param[6],param[7])

// 槽函数定义
typedef void (*slot_fun_def)(uint32_t a,uint32_t b,uint32_t c,uint32_t d,uint32_t e,uint32_t f,uint32_t g,uint32_t h);

// 异步调用线程
static void slot_run(void *t)
{
  param_check(t);
  slot_run_def *s=t;
  int msg_size=sizeof(slot_msg_def)+sizeof(uint32_t)*8;
  slot_msg_def *msg=malloc(msg_size);
  while(s->run)
  {
    rt_mq_recv(s->mb,msg,msg_size,RT_WAITING_FOREVER);
    SLOT_FUN_RUN(msg->fun,msg->param);
  }
  free(msg);
}

// 创建一个线程
sig_thread thread_creat(int pro)
{
  static int count=0;
  char name[20]={0};
  slot_run_def *run=malloc(sizeof(slot_run_def));
  run->run=1;
  sprintf(name,"sig_mq#%d",count);
  run->mb=rt_mq_create(name,(sizeof(slot_msg_def)+sizeof(uint32_t)*8),20,RT_IPC_FLAG_FIFO);
  sprintf(name,"sig_t#%d",count);
  rt_thread_t rt_t=rt_thread_create(name,slot_run,run,2048,pro,20);
  rt_thread_startup(rt_t);
  count++;
  return run->mb;
}

void thread_delete(sig_thread t)
{
  // 删除线程需要删除与此线程相关的所有信号槽
  // 删除消息队列
  param_check(0);
}

// 连接信号和槽参数分别为:信号,槽线程,槽所在类,槽函数
int connect(void *signal_,sig_thread t,void *slot_obj,void *slot)
{
  self_def *s=&g_self;
  signal_def *sig;
  sig=signal_find(signal_);
  if(sig==0) return -1;
  rt_mutex_take(s->mutex,RT_WAITING_FOREVER);
  slot_list *slo=malloc(sizeof(slot_list));
  param_check(slo);
  slo->fun=slot;
  slo->mb=t;
  slo->next=0;
  slo->obj=slot_obj;
  if(sig->head==0)
  {
    sig->head=slo;
  }
  else{
    slot_list *next=sig->head;
    while(next->next!=0)
    {
      next=next->next;
    }
    next->next=slo;
  }
  rt_mutex_release(s->mutex);
  return 0;
}

// 信号和槽断开连接,参数同上
int disconnect(void *signal_,sig_thread t,void *slot_obj,void *slot)
{
  self_def *s=&g_self;
  signal_def *sig;
  sig=signal_find(signal_);
  if(sig==0) return -1;
  rt_mutex_take(s->mutex,RT_WAITING_FOREVER);
  slot_list *next=sig->head;
  slot_list *prev=0;
  while(next!=0)
  {
    if(next->fun==slot&&next->mb==t&&next->obj==slot_obj)
    {
      if(prev) prev->next=next->next;
      else sig->head=next->next;
      free(next);
    }
    prev=next;
    next=next->next;
  }
  rt_mutex_release(s->mutex);
  return -1;
}

// 查找信号结构体
signal_def *signal_find(void *signal_)
{
  extern const int signalstruct$$Base;
  extern const int signalstruct$$Limit;
  signal_def *start=(signal_def *)&signalstruct$$Base;
  signal_def *end=(signal_def *)&signalstruct$$Limit;
  for(signal_def *t=start;t<end;t++)
  {
    if(t->signal_==signal_)
    {
      return t;
    }
  }
  return 0;
}

// 发送信号,这个函数用户无需关心
// 用户编程发送信号使用 emit signal_fun(); 语法即可
int _signal_emit(void *signal_,uint32_t *param,int param_num)
{
  self_def *s=&g_self;
  signal_def *sig=signal_find(signal_);
  if(sig==0) return -1;
  if(param_num>7) return -2;
  int size=sizeof(slot_msg_def)+sizeof(uint32_t)*(8);
  slot_msg_def *m=malloc(size);
  rt_mutex_take(s->mutex,RT_WAITING_FOREVER);
  slot_list *h=sig->head;
  m->param_num=param_num;
  m->src=signal_;
  while(h)
  {
    m->fun=h->fun;
    if(h->obj)
    {
      cpy4byte(m->param+1,param,param_num);
      m->param[0]=(uint32_t)h->obj;
    }else{
      cpy4byte(m->param,param,param_num);
    }
    if(h->mb){
      rt_mq_send(h->mb,m,size);
    }else{
      SLOT_FUN_RUN(m->fun,m->param);
    }
    h=h->next;
  }
  rt_mutex_release(s->mutex);
  free(m);
  return 0;
}

signal.h 文件中添加函数声明:

sig_thread thread_creat(int pro);
void thread_delete(sig_thread t);
signal_def *signal_find(void *signal_);
int connect(void *signal_,sig_thread t,void *slot_obj,void *slot);
int disconnect(void *signal_,sig_thread t,void *slot_obj,void *slot);
int signal_init(void);

// 这个函数用户无需关心
int _signal_emit(void *signal_,uint32_t *param,int param_num);

四.moc脚本实现

moc脚本的工作就是定义信号函数,在每次编译之前必须调用moc脚本,否则编译报错.完整代码如下:

import os

MOD_FILE="mod_signals.c"
MOD_PATH="./source/task/"

def find_file(path:str,fix:str):
    file_list=[]
    for file_name in os.listdir(path):
        if file_name.endswith(fix):
            file_list.append(os.path.join(path, file_name))
    return file_list

def find_signal_def(file):
    list_signal=[]
    with open(file) as f:
        list_str=f.readlines()
        for i in list_str:
            if(i[0:6]=="signal"):
                list_signal.append(i)
    return list_signal

# 截取变量名
def split_par_name(par_str:str):
    ret_str=""
    if(par_str.count('*')>0):
        ret_str=par_str.split("*")[1].strip()
    else:
        ret_str=par_str.split(" ")[1]
    return ret_str

def def_signal_fun(line:str):
    # 删除多余空格
    line=' '.join(line.split())
    # print(line)
    list_str=line.split('(')
    fun_name=list_str[0].split(' ')[1]
    param_str=list_str[1].split(')')[0]
    params=[]
    # 有","则至少有两个参数否则可能有一个参数,可能没有
    if(param_str.count(',')>0):
        params=param_str.split(',')
        for i in range(len(params)):
            params[i]=params[i].strip()
    else:
        t_str=param_str.strip()
        if(len(t_str)>0)and(t_str!="void"):
            params.append(t_str)
    # print(fun_name,params)
    params_num=len(params)
    fun_str=""
    fun_str+="void "+fun_name+"("
    for i in range(params_num):
        fun_str+=params[i]
        if(i<len(params)-1):
            fun_str+=","
    fun_str+=")\n{\n"
    fun_str+="  uint32_t *param=malloc(sizeof(uint32_t)*{d});\n".format(d=params_num)
    for i in range(params_num):
        fun_str+="  param[{d1}]=(uint32_t){d2};\n".format(d1=i,d2=split_par_name(params[i]))
    fun_str+="  _signal_emit({d1},param,{d2});\n".format(d1=fun_name,d2=params_num)
    fun_str+="  free(param);\n}\n"
    fun_str+="signal_export({d});\r\n\r\n\r\n\r\n".format(d=fun_name)
    # print(fun_str)
    return fun_str

def ergodic_signal_fun(path):
    fun_str=""
    list_file=find_file(path,".h")
    for i in list_file:
        list_signal=find_signal_def(i)
        for j in list_signal:
            fun_str+=def_signal_fun(j)
    return fun_str

def mod_fine_creat(file_path):
    with open(file_path,"w+") as f:
        f.write("#include \"stdlib.h\"\n")
        f.write("#include \"signal.h\"\r\n\r\n\r\n\r\n")
        f.write(ergodic_signal_fun(MOD_PATH))

mod_fine_creat(MOD_PATH+MOD_FILE)
print("mod_file creat success.\n")

使用moc脚本:
keil中做如下设置,可在每次编译前自动调用moc脚本:

自动调用moc脚本

五.使用

1.测试程序编写

编写 main.c 如下:

// 使用信号槽功能需要包含此文件
#include "signal.h"
// 此文件声明了pmcu_test_signal 信号,如前文所述
#include "prot_mcu.h"

// 定义槽函数
void slot_test(int *a,int b)
{
  int int_array[20]={0};
  int_array[0]=a[0];
  int_array[1]=a[1];
  int_array[2]=a[2];
  int_array[3]=a[3];
  int_array[4]=a[4];
  int_array[5]=b;
}
// 定义槽函数,假设a是函数所在类
void slot_test2(int *a,int *b,int c)
{
  int int_array[20]={0};
  int_array[0]=a[0];
  int_array[1]=a[1];
  int_array[2]=a[2];
  int_array[3]=a[3];
  int_array[4]=a[4];
  int_array[5]=c;
}

int main()
{
  // 定义数据,异步调用时需保证数据在函数执行时还有效
  int int_array[20]={4,3,5,2,1};
  // 创建异步调用线程,如不需要异步调用,则无需创建线程
  sig_thread t=thread_creat(20);
  // 连接信号和槽,异步调用,这个槽将在线程t中运行
  connect(pmcu_test_signal,t,0,slot_test);
  // 同步调用,这个槽将在调用信号的线程中运行
  connect(pmcu_test_signal,0,0,slot_test);
  // 异步调用,假设int_array是槽函数所在类
  connect(pmcu_test_signal,t,int_array,slot_test2);
  while(1)
  {
    rt_thread_mdelay(1000);
    // 发送信号,emit不是必须的,但出于可读性要求,还是加上比较好
    emit pmcu_test_signal(int_array,8);
  }
  
}

程序中 pmcu_test_signal 信号连接了3个槽,因此发送信号时,每个槽都会执行一遍,其中没有所属类的槽函数 slot_test 会执行两遍,一次在main函数线程,一次在异步线程; slot_test2函数有所属类,其第一个参数就是所属类的指针,执行在异步线程.

2.执行结果

进入调试界面:

调试1

通过调试信息分析:
此时槽 slot_test 在异步线程中执行,并且参数传入正常

调试2

此时槽 slot_test 在同步线程中执行,并且参数传入正常

调试3

此时槽 slot_test2 在异步线程中执行,并且参数传入正常

六.总结

寥寥几行就实现了类于类之间的解耦合,异步调用实现更是简单,基于此模型的编程各模块只需要关注自己内部的业务逻辑,在合适的时候发出信号就行了,至于有没有模块需要,哪些模块需要,就不需要关心了.

到此这篇关于C语言实现信号槽的项目实践的文章就介绍到这了,更多相关C语言 信号槽内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • VS中动态库的创建和调用方式详解

    VS中动态库的创建和调用方式详解

    库的存在形式本质上来说库是一种可执行代码的二进制,​ 静态库和动态库的区别主要是在链接阶段处理库的方式不同而区分的,本文介绍VS中动态库的创建和调用方式,感兴趣的朋友一起看看吧
    2024-01-01
  • C语言结构体简单入门讲解

    C语言结构体简单入门讲解

    这篇文章主要介绍了C语言结构体简单入门讲解,本文讲述了结构体的基本定义和操作,讲解了几个比较实用的函数和案例,希望对你有所帮助
    2021-06-06
  • C++基于回溯法解决八皇后问题示例

    C++基于回溯法解决八皇后问题示例

    这篇文章主要介绍了C++基于回溯法解决八皇后问题,简单描述了八皇后问题,以及回溯法的原理与解决八皇后问题的相关操作技巧,需要的朋友可以参考下
    2017-11-11
  • C++关于类结构体大小和构造顺序,析构顺序的测试详解

    C++关于类结构体大小和构造顺序,析构顺序的测试详解

    这篇文章主要介绍了C++类结构体大小和构造顺序,析构顺序的测试,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-08-08
  • C++四种强制转换原理与价值

    C++四种强制转换原理与价值

    这篇文章主要介绍了C++的四种强制转换原理与价值,文中介绍的非常详细,对学习C语言有一定的参考价值,感兴趣的小伙伴可以参考一下
    2023-04-04
  • C++11 移动构造函数的使用

    C++11 移动构造函数的使用

    本文主要介绍了C++11 移动构造函数的使用,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-01-01
  • C语言实现文件版通讯录的代码分享

    C语言实现文件版通讯录的代码分享

    这篇文章主要为大家详细介绍了如何利用C语言实现一个文件版通讯录,主要运用了结构体,一维数组,函数,分支与循环语句等等知识,需要的可以参考一下
    2023-01-01
  • c++中map容器的使用详解

    c++中map容器的使用详解

    这篇文章主要介绍了c++中map容器的使用详解,C++中map容器提供一个键值对容器,map与multimap差别仅仅在于multiple允许一个键对应多个值,需要的朋友可以参考下
    2023-08-08
  • C语言SetConsoleTextAttribute函数使用方法

    C语言SetConsoleTextAttribute函数使用方法

    这篇文章介绍了C语言SetConsoleTextAttribute函数的使用方法,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-12-12
  • C++静态变量,常量的存储位置你真的了解吗

    C++静态变量,常量的存储位置你真的了解吗

    这篇文章主要介绍了C++中静态变量与常量的存储位置的相关资料,需要的朋友可以参考下,希望能够给你带来帮助
    2021-08-08

最新评论