非本地跳转 (NonLocal Jump)

ddatsh

dev #c

函数内进行跳转,可以用goto

但从一个函数内跳转到另一个函数的某处,goto 不能完成

setjmp 和 longjmp 用来完成这种类型的分支跳转,在处理异常上非常有用

setjmp/longjmp

#include <setjmp.h>
int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int retval);
#include <stdio.h>
#include <setjmp.h>

int E_FOO = 1;
int E_BAR = 2;
int E_UNKOWN = -1;

static jmp_buf env;

void foo(void) {
    // longjmp(env, E_FOO);     // 相当于 throw(E_FOO)
}

void bar(void) {
    longjmp(env, E_BAR);        // 相当于 throw(E_BAR)
}

int main()
{
    int ret = 0;
    int rc;
    rc = setjmp(env);

    if(rc == 0) {                // 相当于 try block
        foo();
        bar();
        printf("run success\n");
    } else if(rc == E_FOO) {    // 相当于 catch(E_FOO)
        printf("error happened in foo()\n");
        ret = E_FOO;
    } else if(rc == E_BAR) {    // 相当于 catch(E_BAR)
        printf("error happened in bar()\n");
        ret = E_BAR;
    } else {                    // 相当于 catch(Exception)
        printf("error happened\n");
        ret = E_UNKOWN;
    }
}

setjmp保存当前的环境(即程序的状态)到数据结构 (jmp_buf),随后可被 longjmp “跳转"回setjmp所保存的程序执行状态

longjmp完成后,程序从对应的setjmp调用处继续执行,如同setjmp调用刚刚完成。longjmp retval传0,setjmp的返回值为1;否则,setjmp的返回值为retval

setjmp函数相当于设置一个程序运行时点,第一次返回 0 表示设置运行时点成功,第二次返回由longjmp,第二次的返回值是longjmp函数的第二个参数值

longjmp函数永不返回,它只是迫使setjmp函数第二次返回,并设置其返回值

setjmp函数和longjmp函数配合能达到类似于 Java 中的trycatchthrow的效果

惯用法:

if( setjmp(x) ){
	/* handle longjmp(x) */
}
switch(setjmp(env)):
    case 0:         //default
        //...
    case 1:         //exception 1
        //...
    case 2:         //exception 2
        //...
    //...

java & c

public class Main {

    static class BadException extends Exception {

        BadException(String msg) {
            super(msg);
        }

    }

    static double divide(double to, double by) throws BadException {
        if (by == 0)
            throw new BadException("Cannot / 0");
        return to / by;
    }

    static void f() {
        try {
            divide(2, 0);
            //...
        } catch (BadException e) {
            System.out.println(e.getMessage());
        }
        System.out.println("done");
    }

    public static void main(String[] args) {
        f();
    }
}
#include <stdio.h>
#include<setjmp.h>

static jmp_buf env;

double divide(double to, double by) {
    if (by == 0) {
        longjmp(env, 1);
    }
    return to / by;
}

void f() {
    if (setjmp(env) == 0) {
        divide(2, 0);
    }
    else {
        printf("Cannot / 0\n");
    }
    printf("done\n");
}

void main() {
    f();
}

sigsetjmp/siglongjmp

POSIX setjmp函数并不保存 signal(BSD 会保存 signal),如果需要连同 signal 一并保存的话,用另一组函数

#include <setjmp.h>
int sigsetjmp(sigjmp_buf env, int savesigs);
void siglongjmp(sigjump_buf env, int retval);

sigsetjmp函数多了一个参数savesigs,不为 0,则会将信号掩码保存到env中,否则不保存

#include <stdio.h>
#include <signal.h>
#include <setjmp.h>
#include <unistd.h>

static sigjmp_buf env;

void handler(int sig) {
    siglongjmp(env, 1); // return non-zero value
}

int main() {
    signal(SIGINT, handler); // 重置Ctrl-C信号

    int rc;
    rc = sigsetjmp(env, 1); // 1: 保存信号掩码
    if (rc == 0) {
        printf("started\n");
    }
    else {
        printf("\rrestarted\n");
    }

    int i = 0;
    while (i++ < 10) {
        sleep(1); // sleep 1 second
        printf("processing...%d\n", i);
    }

    return 0;
}

输出类似

started
processing...1
processing...2
processing...3
restarted       # 按下Ctrl-C
processing...1
processing...2
restarted       # 按下Ctrl-C
processing...1

setjmp 也可以用来模拟 coroutine 。但正确的 coroutine 实现需要为每个 coroutine 配备一个独立的数据栈,setjmp 无法做到

setcontext 库 ,已经不是 C 语言的标准库了