免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
楼主: TiGEr.zZ
打印 上一主题 下一主题

[C] 在ANSI C下设计和实现简便通用的signal-slot机制 [复制链接]

论坛徽章:
0
21 [报告]
发表于 2009-11-06 17:02 |只看该作者
完成了绑定局部变量的任务之后对成功就满怀信心了,让我们再接再厉,一口气把剩下的任务完成,要攻克的堡垒只剩下最后一座了:绑定易失变量的值,以及堆空间的分配和释放,让我们在夕阳落山之前解决它,然后可以美美的睡上一觉。

先说说上次栈帧断言的事,通过宏定义的栈帧大小很难预先计算——用户可能随时修改代码;尽管一个较大的值比较安全,但一方面这不是绝对的,另一方面在一些场合(比如嵌入式)我们要使用紧凑的栈来节约空间,这就要避免栈溢出的情况。尽管一般来说因为栈分配是编译期的事所以无法做到动态进行(c99有动态数组,一些平台上有alloc栈分配函数),但如果能通过断言在运行时给出错误提示那也不错了。

为了得出栈帧大小,我们必须有栈帧起始地址,栈帧结束地址或者附近位置的地址。比如,如果局部变量按顺序排列的话,起始地址可以看作是第一个变量的地址,而结束地址是最后一个变量的地址(附加上函数调用入栈参数的最大长度)。第一个无法预先得到,第二个理论上也存在问题,我们预先假设的顺序也不一定成立——编译器不会保证它。但对进入到slot的栈帧,我们是可以得到起始地址的,就是我们定义的数组伪栈,我们可以把它的地址放到slot里:

  slot->stack_frame_head = (void *)&temp[SLOT_STACK_FRAME_SIZE-1]; // 注意栈递减情况下,起始地址在最后一个字节上

那么栈尾呢?我们要的是应该的栈尾位置,跟定义的伪栈大小无关。这里的解决办法是如果我们知道原先栈的栈尾位置,可以通过和局部变量映射的同样办法映射到偏移后的地址上。那只要得到原先的栈尾位置就可以了:考虑到一旦产生函数调用后,新函数里的变量就肯定位于之前栈的上面了,也就是说可以得到大于并挨着原先栈尾的位置。我们可以在signal connect函数里,对局部变量求地址:

  slot->stack_frame_tail = (void *)&p;

然后在CONNECT宏中求得__SLOT_STACK_OFFSET之后的位置添加:

      assert(SLOT_STACK_FRAME_SIZE > (size_t)__SLOT_PTR->stack_frame_head \
             - ((size_t)__SLOT_PTR->stack_frame_tail - __SLOT_STACK_OFFSET)); \

好了,赶紧回到我们的最终任务上,前面我们绑定的局部变量有一个要求,就是必须按照例子中的那样,main中调用signal的时候,自己还没有退出,但实际使用中可无法保证这一点,一个是,我们可能在一个子函数中完成连接,在调用还没产生前已经返回了,还有一个是,如果并发的情况,你不知道那个函数还有没有结束,如果结束了,和前一种情况一样,它们使用的栈空间已经被释放,你从那个位置复制过来的值就“失效”了。

这个时候我们必须要有可以在堆上分配并获取这些值的办法。方案就是把这些值附加到slot上,然后slot自然也是不能消失的,在调用产生的时候,可以恢复它们。slot附加参数的办法自然和signal是类似的,两种方法,一是和现有signal方案一样,一种是我们讨论过但没有采纳的动态分配方案。这里我们选择了和signal的不同的方案,因为slot不像signal需要预先设计,我觉得这种方案要灵活些。

我们定义参数的类型:

struct __SlotArg {
  size_t addr;
  size_t size;
  void *value;
};

它其中包含三个部分,一个是栈帧上的偏移,一个是大小,还有一个指向它在堆上分配的值的空间。为什么这样设计,请继续往下看。

我们定义完整的slot类型,让它包括一个参数部分:

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;
};

其中的argc表示参数的数量,argv自然指向一个类似于参数数组(或其他集合)的东西。为了提高效率我们采用的是数组,这样可以在一次完成对所有变量所占空间的分配。那么携带参数的方法就是一个变参函数:

void __slot_fetch_args(struct __Slot *slot, unsigned int count, ...)
{
  unsigned int i;
  size_t size;
  char *p;
  va_list ap;
  slot->argc = count;
  // 预分配足够的a参数描述数组空间
  size = sizeof(struct __SlotArg) * count;
  p = (char *)size;
  slot->argv = (struct __SlotArg *)malloc(size);

  // 通过函数参数完成对参数描述数组的赋值
  va_start(ap, count);
  for (i = 0; i < count; i++) {
    slot->argv.addr = (size_t)(va_arg(ap, void *));
    slot->argv.size = va_arg(ap, size_t);
    size += slot->argv.size;
  }
  va_end(ap);
  // 在预分配的空间上一次分配完所有变量值存储的空间
  slot->argv = (struct __SlotArg *)realloc(slot->argv, size);
  p += (size_t)slot->argv;
  for (i = 0; i < count; i++) {
    slot->argv.value = p;
    // 保存值
    memcpy(slot->argv.value, (void *)(slot->argv.addr), slot->argv.size);
    p += slot->argv.size;
  }
}

函数的第一个参数不用说了,第二个参数是所携带参数的个数,也就是slot->argc,接下来的参数传递,是按照大小,和地址两两逐个进行的,这是因为不同的参数肯定有不同的类型,我们不需要处理类型(也无法处理),只需要处理内容(值)就可以了。从注释我们可以看到,上诉参数类型只是用于描述参数的,真正的参数附着在参数描述数组的后面,我们把它们的在堆上的地址保存在参数描述数组里。

然后进入到slot后,我们可以通过一个简单的过程,一次性复制所有参数到当前栈帧上供接下来引用,因为参数描述的addr是前局部变量的地址,因此很容易转换到当前帧上:

void __slot_commit_args(struct __Slot *slot, size_t slot_stack_offset)
{
  unsigned int i;
  for (i = 0; i < slot->argc; i++) {
    memcpy((void *)((size_t)slot->argv.addr - slot_stack_offset), slot->argv.value, slot->argv.size);
  }
}

为了使用上的方便,我们分别用SLOT_FETCH_LOCAL_VAR和SLOT_COMMIT_LOCAL_VAR宏来代替它们:

#define __SLOT_PARAMS(z, n, seq) \
  , &BOOST_PP_SEQ_ELEM(n, seq), sizeof(BOOST_PP_SEQ_ELEM(n, seq))

#define __SLOT_VA_ARGS(n, tuple) \
  n BOOST_PP_REPEAT(n, __SLOT_PARAMS, BOOST_PP_TUPLE_TO_SEQ(n, tuple))

#define SLOT_FETCH_LOCAL_VAR(slot_ptr, n, a)                \
  __slot_fetch_args(slot_ptr, __SLOT_VA_ARGS(n, a));

// #define SLOT_FETCH_LOCAL_VAR0(slot_ptr) SLOT_FETCH_LOCAL_VAR(slot_ptr, 0, ())
#define SLOT_FETCH_LOCAL_VAR1(slot_ptr, ...) SLOT_FETCH_LOCAL_VAR(slot_ptr, 1, (__VA_ARGS__))
#define SLOT_FETCH_LOCAL_VAR2(slot_ptr, ...) SLOT_FETCH_LOCAL_VAR(slot_ptr, 2, (__VA_ARGS__))
#define SLOT_FETCH_LOCAL_VAR3(slot_ptr, ...) SLOT_FETCH_LOCAL_VAR(slot_ptr, 3, (__VA_ARGS__))
#define SLOT_FETCH_LOCAL_VAR4(slot_ptr, ...) SLOT_FETCH_LOCAL_VAR(slot_ptr, 4, (__VA_ARGS__))
#define SLOT_FETCH_LOCAL_VAR5(slot_ptr, ...) SLOT_FETCH_LOCAL_VAR(slot_ptr, 5, (__VA_ARGS__))
#define SLOT_FETCH_LOCAL_VAR6(slot_ptr, ...) SLOT_FETCH_LOCAL_VAR(slot_ptr, 6, (__VA_ARGS__))
#define SLOT_FETCH_LOCAL_VAR7(slot_ptr, ...) SLOT_FETCH_LOCAL_VAR(slot_ptr, 7, (__VA_ARGS__))
#define SLOT_FETCH_LOCAL_VAR8(slot_ptr, ...) SLOT_FETCH_LOCAL_VAR(slot_ptr, 8, (__VA_ARGS__))
#define SLOT_FETCH_LOCAL_VAR9(slot_ptr, ...) SLOT_FETCH_LOCAL_VAR(slot_ptr, 9, (__VA_ARGS__))

#define SLOT_COMMIT_LOCAL_VAR()                                        \
  __slot_commit_args(__SLOT_PTR, __SLOT_STACK_OFFSET);

同样如果你不想用栈复制,可以直接引用堆上的变量,同样需要自己指定类型:

#define SLOT_ARG(n, type)                        \
  (*((type *)__SLOT_PTR->argv[n].value))

最后我们的slot如果需要释放,也需要释放变量分配的空间:

#define SLOT_FREE_ARGS(slot_ptr)         \
  if ((slot_ptr)->argc > 0) {                 \
    free((slot_ptr)->argv);                 \
    (slot_ptr)->argc = 0;                 \
  }

SLOT的初始化需要增加argc字段:

#define SLOT_INIT(slot_ptr)                        \
  (slot_ptr)->argc = 0;                                \
  (slot_ptr)->signal = 0;

差不多完成了。且慢,我们还没处理完连接部分,比如signal,slot单方面释放问题,还有如果是堆上分配的空间,到底如何被释放,比如我们的堆上的slot连接完了之后,signal如果disconnect掉它,它就变成悬浮的对象了(signal并不知道这一点),我们需要有一个最终处理过程:这可以通过signal指定不同signaling标记再调用一次slot完成,重写signal_disconnect函数,增加这一调用:

void __signal_disconnect(struct __Signal *signal, struct __Slot *slot)
{
  if (slot->prev) {
    slot->prev->next = slot->next;
  } else {
    signal->slot = slot->next;
  }
  if (slot->next) {
    slot->next->prev = slot->prev;
  }
  /* 通知slot连接被释放 */
  slot->signaling = -1;
  if (setjmp(signal->environment) == 0) {
    slot->signal_slot_invoke(slot);
  }
}

重写我们SIGNAL_CONNECT宏,增加一个finalization表达式参数:

#define SIGNAL_CONNECT(signal_ptr, slot_ptr, statement, finalization)        \
  {                                                                        \
    SLOT * __SLOT_PTR = (SLOT *)(slot_ptr);                                \
    struct __Signal * __signal_ptr;                                        \
    volatile size_t __SLOT_STACK_OFFSET;                                \
    __SLOT_PTR->stack_addr = (void *)&__SLOT_STACK_OFFSET;                \
    __SLOT_PTR->signal_slot_invoke = &__signal_slot_invoke;                \
    __signal_connect((struct __Signal *)(signal_ptr), __SLOT_PTR);        \
    __SIGNAL_SLOT_ENTRY(__SLOT_PTR);                                        \
    __signal_ptr = __SLOT_PTR->signal;                                        \
    if (__SLOT_PTR->signaling > 0) {                                        \
      __SLOT_STACK_OFFSET = (size_t)&__SLOT_STACK_OFFSET;                \
      __SLOT_STACK_OFFSET = (size_t)(__SLOT_PTR->stack_addr) - __SLOT_STACK_OFFSET; \
      assert(SLOT_STACK_FRAME_SIZE > (size_t)__SLOT_PTR->stack_frame_head \
             - ((size_t)__SLOT_PTR->stack_frame_tail - __SLOT_STACK_OFFSET)); \
      {                                                                        \
        __SLOT_BLOCK statement;                                                \
      }                                                                        \
      longjmp(__signal_ptr->environment, 1);                                \
    } else if (__SLOT_PTR->signaling < 0) {                                \
      __SLOT_PTR->signal = 0;                                                \
      __SLOT_STACK_OFFSET = (size_t)&__SLOT_STACK_OFFSET;                \
      __SLOT_STACK_OFFSET = (size_t)(__SLOT_PTR->stack_addr) - __SLOT_STACK_OFFSET; \
      assert(SLOT_STACK_FRAME_SIZE > (size_t)__SLOT_PTR->stack_frame_head \
             - ((size_t)__SLOT_PTR->stack_frame_tail - __SLOT_STACK_OFFSET)); \
      {                                                                        \
        __SLOT_BLOCK finalization;                                        \
      }                                                                        \
      longjmp(__signal_ptr->environment, 1);                                \
    }                                                                        \
  }

然后是单独释放signal或者slot的宏:

#define SIGNAL_FREE(signal_ptr) \
  while (signal->slot) { \
    __signal_disconnect(signal, signal->slot); \
  } \

#define SLOT_FREE(slot_ptr)                                                \
  if ((slot_ptr)->signal) __signal_disconnect((slot_ptr)->signal, slot_ptr); \
  SLOT_FREE_ARGS(slot_ptr)

让我们来测试一下:

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

  SLOT *slot = (SLOT *)malloc(sizeof(SLOT));

  /* 携带局部变量i,j当前值 */
  SLOT_FETCH_LOCAL_VAR2(slot, i, j);

  SIGNAL_CONNECT(signal, slot
                 , (
                    /* 提交携带的值 */
                    SLOT_COMMIT_LOCAL_VAR();
                    printf("int=%d, float=%f\n", i, j);
                  )
                 , (
                    /* 释放slot参数及自身空间 */
                    SLOT_FREE(slot);
                    free(slot);
                    )
                  );

}

int main()
{

  SIGNAL2(int, float) signal;

  SLOT slot;


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

  /* 子函数中的连接 */  
  connect((struct __Signal *)&signal);

  /* 宿主内部连接 */
  SIGNAL_CONNECT(&signal, &slot
                 , (
                    SLOT_COMMIT_LOCAL_VAR();
                    SLOT_REUSE_LOCAL_VAR(signal);
                    printf("int=%d, float=%f\n", signal._1, signal._2);
                  )
                  , ()
                  );

  SIGNAL2_EMIT(&signal, 5, 10);
  
  SIGNAL_DISCONNECT(&signal, &slot);

  return 0;
}

自此所有的工作基本就告成了,当然始终还存在些微小的修饰,以及高阶功能的增强,如果有人感兴趣了,留待一起讨论改善。

从这里的实现还可以知道,结合signal/slot,我们可以在c中完成类似lambda表达式这种函数式编程功能,你可以定义一系列参数的FUNCTOR宏,来模拟函数对象,然后作为参数进行传递。这些工作就交给大家自己完成了。

附件是目前为止的代码,欢迎大家下载测试并完善。

sigslot.zip

7.03 KB, 下载次数: 68

论坛徽章:
0
22 [报告]
发表于 2009-11-08 22:26 |只看该作者
补充一个图,并修正上次测试例子里的错误遗漏:

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

SLOT *slot = (SLOT *)malloc(sizeof(SLOT));

/* 携带局部变量i,j当前值 */
SLOT_FETCH_LOCAL_VAR2(slot, slot, i, j);

SIGNAL_CONNECT(signal, slot
, (
/* 提交携带的值 */
SLOT_COMMIT_LOCAL_VAR();
printf("int=%d, float=%f\n", i, j);
)
, (
/* 提交携带的值 */
SLOT_COMMIT_LOCAL_VAR();
/* 释放slot参数及自身空间 */
SLOT_FREE(slot);
free(slot);
)
);

}

图八.GIF (4.68 KB, 下载次数: 101)

图八.GIF

论坛徽章:
0
23 [报告]
发表于 2009-11-08 22:28 |只看该作者
除了一般性的事件驱动式编程外,可以引用到这里signal/slot机制的地方就是函数式编程的需求,函数对象是一种在许多语言里被反复实现的东西,不过因为signal/slot本身就是一种特殊的函数调用机制,所以我并没有立即扩充这方面的实现,如果有人需要可以自己实现或者一起讨论它。

我们这里用另外一个实际需求作为用例来展示lambda表达式的运用:异步/并发,也就是一般意义上的线程。在Java在面向对象语言中引入了各种完整的运行机制后,线程作为一个语言特性也得到了大家的关注,这里有一篇文章(1)比较完整的描述了在一个面向对象语言下应该如何规划线程的问题,最终作者提出了一个新的asychronous关键字,他希望可以直接修饰一个方法为异步而免去了调用者的烦恼。尽管在我看来这是不太合适的,但也反应了我们需要更方便的办法来规划这件事情。

作者的的想法还特别借鉴了任务的概念。在一些实时系统中,比如我在用的UCOS-II,没有线程这个东西,并发运行的单元是任务,它们一般是在系统启动时就初始化好的;因为要保证系统的实时性,任务是严格按照优先级的设定来调度的。不过习惯了一般系统里的线程后,我总是会把一些可能需要等待耗时的代码作为异步方式运行;虽然在一般的子任务里的仍然可以继续创建任务,但它是非常繁琐的事情,例如需要分配和指定堆栈,指定优先级(不能冲突),通过长长的参数列表调用函数,以及同样的变量/值传递的问题。这种暂时性的创建过程大部分都是重复的,所以我第一个想法就是如何归整一下,能够让程序自动来完成。

有了signal-slot机制,这种想法的实现就更加方便了,我可以指定一个lambda表达式作为异步运行的单元,就是像这样,在代码里需要异步执行一些东西的时候写:

ASYNC_RUN(...);// 省略号里是异步执行的代码

实现它不是十分困难的事情,UCOS-II的代码我已经有了。现在类似的,我们用gcc环境里通常有的phtread库来演示一下这个过程。相对而言,这会更简单一些——没有堆栈和优先级的分配问题。

我们大概知道需要调用的lambda表达式在结构里由一对signal/slot成员来表示,然后ASYNC_RUN的宏展开后相当于发出signal,让它自动调用slot的代码,也就是上述的省略号部分;当然这是在线程被创建之后,也就是说我们还有一个通用的函数,作为入口点用于调用pthread_create函数来创建线程,然后在其中调用signal就可以了。

数据结构我们定义如下:

struct __AsyncTask {
  pthread_t pid;
  pthread_mutex_t mutex;
  pthread_cond_t running;
  SIGNAL0() signal;
  SLOT slot;
};

其中running成员表示线程是否运行,后面你会看到它的用途:

代码部分我们可以交给两个函数进行,一个__async_create和一个__async_run来完成。

void __async_create(struct __AsyncTask *async_task)
{
  pthread_mutex_init(&async_task->mutex, NULL);
  pthread_cond_init(&async_task->running, NULL);

  pthread_mutex_lock(&async_task->mutex);

  // 将 async_task作为参数传递给执行线程
  pthread_create(&async_task->pid, NULL, (void *(*)(void *))&__async_run, (void *)async_task);

  // 等待线程运行并复制完栈
  pthread_cond_wait(&async_task->running, &async_task->mutex);

  pthread_mutex_unlock(&async_task->mutex);
}

void * __async_run(struct __AsyncTask *async_task)
{
  SIGNAL0_EMIT(&async_task->signal);
  SIGNAL_DISCONNECT(&async_task->signal, &async_task->slot);
  pthread_cond_destroy(&async_task->cond);
  return 0;
}

然后就是定义ASYNC_RUN宏:

#define ASYNC_RUN(a) \
  { \
    struct __AsyncTask __async_task; \
    SIGNAL_INIT(&__async_task.signal); \
    SLOT_INIT(&__async_task.slot); \
    SIGNAL_CONNECT(&__async_task.signal \
                   , &__async_task.slot \
                   , ( \
                       SLOT_REPLICATE_LOCAL_STACK(); \
                       pthread_mutex_lock(&__async_task.mutex); \
                       pthread_cond_signal(&__async_task.running); \
                       pthread_mutex_unlock(&__async_task.mutex); \
                       { \
                         a; \
                       } \
                     ) \
                   , () \
                  ); \
    __async_create(&__async_task); \
  }

这其中需要注意的地方就是并发时,在线程还没有开始运行可能生成线程的函数已经结束了,但这样我们就无法引用到局部自由变量了。为了达成这个目的,当然可以使用SLOT_FETCH_LOCAL_VAR()宏,但我们这里因为确切知道signal发出的时候——几乎同时进行,所以可以用一个锁互斥的操作,让生成函数等待线程运行并复制完本地栈之后再继续运行,复制栈的工作交给SLOT_REPLICATE_LOCAL_STACK宏,前面我们已经描述过得到栈帧大小的办法,所以我们可以省略掉对SLOT_REUSE_LOCAL_VAR宏的使用(注意:但不包括函数的参数部分,如果要使用参数部分的值,另定义局部变量赋值并使用它),这样显得更加傻瓜化一些。SLOT_REPLICATE_LOCAL_STACK宏如下:

#define SLOT_REPLICATE_LOCAL_STACK() \
SLOT_LOCAL_VAR(__SLOT_PTR, struct __Slot *) = __SLOT_PTR; \
SLOT_LOCAL_VAR(__SLOT_STACK_OFFSET, size_t) = __SLOT_STACK_OFFSET; \
SLOT_LOCAL_VAR(__signal_ptr, struct __Signal *)= __signal_ptr; \
memcpy((void *)((size_t)__SLOT_PTR->stack_frame_tail - __SLOT_STACK_OFFSET), __SLOT_PTR->stack_frame_tail \
        , (size_t)__SLOT_PTR->stack_frame_head + __SLOT_STACK_OFFSET - (size_t)__SLOT_PTR->stack_frame_tail);
         
增加一些辅助设施,得到线程句柄的宏,等等:
#define ASYNC_PTHREAD_ID() __async_task.pid
#define ASYNC_LOCAL_VAR SLOT_LOCAL_VAR
...

最后是测试程序:

int main()
{
  int i = 5;
  float j = 10.0;
  volatile pthread_t pid;
  ASYNC_RUN(
            ASYNC_LOCAL_VAR(pid) = ASYNC_PTHREAD_ID();
            printf("int=%d, float=%f\n", i, j);
            );

  while (pid == 0);
  pthread_join(pid, NULL);
  return 0;
}


并发是编程中要处理的一大问题,我们这里只涉及一个非常小的局部,如果可能的话,将来我们将单独涉猎它。

到此为止,整个介绍过程应该可以圆满结束了,欢迎大家多提宝贵意见,帮助我继续改进它。

“咦,这位愁眉苦脸的同学还有什么问题吗?”

“嗯。。。啊。。。”

“哦,你的意思是说,虽然大部分代码是C写就的,但还是存在一小片段的代码无法用C实现,而你的平台上的这种特定方案难以实现是吗?”

这的确是可能的,需要在某些平台上使用汇编可能是不太切实际的要求——“不过没有关系,我这里还有一个用C实现的方案”。

”?。。。“

“是的,虽然它仍然不能保证百分之百的兼容,但相信绝大多数情况下是可行的。最重要的是,它是由纯ANSI C实现的”。

“不过今天的时间不多了,我们留待下次再讲吧”,说完某人就转身离去,后头砸来一片臭鸡蛋。

(1) http://www.ibm.com/developerworks/cn/java/j-king/

[ 本帖最后由 TiGEr.zZ 于 2009-11-8 23:39 编辑 ]

论坛徽章:
0
24 [报告]
发表于 2009-11-09 11:15 |只看该作者
这里有一个简单易用的signal-slot C++实现库,供参考http://sigslot.sourceforge.net/

论坛徽章:
0
25 [报告]
发表于 2009-11-09 12:58 |只看该作者

回复 #24 uskee 的帖子

谢谢,网络上有关c++下这方面的实现非常多,特别是大家对模板运用的得心应手之后。借助于模板可以自动生成中间用于粘接部分的代码,而不需要像我这里做的其实是在系统/编译层面上解决问题。

论坛徽章:
0
26 [报告]
发表于 2009-11-09 13:40 |只看该作者
signal-slot, 什么冬冬? 从LZ的描述来看感觉就是一个observer模式.

论坛徽章:
0
27 [报告]
发表于 2009-11-10 22:43 |只看该作者
先把最近更新的代码发上来,对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会不会好些,望各位指点一二,先就此谢过!

sigslot.zip

5.63 KB, 下载次数: 43

论坛徽章:
0
28 [报告]
发表于 2009-11-18 17:57 |只看该作者
在文章结束前,发布最后一个测试候选版,增加了未知编译器/纯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. ...

sigslot.zip

267.92 KB, 下载次数: 63

论坛徽章:
0
29 [报告]
发表于 2009-11-22 21:07 |只看该作者
上次代码中有一处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.addr <= (size_t)__SLOT_PTR->stack_frame_head - __SLOT_PTR->argv.size \
-             && (size_t)&__SLOT_PTR->argv.addr >= (size_t)__SLOT_PTR->stack_frame_tail); \
+             && (size_t)__SLOT_PTR->argv.addr >= (size_t)__SLOT_PTR->stack_frame_tail); \
     }                                                                        \
   }                                                                        \
     __slot_commit_args(__SLOT_PTR, __SLOT_STACK_OFFSET)

论坛徽章:
0
30 [报告]
发表于 2009-11-22 21:08 |只看该作者
上次留了个包袱,很长时间过去了,想必大家都不耐烦了。前面得到的反馈不多,我就当作都在等最后的结果。不说废话,先直接贴出代码(前面已经给出了完整的代码,这里是示意的,虽然不一定工作,但更加直接明了一些):

首先给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,他们是对应的。之前当我们在不同平台上移植时,也只要处理好调用和入口就可以了。

  1. #define __SIGNAL_SLOT_ENTRY(slot_ptr) \
  2.   { \
  3.     jmp_buf environment; \
  4.     setjmp(environment); \
  5.     int ret = setjmp(slot_ptr->environment; \
  6.     if (ret == 0) { \
  7.       unsigned int i, j = 0; \
  8.       for (i = 0; i < sizeof(jmp_buf) / sizeof(void *); i++) { \
  9.         if (((void **)&environment)[i] != ((void **)&slot_ptr->environment)[i]) { \
  10.           slot->jmpcode[j++] = i; \
  11.         } \
  12.       } \
  13.       slot_ptr->jmpcode[j] = -1; \
  14.     } else { \
  15.       slot_ptr = (struct __Slot *)ret; \
  16.     } \
  17.   }
  18.   
  19. # define __SIGNAL_SLOT_INVOKE(slot_ptr) \
  20.   { \
  21.     jmp_buf environment; \
  22.     if (setjmp(environment) == 0) { \
  23.       unsigned int i; \
  24.       for (i = 0; slot_ptr->jmpcode[i] >= 0; i++) { \
  25.         ((void **)&environment)[slot_ptr->jmpcode[i]] = ((void **)&slot_ptr->environment)[slot_ptr->jmpcode[i]]; \
  26.       } \
  27.       longjmp(environment); \
  28.     } \
  29.   }
复制代码


对整个过程稍有了解的人看了代码一定就立刻明白了,我们抛开实现的细节看看如果一般性面对必须要用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 后记:

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

[ 本帖最后由 TiGEr.zZ 于 2009-11-22 21:10 编辑 ]

sigslot.zip

268.04 KB, 下载次数: 94

您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP