C实现闭包

C语言本身是没有闭包的因为C的函数栈是后进先出,函数已返回,栈帧就会销毁,局部变量就会随之消失,要实现闭包,就要手动模拟v8那套把逃逸的变量放在堆上。

闭包可以通过函数指针 + 捕获的环境。

首先先实现一个闭包的结构体,首先需要函数指针和环境数据,然后是创建闭包和销毁闭包。

//闭包结构体
typedef struct { 
    void* (*fn)(void *env, void *args);
    void* env;
} Closure;
//环境结构体
typedef struct CounterEnv
{
    /* data */
    int count;
    int step;
} CounterEnv;
typedef void* (*ClosureFn)(void* env, void* args); // closurefn type

创建和销毁的方法:

Closure* closure_new(ClosureFn fn, void* env) {
    Closure* c = malloc(sizeof(Closure));
    if (!c) return NULL;
    c -> fn = fn;
    c -> env = env;
    return c;
}
void closure_free(Closure* c, void (*free_env)(void*)) {
    if (!c) return;
    if (free_env && c-> env) free_env(c->env);
    free(c);
}
void* closure_call(Closure* closure, void* args) {
    return closure->fn(closure->env, args);
}

然后是计数器部分,计数器可以创建、执行、销毁:

void* counter_fn(void* env, void* args) {
    CounterEnv* e = (CounterEnv*) env;
    if (args) e->step=*(int*)args;
    e->count += e->step;
    int* result = malloc(sizeof(int));
    *result=e->count;
    return result;
}
void counter_env_free(void* env) {
    free(env);
}
Closure* make_counter(int initial, int step) {
    CounterEnv* env = malloc(sizeof(CounterEnv));
    env->count=initial;
    env->step=step;
    return closure_new(counter_fn, env);
}

计数器是一个简单例子,用于验证闭包机制是否是正常工作的。因为count和step都是从make_counter捕获的,count在函数返回之后任然存货,多次调用间保持。每次调用count都累加,证明环境是共享的,c1和c2有独立env,互不干扰。

这里的Closure结构体就类似于v8里的函数对象,然后closure.env就是我们之前提到的 [[Environment]] 槽,env指向的堆数据就是Context上下文。

使用:

int main() {
    Closure* c1 = make_counter(0, 1); // 0开始,每次+1
    Closure* c2 = make_counter(100, 10);
    for (int i = 0; i < 3; i++) {
        int* r = (int*)closure_call(c1, NULL);
        printf("c1: %d\n", *r);
        free(r);
    }
    for (int i = 0; i < 2; i++) {
    int* r = (int*)closure_call(c2, NULL);
    printf("c2: %d\n", *r);
    free(r);
    }

    int new_step = 5;
    int *r = (int *)closure_call(c1, &new_step);
    printf("c1 (step=5): %d\n", *r);
    free(r);

    closure_free(c1, counter_env_free);
    closure_free(c2, counter_env_free);

    return 0;
}

[18:17:17] PS E:\leetcode\CPP\build>  .\closure.exe
c1: 1
c1: 2
c1: 3
c2: 110
c2: 120
c1 (step=5): 8
[18:17:19] PS E:\leetcode\CPP\build>

C函数栈

这是程序运行时用于管理函数调用的一种后进先出LIFO的内存结构,由操作系统和CPU共同维护,负责保存函数调用时候返回的地址、参数、局部变量等信息。

每当一个函数被调用,系统就会在栈上分配一块内存,称为栈帧。函数执行完毕后,这块内存被自动回收。

高地址
  ┌─────────────┐
  │   ...       │
  ├─────────────┤
  │  main 栈帧  │
  ├─────────────┤
  │  foo 栈帧   │  ← 当前执行 foo
  ├─────────────┤
  │  bar 栈帧   │  ← bar 被 foo 调用
  ├─────────────┤
  │   ...       │
  └─────────────┘
低地址

一个典型的栈帧包含了返回地址,当函数执行完了之后就会回到调用者的哪条指令;参数,传递给函数的实参,部分通过寄存器传递;局部变量,函数内部定义的变量;保存的寄存器,调用寄存器的值,用于恢复现场;栈帧指针RBP,指向栈帧底部;栈指针RSP指向当前栈帧顶部。

C的线程栈通常1MB(windows),或者8MB(linux),栈溢出就会触发EXCEPTION_STACK_OVERFLOW 或 SIGSEGV。