论坛首页 编程语言技术论坛

在ANSI C下设计和实现简便通用signal-slot机制——一种平台相关但易于移植的,lambda表达式风格的,经由抵抗编译器而得的方案

浏览 12415 次
精华帖 (15) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2009-11-10  
先把最近更新的代码发上来,对gcc平台做了详尽的测试,基本上是可以用在项目中了。对未知编译器(纯ANSI C)的支持因为还没有来得及详细测试,尚未包含。

主要更新有:
增加了对c99变长数组和alloca()的支持,在有两者其一的支持下,用户不需要指定栈的大小(大多数平台上都有alloca函数);
缺省做自动栈复制或值提交功能,用户一般不再需要使用SLOT_REUSE_LOCAL_VAR(...),SLOT_COMMIT_LOCAL_VAR()等宏来显式复制变量或值;
用户可以通过一些宏来配置上述选项,见config.h;
其他一些修正,代码优化,更多安全性检查,通用性增强等方便使用。

示例代码:

#include <stdio.h>
#include "sigslot.h"

void connect(struct __Signal *signal)
{
int i = 5;
float j = 10.0;

SLOT *slot = (SLOT *)malloc(sizeof(SLOT));
SLOT_FETCH_LOCAL_VAR3(slot, slot, i, j);
SIGNAL_CONNECT(signal, slot
                , (
                   printf("int=%d, float=%f\n", i, j);
                   )
                , (
                   SLOT_FREE(slot);
                   free(slot);
                   )
                );
}

int main(int argc, char *argv[])
{
SIGNAL2(int, float) signal;
SLOT slot;

SLOT_INIT(&slot);
SIGNAL_INIT(&signal);

connect((struct __Signal *)&signal);

SIGNAL_CONNECT(&signal, &slot
                , (
                   printf("int=%d, float=%f\n", signal._1, signal._2);
                   )
                , ()
                );

SIGNAL2_EMIT(&signal, 5, 10);
SIGNAL2_EMIT(&signal, 50, 100);

SIGNAL_DISCONNECT(&signal, &slot);
SIGNAL_FREE(&signal);

return 0;
}

对于SLOT_REUSE_LOCAL_VAR,SLOT_LOCAL_VAR,SLOT_UPDATE_LOCAL_VAR等宏的命名上我觉得有些歧义,如果用HOST_VAR代替LOCAL_VAR会不会好些,望各位指点一二,先就此谢过!
0 请登录后投票
   发表时间:2009-11-18  
在文章结束前,发布最后一个测试候选版,增加了未知编译器/纯c代码的支持和实现。

改进包括:
增强对反向栈帧的支持。就是只使用一个栈指针或者栈基指针位于栈顶,变量位于底部的情形。这个时候一般无法预测栈帧的大小(除非变量严格有序,并且按栈帧方向增长)。但一个好的副作用是及时没有合法的c99/alloca支持也可以动态分配栈帧(递归调用),所以可以运行时在每个宿主函数内部指定栈帧大小。一个宏SLOT_STACK_FRAME_REVERSE指定这个情况。(ARM以及MSVC Release版本符合这个情况)

兼容性的改善,目前明确测试过的平台有ARM ADS,GCC 3.x/4.x,MSVC 2008;相信通过配置宏的组合可以支持到更多的平台。纯c的实现也同时在三个平台上都可以工作,定义宏SLOT_USE_LONGJMP强制使用C回调。

其他改进,一般情况自动栈复制都可以工作,增加了注释和说明。

作为最后一个测试候选版,目前slot仍然有用户指定,所以接口还保持和过去一样。但下一个版本将交由signal统一管理,从堆上自动分配和释放,用户将一般不显式跟slot打交道,这样一致性更好些,使用负担也轻了;同时增加一个initialization表达式作为connect参数,也就是:

[slot = ]SIGNAL_CONNECT(signal,
                                (initialization ...),
                                (signal processing...),
                                (finalization...)
                               );

也欢迎大家提出意见和建议。

附件是完整版本的库和测试文件,包括boost/proprocessor头文件,编译时指定相应(当前)目录为头文件搜索路径,例如gcc -I. ...
0 请登录后投票
   发表时间:2009-11-22  
上次的代码中有个bug,竟然隐藏在assert断言里,这个版本不再继续,我提交一个patch吧

diff --git a/sigslot/sigslot.h b/sigslot/sigslot.h
index 335046b..4ca72f6 100755
--- a/sigslot/sigslot.h
+++ b/sigslot/sigslot.h
@@ -136,8 +136,8 @@ struct __Slot {
     __SLOT_PTR->signaling = 1; \
   } else if (__SLOT_PTR->stack_frame_head == 0) { \
     __SLOT_PTR->invoke = &__slot_init_invoke_stub; \
-    if (setjmp(__slot_ptr->signal->environment) == 0) { \
-      __sigslot_invoke(__slot_ptr); \
+    if (setjmp(__SLOT_PTR->signal->environment) == 0) { \
+      __sigslot_invoke(__SLOT_PTR); \
     } \
   } else { \
     volatile size_t anti_optimize = (size_t)&__SLOT_STACK_OFFSET; \
@@ -182,8 +182,8 @@ struct __Slot {
#define __SIGNAL_INIT_SLOT() \
   if (__SLOT_PTR->stack_frame_head == 0) { \
     __SLOT_PTR->invoke = &__slot_init_invoke_stub; \
-    if (setjmp(__slot_ptr->signal->environment) == 0) { \
-      __sigslot_invoke(__slot_ptr); \
+    if (setjmp(__SLOT_PTR->signal->environment) == 0) { \
+      __sigslot_invoke(__SLOT_PTR); \
     } \
   } else { \
     volatile size_t anti_optimize = (size_t)&__SLOT_STACK_OFFSET; \
@@ -398,7 +398,7 @@ struct __Slot {
     unsigned int i; \
     for (i = 0; i < __SLOT_PTR->argc; i++) { \
       assert((size_t)__SLOT_PTR->argv[i].addr <= (size_t)__SLOT_PTR->stack_frame_head - __SLOT_PTR->argv[i].size \
-      && (size_t)&__SLOT_PTR->argv[i].addr >= (size_t)__SLOT_PTR->stack_frame_tail); \
+      && (size_t)__SLOT_PTR->argv[i].addr >= (size_t)__SLOT_PTR->stack_frame_tail); \
     } \
   } \
     __slot_commit_args(__SLOT_PTR, __SLOT_STACK_OFFSET)
0 请登录后投票
   发表时间:2009-11-22  
上次留了个包袱,很长时间过去了,想必大家都不耐烦了。前面得到的反馈不多,我就当作都在等最后的结果。不说废话,先直接贴出代码(前面已经给出了完整的代码,这里是示意的,虽然不一定工作,但更加直接明了一些):

首先给slot类型增加一个jmp_buf字段和一个名字叫jmpcode的整型数组:

struct __Slot {
  unsigned int argc;
  struct __SlotArg *argv;
  int signaling;
  void *func_addr;
  void *stack_addr;
  void *stack_frame_head;
  void *stack_frame_tail;
  void (*signal_slot_invoke)(struct __Slot *);
  struct __Signal *signal;
  struct __Slot *next;
  struct __Slot *prev;
  jmp_buf environment;
  int jmpcode[sizeof(jmp_buf) / sizeof(void *)];
};

然后,主体代码不用动,只需要改变调用和slot头部的代码,分别是__SIGNAL_SLOT_INVOKE和__SIGNAL_SLOT_ENTRY,他们是对应的。之前当我们在不同平台上移植时,也只要处理好调用和入口就可以了。

#define __SIGNAL_SLOT_ENTRY(slot_ptr) \
  { \
    jmp_buf environment; \
    setjmp(environment); \
    int ret = setjmp(slot_ptr->environment; \
    if (ret == 0) { \
      unsigned int i, j = 0; \
      for (i = 0; i < sizeof(jmp_buf) / sizeof(void *); i++) { \
        if (((void **)&environment)[i] != ((void **)&slot_ptr->environment)[i]) { \
          slot->jmpcode[j++] = i; \
        } \
      } \
      slot_ptr->jmpcode[j] = -1; \
    } else { \
      slot_ptr = (struct __Slot *)ret; \
    } \
  }
  
# define __SIGNAL_SLOT_INVOKE(slot_ptr) \
  { \
    jmp_buf environment; \
    if (setjmp(environment) == 0) { \
      unsigned int i; \
      for (i = 0; slot_ptr->jmpcode[i] >= 0; i++) { \
        ((void **)&environment)[slot_ptr->jmpcode[i]] = ((void **)&slot_ptr->environment)[slot_ptr->jmpcode[i]]; \
      } \
      longjmp(environment); \
    } \
  }

对整个过程稍有了解的人看了代码一定就立刻明白了,我们抛开实现的细节看看如果一般性面对必须要用C来解决这个问题的话会怎么考虑。

这里剩下没有解决的问题就是跳转地址的问题,c本身没有提供直接的方法得到它,但我们也还可以有一些间接的办法,比如内存扫描。通过一些特征化的代码,我们可以从函数处扫描或许(至少概率上)能够得到大致范围的地址;但这个方法仍然有局限,一是代码的位置不一定是顺序排列的,二是我们无法区分多个slot的情形——代码生成完全依赖于编译器,无法安置其他信息。

这个时候一旦回到最初始的setjmp/longjmp,我们立即就可以想到它们处理的数据中肯定包含有类似于跳转地址这样的信息。于是自然就会得到上述的代码。

在__SIGNAL_SLOT_ENTRY初始化的部分,我们可以通过连续两次调用setjmp得到两个稍有不同的jmp_buf信息,其中必然有我们最想要的跳转地址,而且它们肯定是不同的;剩下的大部分应该是差不多的,因为我们没有改变过多的状态(也许还有一些不重要的寄存器的值发生了变化)。通过对比它们间的不同部分,应该可以得到包含有地址的信息。我们扔掉前一个jmp_buf,只需要留下后者,用于跳转返回就可以了。

当然这里有个如何比较的问题,因为jmp_buf类型是透明的,我们并不确切知道它是如何定义的,也就是其中值的含义。幸好大多数(可能从早期继承过来的代码)平台都是将其作为一个值缓冲区处理的;因此我们可以按类似整型数组这样看待它(在我看到的几个平台上它的确都被定义为整型数组);更一般的,地址类型应该对应着void *,所以就以它作为最小的比较单位成功的可能性最大。

继续考虑跳转,通过前面对栈的分析,我们知道,最开始跳转失败的原因是栈冲突导致的,也就是jmp_buf里面还有一个关键的信息是我们需要的栈指针和栈帧指针。那我们现在已经有了可以工作的伪栈,把它们和跳转地址组合在一起就是我们真正想要的那个jmp_buf。对于jmp_buf我们一向用environment或者context来命名,表示这是计算机的执行环境,也就是寄存器集合的一个快照。

所以我们在slot类型里附加上一个jmpcode用于指示其携带的jmp_buf里究竟有哪写部分可能是跳转地址信息,然后再跳转的时候,再获得当时的运行环境,也就是jmp_buf,把其中的跳转地址修改为之前我们获得的那个就可以了。我们一般性的把jmpcode定义为最大包含有jmp_buf中所有void *类型值的集合,也就是长度为sizeof(jmp_buf)/sizeof(void*)的数组,好在其中存储jmp_buf中可能是跳转地址的位置索引;当然实际长度应该是肯定小于才对。

这里还有一个附带的奖励,当跳转回来后,携带有一个返回值,刚好可以用来表示我们希望传递的slot指针(setjmp返回值或者longjmp的携带的参数是int类型,在大多数32位平台上可以直接转换)。

另外某些平台上,包括gcc/msvc for x86都明确有在setjmp返回后不保证寄存器可以复用,就是隐含这个时候大部分的通用寄存器可能作废了,因此我们无需担心jmp_buf里除栈指针和跳转地址之外其他部分的不同。实际测试中gcc下setjmp的确具有相当于前面我们破坏寄存器的汇编指示的作用。(当然这一点不是通用的,在最终的代码给出的是更加准确的解决方案,修改栈指针而不是跳转地址。)

还有一些可以继续考究的地方,一是前头说的,还有就是可以进一步优化,因为jmp_buf索引位置是不会发生变化的,比较的事情只需要做一次就可以了。然后,如果有一些平台上,缺省的扫描方法不行,或者jmp_buf不是一个平凡的void *数组,你可以自己定制它——直接给出索引值或者查看头文件里的定义。等等,这里就不一一细述了。

这个办法不是一开始就想到的,只是后来希望做到最大的兼容性才在苦思冥想中得出,也因此把它放在最后来讲述。虽然在醒悟的那一刻还是有些兴奋,但再细细回味,觉得想到它应该是一件还算正常的事才对。这里面最关键是不要忽略那些隐含的信息,这是大多数时候思维上的弱点。

好了,编译、运行。。。

“啊!出错了!???” 。。。

原来是一高兴代码写错了(我犯了好几次把后面取索引值的地方直接写成了下标的错误!)

修改,再编译、运行。。。通过,bingo!

[全文完]

最后,给出最新版本,也作为第一个正式的版本。这个版本的接口已经修改成上次说的那种形式,查看test.c可以很容易就明白了。

BTW 后记:

经过不断的锤炼,现在对它能够工作已经十分具有信心。从一开始为写出能够工作的代码费心,转化到目前不论怎么写都不会出错,这其实是一个对问题域的了解过程——解决问题实际上都是这么一个相似的过程,所不同的是我们面对着不同的问题而已。


0 请登录后投票
   发表时间:2009-12-02  
个人觉得,没用。。。。别喷我
0 请登录后投票
   发表时间:2009-12-02  
直言不讳是好事,为何对自己的见解如此没有信心?
0 请登录后投票
   发表时间:2009-12-02  
说实话,我没仔细看,但是觉得,如果用C做嵌入式开发。
都是一些资源非常紧张的情景,实现这个机制,感觉费资源。
如果做一些资源比较大的开发,一般都有OS,有现成的这种机制。

Tiger的意思是让这个机制在各个OS上都可以用,屏蔽差异化吧。
我觉得可以做个wrapper封装一下就好了
0 请登录后投票
   发表时间:2009-12-02  
望指教:)
0 请登录后投票
   发表时间:2009-12-02  
呵呵,相互交流促进学习。

先说说外在的问题,这里的实现几乎是不耗资源的,栈的占用只比函数大一点点,调用上要比直接函数调用慢一点点而已。对比已知C的另一个实现GSignal,算是十分轻便的了,而且使用起来也要方便很多。实际上本质上这已经完全等同于语言自身的支持。

我不知道你所谓的OS支持的机制确切是什么,一般通过一些消息机制可以达到类似的目的,但两者是不能等同的。如同进程和线程,乃至纤程,他们运行在不同的级别上,使用起来的方法有着本质的差异。语言内置的机制具有类型安全性,使用方便等很多优点,虽然它也不是用于代替外部的机制。实现上,这也是无法通过对外部机制简单的封装就可以做到的。

当然进一步而言,还有很多工作要做才能更好满足到工程中的需要,不过只有使用的人才会意识到。

不管怎么说,谢谢你说出自己的想法。最好有机会的话不妨去试试使用它或者其他等价的东西。当你的代码达到一定规模的时候,也许会意识到它的用处。
0 请登录后投票
   发表时间:2009-12-21  
牛人啊。 楼主把C#的委托机制,Java的事实实现原理和机制进行了非常详细的说明,看了后非常有启发,佩服佩服!一直以为C++没有将事件的实现在语言层面的给予支持,是一个非常大的遗憾,以至于用来开发应用有许多不方便的地方。
0 请登录后投票
论坛首页 编程语言技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics