這裡介紹 C 語言的 setjmplongjmp 函數的用法,還有典型的使用範例。

在 C 語言中的 goto 只能跳到函數內部的 label 位置,若要跳到其他函數中則必須使用 setjmplongjmp,這兩個函數在深層的程式錯誤處理上非常好用。


以下是一段簡單的 C 語言程式碼:

#include <stdio.h>
int fun_a(int v) {
  int r = v * 2 - 1;
  return r;
}
int fun_b(int v) {
  int r = fun_a(v) + 6;
  return r;
}
int fun_c(int v) {
  int r = fun_b(v) * 5 - 21;
  return r;
}
int main() {
  int x = 5;
  int result = fun_c(x);
  printf("Result = %d\n", result);
  return 0;
}

這個程式在執行時首先會呼叫 fun_c 函數,而在 fun_c 函數中又呼叫了 fun_b 函數,以此類推最後呼叫到 fun_a 函數,形成了一個比較深層的函數呼叫堆疊(call stack),一般來說,函數呼叫堆疊結構通常是這樣:

函數呼叫堆疊(LibreOffice 原始檔

每個函數的區域變數存在於自己的 stack frame 當中,在這樣的程式呼叫結構中,如果在比較深層的函數裡需要處理錯誤的狀況時,就會比較麻煩,例如在 fun_a 函數中若出現錯誤,我們可能會需要中止之前一連串的呼叫動作,回到 main 中繼續其他動作,如果按照一般的作法,我們會需要定義每個函數的錯誤傳回值,逐層檢查與處理,動作相當繁雜。

setjmplongjmp 函數可以讓程式往回跳到函數呼叫堆疊中的某個函數中,就像是一種跨函數的 goto

首先使用 setjmp 在程式中標示一個目標位置(跳躍的目的地),然後在程式要進行跳躍的地方呼叫 jongjmp 即可。

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

// 儲存程式跳躍時所需之資訊
jmp_buf jmpbuffer;

int fun_a(int v) {
  int r = v * 2 - 1;
  if (r < 0) {
    // 跳躍至 main 函數
    longjmp(jmpbuffer, 1);
  }
  return r;
}
int fun_b(int v) {
  int r = fun_a(v) + 6;
  if (r > 10) {
    // 跳躍至 main 函數
    longjmp(jmpbuffer, 2);
  }
  return r;
}
int fun_c(int v) {
  int r = fun_b(v) * 5 - 21;
  return r;
}
int main() {
  // 設定跳躍目標位置
  int jmpVal = setjmp(jmpbuffer);
  if ( jmpVal == 1 ) {
    printf("fun_a error\n");
  } else if ( jmpVal == 2 ) {
    printf("fun_b error\n");
  } else {  // jmpVal == 0
    int x = -5;
    int result = fun_c(x);
    printf("Result = %d\n", result);
  }
  return 0;
}

setjmp 設定跳躍目標位置時,會需要指定一個特殊的 jmp_buf 變數,用來儲存程式跳躍時所需之資訊,而在直接呼叫 setjmp 函數時其傳回值為 0,若是透過 longjmp 跳回這裡時,其傳回值就會是呼叫 longjmp 時所指定的值,程式設計者可以靠著這個傳回值來判斷並處理不同的錯誤情形。

setjmp 設定好目標位置之後,接著在深層的函數呼叫中出現錯誤並呼叫 longjmp 時,傳入同一個 jmp_buf 變數,並加上一個整數傳回值,這樣程式就會跳到當初設定的目標位置繼續執行,而 setjmp 的傳回值就會是 longjmp 所指定的整數值。

當程式透過 longjmp 返回 main 函數之後,main 之後的函數呼叫堆疊會自動被清空,並從 setjmp 的位置開始執行,就跟程式一開始的狀態類似,只不過 setjmp 的傳回值不同。

函數呼叫堆疊

程式跳躍後的變數

當呼叫 longjmp 之後,程式返回至 main 函數中,而函數呼叫堆疊也跟著回復至原來的樣子,不過接下來的問題就是 main 函數中的變數值會不會也跟著回復原來的值?

以下是一段測試各種不同變數類型的程式碼:

#include <stdio.h>
#include <setjmp.h>
jmp_buf jmpbuffer;
static int g;  // global
void f1() {
  longjmp(jmpbuffer, 1);
}
void f2(int l, int g, int r, int v, int s) {
  printf("local = %d, global = %d, register = %d, "
      "volatile = %d, static = %d\n",
      l, g, r, v, s);
  f1();
}
int main() {
  int l;           // local
  register int r;  // register
  volatile int v;  // volatile
  static int s;    // static
  l = 1, g = 2, r = 3, v = 4, s = 5;

  if ( setjmp(jmpbuffer) != 0 ) {
    printf("local = %d, global = %d, register = %d, "
        "volatile = %d, static = %d\n",
        l, g, r, v, s);
    return 0;
  }

  l = 101, g = 102, r = 103, v = 104, s = 105;

  f2(l, g, r, v, s);
  return 0;
}

同樣的程式在編譯時使用不同的參數會造成不同的結果:

gcc source.c
./a.out
local = 101, global = 102, register = 103, volatile = 104, static = 105
local = 101, global = 102, register = 3, volatile = 104, static = 105
gcc -O source.c
./a.out
local = 101, global = 102, register = 103, volatile = 104, static = 105
local = 1, global = 102, register = 3, volatile = 104, static = 105

加入 -O 讓編譯器進行基本的最佳化之後,globalvolatilestatic 三種變數還是維持其最後所指定的值,而 register 變數則始終都會自動回復原來的值,但 local 變數的值卻改變了。

使用 setjmplongjmp 轉跳時,位於記憶體中的變數會維持其最後的值,而位於 CPU 或浮點數 registers 中的變數則會回復至原來的值,在上面的例子中,local 變數原本位於記憶體中,經過最佳化之後,被放置在 register 中,所以產生了不同的結果。

根據 gcc 的說明文件,在呼叫 longjmp 進轉跳之後,除了 volatile 變數之外,其他的變數都有可能產生不可預期的結果,所以若需要撰寫一個可移植(portable)的程式,請將變數宣告為 volatile