C语言实现信号槽的项目实践
一.前言
前段时间使用qt做了个项目,了解到信号槽这种机制,在解耦合和程序设计的方便程度上体现出极度的优雅与从容.
现在回到单片机程序开发上来,感受到之前的单片机程序写的还不够优雅,使用函数指针能够实现解耦合,但需要设置回调函数,而且不同的模块设计的回调函数不尽相同,最大的痛点是实现异步调用非常复杂,与优雅二字基本是毫无关联了.
网上查阅了一遍,发现别人设计的信号槽要么只支持同步调用,要么实现过程复杂,代码量巨大,要么代码量巨大还只支持同步调用,要么居然还要自己去实现信号,信号不是给个函数声明就行了吗?还有的是闭源代码,好像是需要购买?
介于以上原因,本人打算自己实现信号槽机制,应用于单片机编程.实现的完整代码不到300行,支持同步异步混合调用,一对多多对一多对多等.当然,本文程序也有局限性,就是只支持单片机.因为只测试了单片机.
二.平台及运行环境
CPU | STM32F407 |
---|---|
内存 | 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脚本:
五.使用
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.执行结果
进入调试界面:
通过调试信息分析:
此时槽 slot_test 在异步线程中执行,并且参数传入正常
此时槽 slot_test 在同步线程中执行,并且参数传入正常
此时槽 slot_test2 在异步线程中执行,并且参数传入正常
六.总结
寥寥几行就实现了类于类之间的解耦合,异步调用实现更是简单,基于此模型的编程各模块只需要关注自己内部的业务逻辑,在合适的时候发出信号就行了,至于有没有模块需要,哪些模块需要,就不需要关心了.
到此这篇关于C语言实现信号槽的项目实践的文章就介绍到这了,更多相关C语言 信号槽内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
C++调用C函数报错无法解析的外部命令/无法解析的外部符号问题
这篇文章主要介绍了C++调用C函数报错无法解析的外部命令/无法解析的外部符号问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教2023-08-08
最新评论