Return values

From Allegro Wiki
Jump to: navigation, search

A return value is a value that is returned by a function in C, C++, and nearly every other programming language known to man. While functions usually accept input through parameters, they usually return output through a return value[1]. Modern languages like C++, C#, and Java support a mechanism known as exception handling to handle errors in special blocks known as catch blocks. Older languages, like C, however, lack such a feature and normally rely on special return values to indicate an error occurring in a called function.

Why Are Return Values So Important?

In addition to returning desired outputs, return values in C are normally used to also indicate errors. This is certainly true in the Allegro library. Nearly every function that can fail, which is nearly every function, has a defined set of return values to indicate success or failure. If a function call fails that another function call depends on then it's likely that the dependent call is going to fail too, or worse, it could do something unintended or undefined instead. If that happens then your program is no longer doing what it's supposed to do and a program that isn't doing what it's supposed to do is useless (and can even be dangerous).

Often times C libraries are designed to be as efficient as possible. Many modern languages include a ton of run-time checks, and encourage the user to do a number of run-time checks, to ensure that things are working as expected. C programs often assume that these checks are done already and take a more efficient, laid back approach. For example, instead of every single function checking that a pointer is not NULL (AKA 0), many will just assume that the calling programmer checked the pointer before passing it in. What this means is that if something in your program fails and you don't acknowledge it properly there is a good chance that your program will later crash; or worse, do something really dangerous, like corrupt its memory.

Example

We often see the following initialization code posted to the forum by beginners trying to figure out why their program doesn't work.

   #include <allegro.h>
   
   int main(int argc, char * argv[])
   {
       allegro_init();
       install_keyboard();
       set_color_depth(24);
       set_gfx_mode(GFX_AUTODETECT, 800, 600, 0, 0);
   
       BITMAP * buffer = create_bitmap(800, 600);
   
       return 0;
   }

What's wrong with it? None of the return values are being checked. If allegro_init fails then nearly every other Allegro routine is going to fail too. If install_keyboard fails (unlikely, but still technically possible) then your program won't get any keyboard input (the user won't be able to interact with it). If set_gfx_mode fails then you probably won't have a window, screen, numerous things won't work properly, and you won't receive input either. If create_bitmap fails and you later try to use the BITMAP that it was supposed to create then you'll dereference the memory address 0x0, which is never valid, and the kernel will terminate your program as a result (with no indication of why).

How would you fix this code? By checking return values! Let's start with allegro_init. We want to check the return value, but how do we know what to check for? Look in the manual! If we take a look at the manual entry for allegro_init, we'll see this:


   Macro which initialises the Allegro library. This is the same thing as calling install_allegro(SYSTEM_AUTODETECT, &errno, atexit).


Uh oh! That doesn't tell us what the return value is! Yes it does, but indirectly. Since it's the same as a call to install_allegro, we need to find out what install_allegro returns! Back to the manual! Ah, here we go:


   int install_allegro(int system_id, int *errno_ptr, int (*atexit_ptr)());
   This function returns zero on success and non-zero on failure (e.g. no system driver could be used).


Eeek, no system driver could be used sounds kind of serious. We probably don't want our program to keep running if that's the case. In fact, it's fairly common practice in game programming to just exit when an error occurs, especially if you don't know how to work around the error. It's important though to first display a message to the user explaining what happened so they know what's wrong. Note that install_allegro, and by extension, allegro_init, return a value of type int which will be zero on success and non-zero on failure[2]. That's a fairly common practice in Allegro and even in C. Now that we know what to look for and how we're going to handle it, let's write the fix:

   int ret = allegro_init();
   
   // allegro_init failed...
   if(ret != 0)
   {
       fprintf(stderr, "Failed to initialize Allegro! %s\n", allegro_error);
       exit(1);
   }

What have we done here? First, we stored the return value from allegro_init in a variable called ret. Then we compare ret to 0. If ret is not 0 then it means allegro_init failed. In that case, we use the fprintf function (from stdio.h) to print an error message to the user, including allegro_error which is a string that Allegro fills with a descriptive message for some of its errors. Finally, we call exit (from stdlib.h), which exits the program immediately. Generally this should be safe because the operating system will clean up any resources (memory and files and such) that we were using automatically.

You could just about copy and paste that if block above to use for most checks. In fact, if you wanted to, you could wrap it into a function to save you some typing:

   void check_ret(const int ret, char * msg, ...)
   {
       if(ret != 0)
       {
           va_list ap;
           va_start(ap, msg);
   
           vfprintf(stderr, msg, ap);
           fputs("\n", stderr);
   
           va_end(ap);
   
           exit(1);
       }
   }

The ... and va_ parts are called variadic parameters (from stdarg.h)... I used them so that you could use a printf-style string and arguments, but you don't have to if you don't want to. I also used the vfprintf function instead of fprintf because instead of accepting a variable number of arguments, it accepts a va_list, so that we can pass our variable arguments on to it. If you don't understand this paragraph then just skip it and replace all of that stuff with fputs, which prints a single unformatted string (so you can't pass allegro_error to it).

Then, to use it, you would do something like this:

   check_ret(allegro_init(), "Failed to initialize Allegro! %s", allegro_error);

Now those 4 lines of code are reduced to only one! You could use the same basic call for other Allegro routines that return non-zero on failure, like install_keyboard and set_gfx_mode. Be sure to change the message part though to describe what actually failed (e.g., "Failed to install keyboard!" or "Failed to set the graphics mode!").

Memory Allocation

A notable exception to the guideline for non-zero meaning failure is when a function is supposed to create a structure for you and return a pointer to it. A perfect example is create_bitmap. With create_bitmap, which returns a BITMAP * (pointer to a BITMAP), 0 indicates a "null pointer", which is used to indicate that no memory is allocated for it at all. A non-zero value indicates that it succeeded and the value is a pointer to the created BITMAP. In this case, the pattern changes. Instead of exiting when the return value is non-zero, you want to exit when the return value is zero.

Here's another example function that could handle those cases:

   void check_ptr(void * const ptr, char * msg, ...)
   {
       if(ptr == 0)
       {
           va_list ap;
           va_start(ap, msg);
   
           vfprintf(stderr, msg, ap);
           fputs("\n", stderr);
   
           va_end(ap);
   
           exit(1);
       }
   }

To use it, you would do something like this:

   BITMAP * buffer = create_bitmap(800, 600);
   
   check_ptr(buffer, "Failed to create screen buffer! Out of memory?");

Full Initialization Example "Fixed"

   #include <allegro.h>
   #include <stdarg.h>
   #include <stdio.h>
   #include <stdlib.h>
   
   void check_ptr(void * const, char *, ...);
   void check_ret(const int, char *, ...);
   
   int main(int argc, char * argv[])
   {
       check_ret(
               allegro_init(),
               "Failed to initialize Allegro! %s",
               allegro_error);
   
       check_ret(
               install_keyboard(),
               "Failed to install keyboard! %s",
               allegro_error);
   
       set_color_depth(24);
   
       check_ret(
               set_gfx_mode(GFX_AUTODETECT, 800, 600, 0, 0),
               "Failed to set graphics mode! %s",
               allegro_error);
   
       BITMAP * buffer = create_bitmap(800, 600);
   
       check_ptr(
               buffer,
               "Failed to create screen buffer! Out of memory?");
   
       // ...game here...
   
       destroy_bitmap(buffer);
   
       return 0;
   }
   
   void check_ptr(void * const ptr, char * msg, ...)
   {
       if(ptr == 0)
       {
           va_list ap;
           va_start(ap, msg);
   
           vfprintf(stderr, msg, ap);
           fputs("\n", stderr);
   
           va_end(ap);
   
           exit(1);
       }
   }
   
   void check_ret(const int ret, char * msg, ...)
   {
       if(ret != 0)
       {
           va_list ap;
           va_start(ap, msg);
   
           vfprintf(stderr, msg, ap);
           fputs("\n", stderr);
   
           va_end(ap);
   
           exit(1);
       }
   }

Thanks! Now I Can Just...

...Wrap all of your Allegro calls in my example functions and not have to read the manual? Not quite. As I said earlier, not all functions use non-zero values to indicate failure. In fact, install_mouse uses -1 to indicate an error; 0 means the mouse is already installed, and a positive return value indicates the number of mouse buttons. So you can never just assume that any C function works a particular way. The author of that function will most likely try to make the successful use case as convenient as possible, and will tailor its error reporting mechanism around that. Be sure to read the manual entry for every function that you're not familiar with! If a particular entry is vague or you don't understand what it means then ask for help!

What About Allegro 5?

All of the examples so far have been based on Allegro 4. Allegro 5 has just been released so I guess it's worth noting that, yes, you are still expected to check return values in Allegro 5! It's just as important as it is with Allegro 4. Be sure to check the manual corresponding to your version of the library because while allegro_init from Allegro 4 returned non-zero to indicate an error, al_init from Allegro 5 returns a bool instead, with false (AKA 0) on failure! If in doubt, refer to the manual for any library that you're using.

This Is Just For Show Though, Right?

You may be thinking to yourself that this isn't really necessary and those functions will never fail so you don't have to bother checking. Unfortunately, a lot of beginners and even some naive intermediates think this way. Whether or not you predict a function call failing, if it can ever fail then you should handle it. A lot of people post to the forums asking for help with their near perfect code. I say near perfect because the code works fine; if all of the function calls succeed. If anything is wrong, like the user's "installation" or run-time environment is broken (maybe bitmaps aren't where they need to be[3]), then the program crashes hard and the user (and the programmer) is left wondering what's wrong. The problem now is that to debug it, the programmer (that's you) has to either litter the code with fprintf calls or run it through a debugger to figure out where it stops running and why. It's a lot easier to just check return values.

References

  1. Some functions return output through pointer (C) or reference (C++) parameters as well.
  2. Non-zero on failure is just a "fancy" way of saying that anything that isn't zero indicates an error.
  3. Probably the most common problem that I've seen are of this type.