Allegro.cc - Online Community

Allegro.cc Forums » Allegro Development » 4.3 error handling

This thread is locked; no one can reply to it. rss feed Print
4.3 error handling
Kitty Cat
Member #2,815
October 2002
avatar

Because SF is giving problems with the mailing list, and I could always use more feedback, I thought I'd toss up my idea about error handling in 4.3 here.

My initial email follows (with [code] blocks added for your sanity :P)

My original message said:

While playing around with the sound code again, I thought it'd be nice if it had some error handling abilities. I came up with this in about 15 to 30 minutes. I was just wondering if this is a good direction to put it in, or if you wanted something else. Here's the main code:

1/****** Quick'n'dirty error API ******/
2 
3#include <setjmp.h>
4#include <signal.h>
5#include <stdio.h>
6 
7/* public */
8typedef enum {
9 AL_NO_ERROR = 0,
10 AL_INVALID_PARAM = 1,
11 AL_INVALID_OBJECT = 2,
12 
13 AL_GENERIC_ERROR = 256
14} AL_ERROR_ENUM;
15 
16AL_ERROR_ENUM al_get_last_error();
17void al_enable_error_throws();
18void al_disable_error_throws();
19void al_error_throw(AL_ERROR_ENUM code);
20AL_ERROR_ENUM al_get_last_error();
21 
22#define al_error_try \
23if(!setjmp(*_al_get_next_jump_point()) \
24{
25 
26#define al_error_catch \
27 _al_clear_last_error_point(); \
28} \
29else
30 
31 
32/* internal */
33jmp_buf *_al_get_next_jump_point();
34void _al_clear_last_error_point();
35 
36 
37/* source only */
38static __thread AL_ERROR_ENUM _al_errcode;
39 
40static __thread size_t _al_num_jump_points;
41static __thread jmp_buf _al_jump_points[16];
42 
43static __thread bool _al_throw_errors = true;
44 
45 
46static void _unhandled_allegro_error()
47{
48 /* This would obviously use system-specific methods (popup box, whatever),
49 with more proper cleanup */
50 fprintf(stderr, "An unhandled error (code: %d) was caught!\nAborting\n",
51al_get_last_error());
52 raise(SIGABRT);
53}
54 
55static jmp_buf *get_last_jump_point()
56{
57 if(_al_num_jump_points == 0)
58 _unhandled_allegro_error();
59 return &_al_jump_points[--_al_num_jump_points];
60}
61 
62jmp_buf *_al_get_next_jump_point()
63{
64 if(_al_num_jump_points >= 16)
65 al_error_throw(AL_GENERIC_ERROR);
66 return &_al_jump_points[_al_num_jump_points++];
67}
68 
69void _al_clear_last_error_point()
70{
71 if(_al_num_jump_points == 0)
72 _unhandled_allegro_error();
73 --_al_num_jump_points;
74}
75 
76void al_enable_error_throws()
77{
78 _al_throw_errors = true;
79}
80 
81void al_disable_error_throws()
82{
83 _al_throw_errors = false;
84}
85 
86void al_error_throw(AL_ERROR_ENUM code)
87{
88 _al_errcode = code;
89 if(_al_throw_errors)
90 longjmp(*get_last_jump_point(), code);
91}
92 
93AL_ERROR_ENUM al_get_last_error()
94{
95 AL_ERROR_ENUM err = _al_errcode;
96 _al_errcode = AL_NO_ERROR;
97 return err;
98}
99 
100/*************/

C99 defines __thread, and Windows has a similar specifier. So as you can see, it uses TLS, meaning seperate threads erroring won't conflict with each other. To use it, you have these methods:

** Method #1 (the lazy/newbie approach):

sound_driver = al_audio_init_driver(NULL);
/* Allegro will have SIGABRT'd by now if there was a problem,
   we can continue assuming everything's okay */
..more game code..

** Method #2 (the C++ approach):

al_error_try (
   sound_driver = al_audio_init_driver(NULL);
   ..more related code..
}
al_error_catch {
   AL_ERROR_ENUM code = al_get_last_error();
   /* We only handle invalid parameter errors */
   if(code != AL_INVALID_PARAM)
      al_error_throw(code);
   show_my_error("Could not init sound!");
}
..more game code..

** Method #3 (the C approach):

al_error_disable_throws();

sound_driver = al_audio_init_driver(NULL);
if(!sound_driver)
{
   AL_ERROR_ENUM code = al_get_last_error();
   if(code != AL_INVALID_PARAM)
   {
      al_error_enable_throws();
      al_error_throw(code);
   }
   show_my_error("Could not init sound!");
}

al_error_enable_throws();
..more game code..

So, what do you think? I kinda like it, but it may be too hacky for some people's tastes. I believe it's logically sound though, so it shouldn't cause problems..

After this, Elias responded:

His response said:

I think, for C code, those #defines are really ugly.. at least to me they do look ugly. If I wanted to use #2, then I would use C++, not C.

And actual C++ users wouldn't benefit from setjmp/longjmp and those #defines, since real exceptions should be raised for C++. I'd say, the best idea for C++ is a wrapper, where al_sound::init would raise an exception.

The TLS error code and NULL return is all that seems needed to me for C. For the #1 approach, we could have a flag to al_init, like al_init(ABORT_ON_ERROR). And then Allegro would automatically abort on error - just like in your code.

The #3 approach looks much simpler than the #2 approach to me anyway, so I don't think anything is wrong with it :)

One idea similar to setjmp/longjmp might be an error callback, something like:
void al_set_error_callback(void *(user_error_handler)(AL_ERROR error));
Users could set it to their own function, and it gets called with the error code. That way, it would be possible for users to use setjmp/longjmp inside the callback if they so wish (I think at least, I never fully could grasp how setjmp/longjmp works), or also raise C++ exceptions (unless, C++ exceptions can't be raised from a C callback, I remember something in that direction). Or they could simply have abort in there, then we wouldn't even need an extra flag for it in al_init.

To which I responded:

My response said:

[quote He]I think, for C code, those #defines are really ugly.. at least to me they do look ugly. If I wanted to use #2, then I would use C++, not C.

But Allegro doesn't use C++. Since Allegro is going to use C (AFAIK), or at least expose a C-only API, it wouldn't be able to properly throw real exceptions.

Besides, with the way that code is, you can use any of the three at any time. Don't want to use the try/catch defines? Then just call al_error_disable_throws() once at the beginning of your program and don't think anything of them (just be sure to check for failure return codes). Too lazy to do your own error checking? Leave throws enabled and don't worry about the try and catch defines.

There's many times I wish I could just execute a string of commands and not have to check the return code of each one individually. But I'd still like to be able to handle the errors manually myself, without having to use a seperate callback function. Something like:

1al_error_try {
2 audio_driver = al_audio_init_driver(NULL);
3 
4 main_voice = al_voice_create(audio_driver, 44100, AL_AUDIO_16_BIT_INT,
5AL_AUDIO_4_CH, 0);
6 al_voice_get_long(main_voice, AL_AUDIO_FREQUENCY, &voice_freq);
7 al_voice_get_enum(main_voice, AL_AUDIO_CHANNELS, &channels);
8 
9 main_mixer = al_mixer_create(voice_freq, AL_AUDIO_32_BIT_FLOAT, channels);
10 
11 al_voice_attach_mixer(main_voice, main_mixer);
12}
13al_error_catch {
14 AL_ERROR code = al_get_last_error();
15 show_my_error("Error initializing sound!\n%s\n", al_get_error_string());
16 if(no_sound_is_fatal)
17 al_error_throw(code);
18}

Having to check each function in that try block manually and basically call the same error-handling code would be very wasteful. And putting those few lines into a seperate function to install as a callback temporarilly would not be very clean (plus the trouble of being able to save and restore a previous callback, if one was installed, and optionally continue from outside of the "error zone").

NOTE: The al_get_error_string function I threw in up there isn't in the code I posted, but it would be useful to have a human-readable string of the problem along with a simple error code.

He said:

And actual C++ users wouldn't benefit from setjmp/longjmp and those #defines, since real exceptions should be raised for C++. I'd say, the best idea for C++ is a wrapper, where al_sound::init would raise an exception.

A C++ wrapper could still raise real exceptions:

al_audio::init()
{
   al_error_try {
      driver = al_audio_init_driver(NULL);
   }
   al_error_catch {
      throw al_get_last_error();
   }
}

That also has the benefit of leaving the real throwing in C++, so you needn't have to worry about C++ exceptions through C.

He said:

The TLS error code and NULL return is all that seems needed to me for C.

Then that's all you have to use. Just call al_error_disable_throws() and do it the C way.

He said:

For the #1 approach, we could have a flag to al_init, like al_init(ABORT_ON_ERROR). And then Allegro would automatically abort on error - just like in your code.

My code has the benefit of being able to turn such aborts on and off at will. Like, say, if you want to init video without doing specific error checking, but then check the errors while initializing sound. It's also transparent, so you could put in a single try/catch around the whole initialization section without having to change the initialization code itself.

He said:

The #3 approach looks much simpler than the #2 approach to me anyway, so I don't think anything is wrong with it :)

Then, again, you're free to use it. :) Nothing's forcing you to have to do it the first or second way.

He said:

One idea similar to setjmp/longjmp might be an error callback, something like:

void al_set_error_callback(void *(user_error_handler)(AL_ERROR error));

It might be a good idea to use that anyway, for uncaught errors (eg. if an error is generated and either throws are disabled, or not within a try block). The error handler can then return non-0 to tell Allegro to clean up and SIGABRT, or 0 to tell it to keep going. Though it probably shouldn't be allowed to keep going if throws are enabled.

He said:

Or they could simply have abort in there, then we wouldn't even need an extra flag for it in al_init.

Allegro knows best how to clean itself up (as long as it can keep track of its
own state, anyway). There should always be a way to tell Allegro to exit
cleanly now, and raise a signal for debuggers.

That said, however, I do see a small problem with my code. You wouldn't be
able to jump out from the try block (via goto, a caught exception, another
longjmp, etc) and continue using Allegro code, or else the code will throw to
the wrong setjmp point later on. Though I don't think it's that big of a deal
to simply note that you can't do that, if something like this is used. You
could always throw a custom allegro error in the try block and check for it
in the catch block, where you can jump out however you want.
</quote>
That second message hasn't made it to the SF mailing list yet (despite the fact that I sent it over 13 hours ago and counting :-X).

--
"Do not meddle in the affairs of cats, for they are subtle and will pee on your computer." -- Bruce Graham

Ron Novy
Member #6,982
March 2006
avatar

Here is a simple, thread safe and easy to use C way that I believe might be a better compromise.
This is actually 3 files mixed together (except.h, except.c and extry.c) but it will compile and run as is (for testing purposes of course) ;)

1/*
2 * I was inspired by the discussion in the mailing list and
3 * thought I'd throw ;) this in. It might be a good compromise
4 * that everyone may be able to agree on and may turn out to be
5 * very useful.
6 *
7 * Some proposed exception handling.
8 * by Ron Novy
9 *
10 * Thanks to Chris and the others for there inspiration.
11 *
12 * This makes use of __FILE__ and __LINE__ to give useful information to the
13 * user about what went wrong. It is also thread safe and doesn't require any
14 * extra memory or unnecessary function calls to try and emulate the C++ style
15 * of exception handling. It's very simple and easy to use too.
16 *
17 * The only cons I can think of are that it's not real exception handling and
18 * its not C++ style. But as this is C, are those really cons?
19 *
20 * this is 3 files mixed together (except.h, except.c and extry.c) but
21 * it will compile and run as is (for testing ;)
22 */
23/* ----=== except.h start ===---- */
24#include <allegro.h>
25 
26 
27/* extended version */
28#define AL_TRY_EX(condition, str, exception_handler) \
29 { \
30 if (condition) \
31 exception_handler(str, __FILE__, __LINE__); \
32 }
33 
34 
35/* default version */
36#define AL_TRY(condition, str) \
37 AL_TRY_EX(condition, str, al_default_exception)
38 
39 
40/* non-fatal GUI version */
41#define AL_TRY_GUI(condition, str) \
42 AL_TRY_EX(condition, str, al_gui_exception)
43 
44 
45/* fatal GUI version */
46#define AL_TRY_FGUI(condition, str) \
47 AL_TRY_EX(condition, str, al_gui_fatal_exception)
48 
49/* ----=== except.h end ===---- */
50 
51 
52 
53/* ----=== except.c start ===---- */
54//#include <allegro.h>
55 
56/* al_default_exception:
57 * This is used as a default exception handler. This default implies a fatal
58 * exception and will exit the program.
59 */
60void al_default_exception(AL_CONST char *str, AL_CONST char *file, int line)
61{
62 set_gfx_mode(GFX_TEXT, 0, 0, 0, 0);
63 allegro_message("Exception in File '%s' at Line: %d\n%s\n",
64 file, line,
65 ((str) ? str : "Allegro: Unknown exception occurred!") );
66 exit(0);
67}
68 
69 
70 
71/* al_gui_exception:
72 * This can be used as an alternative to the default exception handler.
73 * This version can be used when the program is in a graphics mode and you can
74 * access Allegro's GUI system.
75 */
76void al_gui_exception(AL_CONST char *str, AL_CONST char *file, int line)
77{
78 int r;
79 char tmp[512];
80 
81 uszprintf(tmp, sizeof(tmp),
82 "Exception in file '%s' at line: %d.",
83 file, line);
84 
85 r = alert(tmp,
86 ((str) ? str : "Allegro: Unknown exception occurred!"),
87 "It may be possible to continue (but at your own risk).",
88 " &Continue ", " &Exit ", 'c', 'e');
89 
90 if (r == 2)
91 exit(1);
92}
93 
94 
95 
96 
97/* al_gui_fatal_exception:
98 * This is the same as al_gui_exception except ;) that it is always fatal.
99 */
100void al_gui_fatal_exception(AL_CONST char *str, AL_CONST char *file, int line)
101{
102 char tmp[512];
103 
104 uszprintf(tmp, sizeof(tmp),
105 "Fatal exception in file '%s' at line: %d.",
106 file, line);
107 
108 alert(tmp,
109 ((str) ? str : "Allegro: Unknown fatal exception occurred!"),
110 "The program must be shutdown.",
111 " &Exit ", NULL, 'e', 'x');
112}
113 
114/* ----=== except.c end ===---- */
115 
116 
117/* ----=== extry.c start ===---- */
118/*
119 * Example program for simple Allegro exception handling.
120 * by Ron Novy
121 *
122 * Try this, that, and everything... Try your patience ;)
123 * This example may go too far as it uses AL_TRY on everything including
124 * install_keyboard, install_mouse, ..., but it gives a good example of
125 * how it works and other ways you can use it.
126 *
127 */
128 
129 
130#include <allegro.h>
131 
132 
133 
134/* simple function to just get into any gfx mode.
135 * Returns TRUE if a mode was set or FALSE if an error occurred.
136 */
137int my_set_gfx_mode(int width, int height)
138{
139 int desk_depth = desktop_color_depth();
140 
141 if (desk_depth != 0) {
142 set_color_depth(desk_depth);
143 
144 if (set_gfx_mode(GFX_AUTODETECT, width, height, 0, 0) == 0)
145 return TRUE;
146 }
147 
148 set_color_depth(32);
149 if (set_gfx_mode(GFX_AUTODETECT, width, height, 0, 0) == 0)
150 return TRUE;
151 
152 set_color_depth(24);
153 if (set_gfx_mode(GFX_AUTODETECT, width, height, 0, 0) == 0)
154 return TRUE;
155 
156 set_color_depth(16);
157 if (set_gfx_mode(GFX_AUTODETECT, width, height, 0, 0) == 0)
158 return TRUE;
159 
160 set_color_depth(15);
161 if (set_gfx_mode(GFX_AUTODETECT, width, height, 0, 0) == 0)
162 return TRUE;
163 
164 set_color_depth(8);
165 if (set_gfx_mode(GFX_AUTODETECT, width, height, 0, 0) == 0)
166 return TRUE;
167 
168 return FALSE;
169}
170 
171 
172 
173void my_gui_exception_handler(AL_CONST char *str, AL_CONST char *file, int line)
174{
175 char tmp[1024]; /* 1024 ? Just make it work Bill. */
176 
177 uszprintf(tmp, sizeof(tmp),
178 "My gui exception in file '%s' at line %d.",
179 file, line);
180 
181 alert(tmp, str, "Just click OK to continue.", " &OK ", NULL, 'o', 'k');
182}
183 
184 
185 
186/* Custom define to make custom exceptions easier */
187#define MY_AL_TRY_GUI(condition, str) \
188 AL_TRY_EX(condition, str, my_gui_exception_handler)
189 
190 
191 
192int main(int argc, char *argv[])
193{
194 int x = 0, y = 1;
195
196 AL_TRY(allegro_init(), "Could not initialize allegro!")
197 AL_TRY(install_timer(), "Could not install allegro timers!")
198 AL_TRY(install_keyboard(), "Could not install allegro keyboard driver!")
199 AL_TRY(install_mouse() == -1, "Could not install allegro mouse driver!")
200 
201 /* try to set a gfx mode */
202 AL_TRY(!my_set_gfx_mode(640, 480), allegro_error)
203 
204 
205 /* we're in graphics mode so try the gui exception handler */
206 AL_TRY_GUI(x != y, "'x' is not equal to 'y'.")
207 
208 
209 /* make x equal to y */
210 x = y;
211 
212 /* now try our own little handler we defined above */
213 MY_AL_TRY_GUI(x == y, "'x' is equal to 'y'.")
214 
215 
216 /* now show the unknown exception message */
217 AL_TRY(x == y, NULL)
218 
219 /* this next bit of code should never execute */
220 alert("We made it?",
221 "How on earth did that happen?",
222 "This code should be unreachable in extry.c",
223 "&OK", NULL, 'o', 'k');
224 
225 return 0;
226}
227END_OF_MAIN()
228 
229/* ----=== extry.c end ===---- */

Any way check it out, compile it then let me know what you think.

[EDIT] I attached a new version that does a lot of ascii->current text format conversions. I'm not sure if they would be necessary but it works just as well.

----
Oh... Bieber! I thought everyone was chanting Beaver... Now it doesn't make any sense at all. :-/

kazzmir
Member #1,786
December 2001
avatar

I dont really see how that helps. What if you want to perform some cleanup at the end of a block of code?

FILE * f = fopen( "foobar" );
try{
  method1(f); // could throw error
  method2(f); // could throw error
  method3(f); // could throw error
  return f;
} catch ( whatever ){
   fclose( f );
}
return NULL;

How can you emulate that by putting a TRY{} thing around each statement? The point is to put all of the error code in one place.

Kitty Cat
Member #2,815
October 2002
avatar

There seems to be a question over whether setjmp/longjmp is actually safe to use with local variables lying around. As I understand it, local variables are supposed to be on the stack, and can be put into registers for speed optimizations. Since they're supposed to be on the stack, I'd think the compiler would be smart enough to not do anything dangerous around setjmp/longjmp that would compromise that (eg. copying vars from registers into the proper stack location before longjmp completes, and having the vars reloaded from the stack into the proper registers before setjmp returns non-0). Does anyone have any info that might clear this up?

Also another idea might be to include a C++ exception throwing callback for errors (eg. an error occurs, Allegro calls a callback, etc), and have an al_init option to use it by default. However, the issue with that seems to be thus:

  • You need -fexceptions to be able to throw exceptions from C++, through C code, and back to C++, using GCC. Do other compilers support a similar option?

  • MSVC vs GCC. Would throwing an exception from GCC-generated code through an MSVC DLL (or vice-versa) cause problems?

</li>

--
"Do not meddle in the affairs of cats, for they are subtle and will pee on your computer." -- Bruce Graham

Matthew Leverton
Supreme Loser
January 1999
avatar

I've always been suspicious of libraries that try to over extend the language into something it's not. I think if someone really wants exception handling, they should use C++ and a wrapper over Allegro that provides that. And yes, I realize that doesn't help out people writing core Allegro routines.

But if your proposed system did work on every compiler under all situations, I wouldn't see any harm in it. I'd still think it didn't belong, but I'm not the one writing code...

Peter Wang
Member #23
April 2000

Quote:

There seems to be a question over whether setjmp/longjmp is actually safe to use with local variables lying around.

It's not a question. Try this program:

1#include <setjmp.h>
2#include <stdio.h>
3 
4jmp_buf env;
5 
6int f(void)
7{
8 int c = 0;
9 if (setjmp(env)) {
10 return c;
11 } else {
12 c++;
13 longjmp(env, 1);
14 }
15}
16 
17int main(void)
18{
19 printf("%d\n", f());
20 return 0;
21}

On my system, compiling with `gcc -O0' prints "0". Compiling with `gcc -O' or above prints "1". You really do need to declare `c' as `volatile'.

Carrus85
Member #2,633
August 2002
avatar

Of course, if allegro were to use c++, this whole chain of arguments would be a nonissue. (Or better yet, the core allegro code is in C, then a C++ wrapper is written to interact with the "core" (Depending on design, one could even write a C wrapper as well that abstracts some of the technicalities away from the core)). So, you basically make the core library require as verbose of arguments as possible (don't assume things based on previous settings and such unless the function's nature requires it), and write wrappers to abstract things away from the complicated core library (so, for example, in the core library, you might only have one version of create_bitmap, that takes width, height, and color depth arguments. The wrapper library can handle global colordepths externally to the core library).

Ron Novy
Member #6,982
March 2006
avatar

I'm not sure if setjump is thread-safe though. Imagine that multiple threads tried to use setjump at the same time. The results would then be undefined. After my previous post I searched through allegro.c and discovered that my code was very similar to allegro's ASSERT code. Maybe there could be a solution that would jump using a #define macro instead of calling a function directly.

1#define AL_TRY(condition, jump) \
2 if (!(condition)) \
3 goto jump;
4 
5int myfunc(int stuff)
6{
7 /* instead of assert which compiles to nothing in non-debug versions */
8 AL_TRY(stuff, myfunc_exception)
9 return TRUE; /* All is well */
10 
11myfunc_exception:
12 my_exception_message_proc("An exception occurred!", __FILE__, __LINE__);
13 return FALSE;
14}

[edit]
It's simple and pretty much the same is programming without AL_TRY but this way you get cleaner looking code with the c++ style you might be looking for.

----
Oh... Bieber! I thought everyone was chanting Beaver... Now it doesn't make any sense at all. :-/

Thomas Fjellstrom
Member #476
June 2000
avatar

Quote:

I'm not sure if setjump is thread-safe though.

Which is why the OP suggested TLS (thread local storage, aka __thread) allong with set/longjmp.

--
Thomas Fjellstrom - [website] - [email] - [Allegro Wiki] - [Allegro TODO]
"If you can't think of a better solution, don't try to make a better solution." -- weapon_S
"The less evidence we have for what we believe is certain, the more violently we defend beliefs against those who don't agree" -- https://twitter.com/neiltyson/status/592870205409353730

Kitty Cat
Member #2,815
October 2002
avatar

One of the things I was going for was a clean code execution path. So you could run some code, if it errors, stop and run some cleanup code, then continue running code. eg:

al_audio_init();
al_create_voices();
if there was an error
{
   al_audio_close();
   warn("no sound!");
}

more_code_here();

Of course, if the first function fails, you don't want to run the second. But in either case, you want to run the same error handling code, then continue on whether there was an error or not.

As well, for newbies or someone who just wants to quickly get something down, you're not going to want or know to tediously check every function for errors, so having a simple way to automatically catch errors for you, at least in the beginning, would be very helpful.

--
"Do not meddle in the affairs of cats, for they are subtle and will pee on your computer." -- Bruce Graham

Go to: