הסתעפויות גלובליות Global jumps

 

 

    הסתעפויות  גלובליות הינו מנגנון המאפשר לנו לבצע "חזרה" לרוטינה מכמה רמות קריאה ולא אחת.  המנגנון הזה הוא מעין הכללה של הפקודה  return. הפקודה return  מאפשרת לנו מכל מקום בפונקציה לחזור רמה אחת. אם אנחנו רוצים לחזור מספר רמות כלפי מעלה הדבר כרוך בשינוי התוכניות שבדרך, דבר שהוא בעיתי מאד ממספר בחינות, למשל תארו לעצמכם מה קורה כאשר שיש מספר קריאות של כל רוטינה בדרך.

 

המנגנון התומך בהסתעפויות מסוג זה ב-C הם הרוטינות setjmp ו-longjump.

 

  אפשר להדגים את הנושא בצורה הבאה:

 

  Void f1()

  {

     return;

   }

 

Void f2()

{

    f1();

}

Void f3()

{

 f2();

}

 

int main()

{

  f3();

 

}

 

 

מה שקורה כאן הוא ש-main קורא ל-f3 שקורא ל-f2 שקורא ל-f1. ואנחנו כאילו רוצים לחזור מ-f1 ישירות ל-main.

 

מתי נרצה לעשות כזה דבר?  יש מספר הקשרים שיש בזה הגיון, למרות שבאופן עקרוני, מעטים יחלקו שמימוש קפיצה מסוג זה חורג מתכנות מבני ובחזקת מימוש קוד בעיתי. זאת משום שמגיעים לקוד אחד מהקשרים שונים, דבר המקשה על חיזוי, שליטה וניפוי שגיאות.  דברים כאלו יכולים להתרחש תוך כדי שינויים בתוכנית, מאותה סיבה שמשתמשים ב אמצעי תכנות בעיתי אחר ה- goto למשל אם יש לנו רובד עמוק של if –ים ומתרחש איזה שהוא צורך לצאת משם, אנחנו עשויים למצוא את עצמנו ממשים קוד נוסח

if (….)

  if (….)

    if (….)

      …..

       if ( disaster)

           goto out_of _here

 

……

 

out_of_here:

 

כלומר, מאחר ו-break לא עושה את מה שצריך, משתמשים פה ב-goto.

 

שימוש ב-longjmp יכול להגביל לתסריט שתואר לעיל רק שבמקום שאנחנו נמצאים ב-if מכונן, אנחנו נמצאים בקריאה מכוננת של פונקציות.

 

תסריט אחר של שימוש ב-longjmp הוא מימוש תפריט ראשי שאליו רוצים לחזור הוא מעין "תפריט ראשי" אליו צריך לחזור מכל מיני מקומות באופן לא כל כך צפוי, כמו קפריזות של משתמש.  זו דוגמא שנראה ממומשת כאן.

 

נניח שאנחנו ממשים תוכנית של תפריט ראשי המאפשרת למשתמש כל מיני אפשרויות ואנחנו רוצים לאפשר לו לאותת "לחזור לתפריט הראשי" בכל רגע או ע"י הכנסת האפשרות הזו כאופציה למשתמש או לאפשר חזרה כזו באופן יזום ע"י המשתמש למשל ע"י לחיצת Ctrl-C כאשר הוא מרגיש שהסתבך או פנה לכיוון לגמרי לא נכון.  למשל, רובנו מכירים במענים קוליים את המשפט "בכל רגע ניתן לחזור לתפריט הראשי ע"י לחיצת ...".  אופציות דומות קיימות ע"י לחיצת מקש מיוחד (למשל לחיצת Esc)  בתוכניות עיבוד נתונים, או אופציות כאלו בתפריטים פנימיים "לחזרה לתפריט ראשי לחץ ...").

 

המנגנון עצמו

 

  תוכניתן הרוצה לממש רעיון מסוג זה חייב "לשמר" את הנקודה שבה הוא רוצה לחזור אליה ע"י הפקודה setjmp ולבצע את ההסתעפות בפועל ע"י הפקודה longjmp.  ההכרזות על הפונקציות הללו הם

 

  #include <setjmp.h>

 

  int setjmp(jmp_buf env);

 void longjmp(jmp_buf env, int val);

 

 

הטיפוס jmp_buf הוא מערך רשומות בגודל 1. מי שרוצה להשתמש במנגנון, חייב להגדיר משתנה מהסוג הזה ולהעביר אותו (את הכתובת שלו למעשה) כפרמטר הן ל-setjmp והן ל-longjmp. במשתנה הזה נשמר ה"סביבה" של הנקודה אליו רוצים לחזור. הפונקציה setjmp משמרת את האינפורמציה והפונקציה longjmp משתמשת בו.  בכדי להבין איך  הפונקציות הללו עובדות צריך לדעת ברמת האסמבלי איך C משמר כתובות חזרה ומסגרות של משתנים במחסנית, ולא נכנס לכך כאן.

  המתכנת חייב לקרוא ל-setjmp בנקודה שאליו הוא רוצה לחזור.  ההסתעפות חזרה תמומש ע"י קריאה לפונקציה longjmp.  ברגע  הקריאה ל-longjmp  ההסתעפות  תמומש כ"חזרה" מ-setjmp. למנגנון יש שירות המאפשר למתכנת להבחין בין הקריאה המקורית ל-setjmp לבין ה"חזרה" ממנו כתוצאה מקריאה ל-longjmp: הקריאה המקורית ל-setjmp תחזיר תשובת פונקציה אפס, בעוד שכל "חזרה" מ-setjmp הנובעת מקריאה ל-longjmp תחזיר את ערך הפרמטר val שהועבר ל-logjmp בתנאי שהוא שונה מאפס.

 

הפרמטר buf_env  שמבחינה מעשית חייב להיות רשומה גלובלית המכיל למעשה את המידע הנחוץ לבצע את החזרה.  אם רוצים, אפשר לממש מספר "נקודות חזרה" כאלו בתוכנית אחת אם מגדירים מספר משתנים גלובליים כאלו.

 

  במידה ואנחנו רוצים לבצע הסתעפות כזו באמצעות מנגנון הסיגנלים עלינו להשתמש ברוטינות דומות sigsetjmp ו-siglongjmp שמשמרות. משחזרות לא רק את מצב המחסנית אלא גם את הגדרות התגובה לסיגנלים.  ההכרזות על הרוטינות הללו הם

 

       int sigsetjmp(sigjmp_buf env, int savesigs);

       void siglongjmp(sigjmp_buf env, int val);

 

המתכנת חייב לציין ערך שונה מאפס לפרמטר savesigs בכדי ל"אותת" שהוא מעוניין ש-sigsetjmp ו-siglongjmp ישמרו וישחררו בהתאמה את הגדרות התגובה לסיגנלים.

 

להלן תוכנית דוגמא שמממשת "חזרה לתפריט ראשי" כאופציה בתפריטים פנימיים.

 

/* myutils.h - include declarations of utilities */

 

extern void fatal();

extern void syserr();

 

/* myutils.c - service utility routines */

 

#include <stdio.h>

#include <errno.h>

 

extern int errno, sys_nerr;

 

/*  print system error messge and  terminate */

void syserr(msg)

char *msg;

{

 perror(msg);

 exit(1);

} /* syserr */

 

/*  print application error messge and  terminate */

void fatal(msg)

char *msg;

{

 fprintf(stderr, "Application program error: %s.", msg);

 exit(2);

}

 

/* jmpdemo1.c - Program a main menu to return to  */

 

#include <stdio.h>

#include <setjmp.h>

#include "defs.h"

 

 

static jmp_buf jmpbuf;

 

void ask_return()

{

  char str[80];

 

     puts("Return to main menu (y/n)?");

     fgets(str,80, stdin);

 

     if (str[0] == 'y')

         longjmp(jmpbuf,1);

 

} /* ask_return */

 

void ha_ha_loop()

{

 int i, c;

 

 while(1)

  {

     for(i=0; i < 3; i++)

     {

       puts("Ha Ha Ha");

       sleep(3);

     } /* for */

    ask_return();

  } /* while */

 

} /* ha_ha_loop */

 

void dollar_loop()

{

 

 int i;

 

   while(1)

    {

     for(i=0; i < 3; i++)

      {

        puts("$$$$$$$$$ ");

        sleep(3);

      } /* for */

 

      ask_return();

    } /* while */

 

} /* dollar_loop */

 

void mainloop()

{

  char answer[80];

 

  while (1)

  {

    puts("Press 1 for Ha Ha Ha.");

    puts("Press 2 for $$$$$$$$.");

    puts("Press 3 for to quit.");

 

    fgets(answer,80, stdin);

 

    switch (answer[0])

    {

      case '1':

       ha_ha_loop();

      case '2':

       dollar_loop();

      case '3':

       return;

    } /* switch */

  } /* while */

 

} /* mainloop */

 

 

int main()

{

  if (setjmp(jmpbuf) == 0)

   puts("Main Menu:");

  else

    puts("Returning to main menu....");

 

 

 mainloop();

 

 return 0;

 

} /* main */

 

 

הידור ופלט ריצה:

% cc jmpdemo1.c myutils.c

% Main Menu:

Press 1 for Ha Ha Ha.

Press 2 for $$$$$$$$.

Press 3 for to quit.

1

Ha Ha Ha

Ha Ha Ha

Ha Ha Ha

Return to main menu (y/n)?

n

Ha Ha Ha

Ha Ha Ha

Ha Ha Ha

Return to main menu (y/n)?

y

Returning to main menu....

Press 1 for Ha Ha Ha.

Press 2 for $$$$$$$$.

Press 3 for to quit.

3

%

 

 

התוכנית הבאה היא גרסה שבה החזרה לתפריט הראשי נעשה ע"י Ctrl-C והסיגנל SIGINT שנובעת ממנו:

 

/* jmpdemo2.c - Program ^C to interrupt an activity, not the

entire program.  */

 

#include <stdio.h>

#include <setjmp.h>

#include <signal.h>

#include <setjmp.h>

#include "defs.h"

 

 

static sigjmp_buf jmpbuf;

 

void ha_ha_loop()

{

   while(1)

     {

       puts("Ha  Ha  Ha ");

       sleep(3);

     } /* while */

 

} /* ha_ha_loop */

 

void dollar_loop()

{

   while(1)

    {

      puts("$$$$$$$$$ ");

      sleep(3);

    } /* while */

 

} /* dollar_loop */

 

void mainloop()

{

  char answer[80];

 

  while (1)

  {

    puts("Press 1 for Ha Ha Ha.");

    puts("Press 2 for $$$$$$$$.");

    puts("Press 3 for to quit.");

 

    fgets(answer, 80, stdin);

 

    switch (answer[0])

    {

      case '1':

       ha_ha_loop();

      case '2':

       dollar_loop();

      case '3':

       return;

    } /* switch */

  } /* while */

 

} /* mainloop */

 

 

static void jumper() /* abandon internal activity, resume mainloop */

{

  siglongjmp(jmpbuf,1);

  fatal("Longjmp returned");

}

 

 

int main()

{

 

  if (signal(SIGINT, jumper) == SIG_ERR)

      syserr("signal");

  if (sigsetjmp(jmpbuf,1) != 0)

    puts("....Interrupt");

 

 mainloop();

 

 return 0;

 

} /* main */

 

הידור ופלט ריצה:

% cc jmpdemo2.c myutils.c

% ./a.out

Press 1 for Ha Ha Ha.

Press 2 for $$$$$$$$.

Press 3 for to quit.

2

$$$$$$$$$

$$$$$$$$$

$$$$$$$$$

....Interrupt

Press 1 for Ha Ha Ha.

Press 2 for $$$$$$$$.

Press 3 for to quit.

1

Ha  Ha  Ha

Ha  Ha  Ha

....Interrupt

Press 1 for Ha Ha Ha.

Press 2 for $$$$$$$$.

Press 3 for to quit.

3

%