文章目录

##C语言##
函数内部的流程控制有:

  • if … else …
  • while
  • break
  • continue
  • goto

函数间流程控制:

  • setjmp()和longjmp()

一个程序的状态依赖于当前内存内容(如代码、全局变量、堆、栈)和寄存器的内容。寄存器内容包括stack pointer(sp),stack base pointer(bp)以及progran counter(pc)。C语言函数间的流程控制的实现简单的说就是setjmp()保存了寄存器的内容,而longjmp()则将先前保存的寄存器内容重新装载到寄存器中,从而将程序状态改变到setjmp()调用时的状态。

一个setjmp()、longjmp()的例子:

#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>

int main() {
    jmp_buf env;
    int i;

    i = setjmp(env);
    printf("i = %d\n",i);

    if(i != 0) return 0;

    longjmp(env, 2);
    printf("Does this line get printed?\n");
    return 0;
}

运行的结果:

i = 0
i = 2

第一次成功调用setjmp()返回0,然后调用longjmp()传入参数2,这导致程序返回到setjmp()并且返回2,继续执行满足if语句程序退出。

使用setjmp()时,最常见的错误就是对setjmp()进封装,用一个函数来调用它。

一个错误用例:

int a(char* s, jmp_buf env) {
    int i;

    i = setjmp(env);
    printf("Setjmp returned -- i = %d, 0x%x\n", i, (unsigned)s);

    printf("s = %s\n", s);
    return i;
}

int b(int i, jmp_buf env) {
    printf("In B: i = %d. Calling longjmp(env,i)\n",i);

    longjmp(env, i);
}

int main() {
    jmp_buf env;

    if(a("Jim", env) != 0) return 0;
    b(3, env);
    return 0;
}

在本人的测试机上运行程序会有如下输出:

Setjmp returnd -- i = 0, 0x8048622
s = Jim
In B: i = 3. Calling longjmp(env, i)
Setjmp returned -- i = 3, 0x3
Segmentation fault (core dumped)

我们来看看为什么会发生这种情况。当main()被调用的时候,栈的情形如下图所示:

         Stack        
|----------------|
|                |
|                |
|                |
|                |
   |                |
|                | <-------- sp
| env[0]         |
| env[1]         |
| env[2]         |               pc = main
| env[3]         |
| ....           |
| env[8]         |
| other stuff    | <------- bp
|--------------- |

当main()调用a(),参数由右到左被压入栈中,pc、bp寄存器的值也被压入栈中,同时pc、bp和sp寄存器存入新值,如下图所示:

                     Stack        
               |----------------|
               |                |
               |                | <--------- sp, bp
/------------- | old bp in main |
|              | old pc in main |
|   "Jim" <--- | s = "Jim"      |
|         /--- | pointer to env | 
|         \--> | env[0]         |
|              | env[1]         |
|              | env[2]         |               pc = a
|              | env[3]         |
|              | ....           |
|              | env[8]         |
\------------> | other stuff    | 
               |--------------- |

在a()中首先为局部变量i分配空间:

                      Stack        
               |----------------|
               |                | <--------- sp
               |      i         | <--------- bp
/------------- | old bp in main |
|              | old pc in main |
|   "Jim" <--- | s = "Jim"      |
|         /--- | pointer to env | 
|         \--> | env[0]         |
|              | env[1]         |
|              | env[2]         |               pc = a
|              | env[3]         |
|              | ....           |
|              | env[8]         |
\------------> | other stuff    | 
               |--------------- |

接着调用setjmp(),这会使得当前sp、bp和pc寄存器的值被保存到env中。最后a()输出“i = 0”和“s = Jim”并且返回到main()中。现在栈中的情况看起来好像跟调用a()之前是一样的,除了env中保存了一些调用setjmp()时的老的寄存器的值,栈如下图所示:

       Stack        
|----------------|
|                |
|                | 
|                |
|                |
|                |
|                | <----------- sp
| env[0]         |
| env[1]         |
| env[2]         |               pc = main
| env[3]         |
| ....           |
| env[8]         |
| other stuff    | <------------ bp
|--------------- |

然后main()调用b(),栈如下图所示:

                     Stack        
               |----------------|
               |                |
               |                | <--------- sp, bp
/------------- | old bp in main |
|              | old pc in main |
|              | i = 3          |
|         /--- | pointer to env | 
|         \--> | env[0]         |
|              | env[1]         |
|              | env[2]         |               pc = b
|              | env[3]         |
|              | ....           |
|              | env[8]         |
\------------> | other stuff    | 
               |--------------- |

在b()中longjmp()被调用,寄存器中的值被保存在env中的值所替换,也就是说现在寄存器中的值是替换成了a()调用setjmp()是的寄存器的值,但是栈中的值并没有变,现在的栈如下图所示:

                      Stack        
               |----------------|
               |                | <--------- sp
               | i = 2          | <--------- bp
/------------- | old bp in main |
|              | old pc in main |
|              | s??    = 3     |
|         /--- | pointer to env | 
|         \--> | env[0]         |
|              | env[1]         |
|              | env[2]         |               pc = a
|              | env[3]         |
|              | ....           |
|              | env[8]         |
\------------> | other stuff    | 
               |--------------- |

现在问题出来了参数s的值变成了3,当要输出字符串s时,访问了一个错误的地址最终导致了core。


##Scheme语言##

在C中setjmp()只是保存了寄存器的值,这就对流程控制有一些限制,只能在函数内部或者在更高层的调用函数间跳跃。可能大家回想如果我们不但保存寄存器的值,还保存栈中的值会这么样呢?既保存寄存器又保存栈的值正式某些编译器(如Guile,Gambit Scheme[1])实现scheme语言中的continuation的方法。(吐槽:学术界和工程界的差别就在于一个把问题描述的尽量复杂抽象,而另一个却要尽量把问题简单化,虽然这种差别是有意义的,但写continuation的文档也太TM云里雾里了吧!好吧我理解能力差……)

###Continuation定义###

在scheme语言中有一个函数叫做call-with-current-continuation(简称call/cc),这个函数有一个参数(在下文中我们都称该参数为receiver)。receiver必须是一个只有一个参数的函数,并且这个参数就是continuation,它是一个形如(lambda (x) …)的lambda表达式。当(call/cc receiver)调用时内部会保存当前的寄存器和栈的值,宾且执行(receiver continuation),当continuation调用时就会将保存的寄存器和栈值重新加载。我们可以用如下两个步骤来得到continuation函数的表达式:

  1. 将表达式用口代替
  2. 将结果表达式封装为一个形如(lambda (口) …)的lambda表达式

我们来看一个例子,(+ 3 (* 4 (call/cc receiver)))产生的continuation表达式为:

(lambda (口)
    (+ 3 (* 4 口)))

###几个例子###

1.从循环中退出

首先定义一个循环函数:

(define infinite-loop
  (lambda (procedure)
    (letrec ((loop (lambda ()                    
                     (procedure)
                     (loop))))
      (loop))))

procedure执行时遇到某些条件我们可能想要退出循环,遗憾的是在scheme中并没有提供C语言中的return功能,但是我们可以通过call/cc来实现与return相同的功能,定义如下:

(define procedure
  (lambda (args)
    (let ((receiver (lambda (return)
                      (infinite-loop
                       (lambda ()
                         (if *exit-condition-met*
                             (return exit-value)
                             *action-to-be-performed*))))))
      (call/cc receiver))))

在上面的代码中为了便于理解我们将传递给receiver的continuation命名为为return,这样看起来跟C语言中的return更为相似。

利用上面的框架我们实现一个从0数到n并退出的程序:

(define count-to-n
  (lambda (n)
    (let ((receiver (lambda (return)
                      (let ((count 0))
                        (infinite-loop
                         (lambda ()
                           (if (= count n)
                               (return count)
                               (begin
                                 (display "The count is:")
                                 (display count)
                                 (display "\n")
                                 (set! count (+ count 1))))))))))
      (call/cc receiver))))

运行(count-to-n 4)结果为:

The count is:0
The count is:1
The count is:2
The count is:3
4

根据上面提到的两个步骤,我们可以知道return形如(lambda (口) 口),即(return count)的返回值既是count,所以最后一行的结果显示为4。

2.利用continuation实现loop

代码如下:

(define get-continuation-with-values
  (lambda (values)
    (let ((receiver (lambda (proc) (cons proc values))))
      (call/cc receiver))))

(define loop-with-current-value
  (lambda (values)
    (let ((here-with-values (get-continuation-with-values values)))
      (let ((continue (car here-with-values))
            (current-values (cdr here-with-values)))
        (display current-values)
        (if (< (car current-values) 5)
            (continue (cons continue
                            (map (lambda (x) (+ x 1))
                                 current-values)))
            (display "Done!"))))))

运行((loop-with-current-value ‘(1 2 3))得到结果:

(1 2 3)(2 3 4)(3 4 5)(4 5 6)(5 6 7)Done!

在get-continuation-with-values中将continuation与values连接在一起并且返回,((loop-with-current-value ‘(1 2 3))运行时,第一次调用get-continuation-with-values得到的值为’(continuation 1 2 3),接着将continue赋值为continuation,将current-values赋值为’(1 2 3),如果current-values的第一个值小于5,则将continue与’(2 3 4)链接成为’(continuation 2 3 4),因为continue是一个continuation其形式为(lambda (口) 口),所以continuae调用的结果就是返回’(continuation 2 3 4)并且流程跳到给here-with-values赋值的地方,然后继续执行,当if条件满足则退出。

##总结##

Schmeme中的continuation的实现在思路上跟C中的setjmp()是相同的,但实现更复杂,功能也就更加强大。可以说setjmp()实现的功能是continuation的子集,但我们可以借助setjmp()来方便理解,利用continuation可以实现更加复杂的功能,比如用户态的进程即协程(coroutine)等。

文章目录

举个栗子……