継続というプログラミングの概念があります.
簡単に表現すると継続は現在実行している命令以降の残りの演算です.プログラミング言語によってはこの継続を第一級の値として取得できるものもあります.例えばLisp方言であるSchemeは,そういった機能を持つ言語のひとつです.
継続をSchemeのプログラムとして簡単に説明します.Schemeはcall/cc
(別名: call-with-current-continuation
)という関数で継続を値として取得するインターフェースを持っています.
(call/cc proc)
参考:Revised^7 Report on the Algorithmic Language Scheme
call/cc
は単一の引数をとる関数を引数とする関数です.引数として渡された関数procに、call/cc呼び出し直後の継続を渡して評価します。
下記のコードを例にします.(引用
(define list-length
(lambda (obj)
(call/cc (lambda (return)
(letrec ((r
(lambda (obj)
(cond
((null? obj) 0)
((pair? obj) (+ (r (cdr obj)) 1))
(else (return #f))))))
(r obj))))))
(print (list-length '(1 2))) ; => 2 (リストの長さは2)
(print (list-length '(1 2 . 3))) ; => #f (リストではないので#f(false)が返される
(print (list-length '(1 2 (3 4)))) ; => 3 (リストの長さは3)
list-length
は引数がリストであればその長さを返し,そうでなければ#f
(false)を返す関数です.call/cc
は引数のラムダ式に直後の継続を渡すので,return
には継続が束縛されています.直後のcond
では引数の値の型によって処理を振り分けていますが,値がnull
(リストの終端)でもpair
(リスト)でもなければ(return #f)
を呼び出します.return
には継続が束縛されてるので,処理がcall/cc
を呼び出した直後に移りcall/cc
から#f
が返されます.
Schemeなどの継続が強力なのは,継続をプログラムのどこでも,いつでも,何度でも呼び出せる点です.関数の呼び出しの中で取得された継続は,その関数が値を返した後でも呼び出せます.
この機構により,再帰的に呼び出された関数のなかから一気に抜け出す大域脱出の仕組みが実現できます.このほかにも早期returnやコルーチンも実装できます.
C言語では通常,継続を第一級の値として取得することはできません.本稿ではC言語で疑似的に継続の取得と呼び出しをする方法の一つを説明します.
C言語で上記のような継続の取得と呼び出しを実現する小さなライブラリが下記のコードです.
ライブラリを初期化した地点から,継続を取得した地点までのコールスタックを保存して復元するようになっており,取得した継続を(初期化した地点より後であれば)どこでも・何度でも呼び出せます.
保存対象はコールスタックのため,ローカル変数の値は保存されますが,グローバル変数の値は保存されません.
#ifndef CONTINUATION_H
#define CONTINUATION_H
#include <alloca.h>
#include <setjmp.h>
typedef struct {
void *stack;
unsigned long stacklen;
void *rsp;
jmp_buf cont_reg;
} continuation;
void init_continuation(void *rbp);
#define GETRSP(rsp) asm volatile("mov %%rsp, %0" : "=r"(rsp));
#define GETRBP(rbp) asm volatile("mov %%rbp, %0" : "=r"(rbp));
// main関数などの初めに記述
#define INIT_CONTINUATION() \
{ \
void *main_rbp; \
GETRBP(main_rbp); \
init_continuation(main_rbp); \
}
// 現在の継続を取得
int get_continuation(continuation *c);
// 継続を呼びだす
void call_continuation(continuation *c, int val);
// 取得した継続のメモリ領域
void free_continuation(continuation *c);
#endif
#include <stdlib.h>
#include <setjmp.h>
#include <string.h>
#include "continuation.h"
static void *main_rbp;
void init_continuation(void *rbp) {
main_rbp = rbp;
}
int get_continuation(continuation *c) {
void *rsp;
GETRSP(rsp);
c->rsp = rsp;
c->stacklen = main_rbp - rsp + 1;
c->stack = malloc(sizeof(char) * c->stacklen);
memmove(c->stack, c->rsp, c->stacklen);
return setjmp(c->cont_reg);
}
void _cc(continuation *c, int val) {
memmove(c->rsp, c->stack, c->stacklen);
longjmp(c->cont_reg, val);
}
void call_continuation(continuation *c, int val) {
volatile void *q = 0;
do {
q=alloca(8);
} while (q > c->rsp);
_cc(c, val);
}
void free_continuation(continuation *c) {
free(c->stack);
}
ライブラリは次のAPIを持っています.基本的な使用法はsetjmp()/longjmp()
と同じ形式です.ただし,ライブラリの使用前の初期化とメモリの解放が必要となります.
・INIT_CONTINUATION()
:継続ライブラリの初期化.取得する継続の開始点となる
・get_continuation()
:継続の取得.継続を取得した場合は0,継続が呼び出されて制御が戻ってきた場合は0以外の値を返す
・call_continuation()
:継続の呼び出し.第二引数の値はget_continuation()
関数の戻り値となる
・free_continuation()
:継続が利用しているメモリ領域の解放
継続を取得するためには,継続の取得前にINIT_CONTINUATION()
マクロを呼び出してライブラリを初期化する必要があります.このマクロが呼び出された関数以下のローカル変数が継続に保存されるようになります.初期化前に継続を取得したり呼び出すことはできません.
下記のプログラムは継続を使った簡単なプログラムの例です.このプログラムは,main()
関数から呼び出した関数の中で継続を取得し,再度main()
関数に戻ってきた後に取得した継続を呼び出すという動作をしています.main()
関数の中ではローカル変数aを定義しています.継続の呼び出しによってローカル変数の値が継続取得時点での値に戻ること,また関数呼び出しした中から継続を取得して関数の戻り先から継続を呼び出せることを確認します.gcc main.c continuation.c
でコンパイル可能です.
#include <stdio.h>
#include "continuation.h"
// 継続を保存する大域変数
continuation cont;
int func2() {
puts("func2");
if (get_continuation(&cont) == 0) {
// 関数は継続を取得した場合0を返す
return 0;
} else {
// 継続が呼び出された場合0以外を返す
puts("func2 cont");
return 1;
}
}
int func() {
puts("func");
return func2();
}
int main(int argc, char *argv[]) {
INIT_CONTINUATION();
puts("main");
int a = 0;
if (func() == 0) {
puts("main_1");
a++;
printf("a: %d\n", a);
// 継続を呼び出す(第二引数はget_continuation()関数の返り値になる)
call_continuation(&cont, 1);
} else {
puts("main_2");
printf("a: %d\n", a);
}
free_continuation(&cont);
}
コンパイルしたプログラムを実行すると下記の出力が得られます.
main
func
func2
main_1
a: 1
func2 cont
main_2
a: 0
main()
関数は呼び出されるとローカル変数aを定義&0を代入して文字列"main"
を出力します.
次にfunc()
関数が呼ばれますが,これはすぐにfunc2()
関数を呼び出します.
func2()
関数は文字列"func2"
を出力すると,get_continuation()
関数で継続を取得します.
ここで取得された継続には,この後の処理やローカル変数(ここではmain()
関数で定義されたa=0
)の情報が含まれています.get_continuation()
関数は継続を取得した際は0を返すため,return 0
に到達し,最終的にmain()
関数まで制御が戻ります.
main()
関数から呼び出したfunc()
関数は0を返すため,文字列"main_1"
が出力されます.
ここで,変数aの値をインクリメントします.よって変数の値は1となり,"a: 1"
が出力されます.
次にcall_continuation()
で継続を呼び出すと,プログラムの制御が先ほど取得したfunc2()
の中のget_continuation()
関数が値を返したところまで戻ります.
このとき,get_continuation()
関数は1を返すため,else
節に制御が移り"func2 cont"
が出力されます.
再度func2()
からfunc()
関数を通してmain()
関数にreturn
されます.そしてmain()
関数のelse
節が実行され"main_2"
が出力されます.
継続を呼び出した際にmain()
関数のローカル変数も元の0に戻るため,再度a
を参照すると"a: 0
“が出力されます.
今回の継続ライブラリは私が趣味で書いているScheme風の処理系にcall/cc
関数を追加するために実装しました.
現在の処理系の実装では,Schemeの関数呼び出しとCの関数呼び出しが1対1で対応しているという戦略をとっているため,継続の機能を実現するためにはCレベルで処理を戻す必要が出てきたためです.
call/cc · kat0h/my-clisp@b0e82a3 · GitHub
これは私の処理系にcall/cc
を実装した際のコミットです.かなり小さな実装ですが,これによって大域脱出やコルーチン・例外機構など非常に強力な言語機能が処理系に実装できたことになります.
後編ではこのライブラリの動作の仕組みを説明します.
移植性のあるCの継続ライブラリ 多田 好克
https://ipsj.ixsq.nii.ac.jp/ej/?action=repository_uri&item_id=30459&file_id=1&file_no=1
継続 Wikipedia
https://ja.wikipedia.org/wiki/%E7%B6%99%E7%B6%9A
なんでも継続 Kawai Shiro
http://practical-scheme.net/docs/cont-j.html