- 论坛徽章:
- 0
|
完成了绑定局部变量的任务之后对成功就满怀信心了,让我们再接再厉,一口气把剩下的任务完成,要攻克的堡垒只剩下最后一座了:绑定易失变量的值,以及堆空间的分配和释放,让我们在夕阳落山之前解决它,然后可以美美的睡上一觉。
先说说上次栈帧断言的事,通过宏定义的栈帧大小很难预先计算——用户可能随时修改代码;尽管一个较大的值比较安全,但一方面这不是绝对的,另一方面在一些场合(比如嵌入式)我们要使用紧凑的栈来节约空间,这就要避免栈溢出的情况。尽管一般来说因为栈分配是编译期的事所以无法做到动态进行(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宏,来模拟函数对象,然后作为参数进行传递。这些工作就交给大家自己完成了。
附件是目前为止的代码,欢迎大家下载测试并完善。 |
|