tags:

views:

261

answers:

7

Based on the answers to a previous question, here's a design for a simple error reporting system for my embedded C app. I'd appreciate some feedback.

My app has code forming an overall program plus several layers of lower-level libraries. At the lowest level, the library gets a byte to use for error codes, so 255 total errors. They'll be encoded in an enum, like:

enum lib1ErrorCodes {
    ERR_NO_ERROR,
    ERR_NO_CONNECTION,
    ...
    ERR_MISC
};

This is passed up the chain either through a global variable:

unsigned char lib1ErrNo;

The next higher level contains error codes for the libraries it uses:

enum lib2ErrorCodes {
    ERR_NO_ERROR,
    ERR_LIB1,
    ERR_FILE_EXISTS,
    ...
    ERR_MISC
}

Lib1's error is detected and is flagged in this level's error variable:

unsigned char lib2ErrNo = ERR_LIB1;

At the top level, when it becomes time to report all this to the user, these are detected:

if (lib3ErrNo == ERR_LIB2)
    if (lib2ErrNo == ERR_LIB1)
        printf("Error %d: %s", lib1ErrNo, lib1ErrDesc);

The only negatives I can think of in this scheme is the need to set aside a few error codes in each library to point to the libraries under it, and for the top-level program to include all these levels for their error codes.

What's the appropriate way to do this, if this isn't it?

I want:

  • Unique error propagation all the way up to the top level so I can report it.
  • Preferably not passing around large structures, as this is an embedded app.
+2  A: 

I think that having the same value for different errors -- in your example both ERR_NO_CONNECTION and ERR_LIB1 have a value of 1 -- is not wise.

I would recommend to reserve value ranges for each of your libraries and explicitly assign these values to your error codes.

enum lib1ErrorCodes {
    ERR_NO_ERROR = 0,
    ERR_NO_CONNECTION = 1,
    ...
    ERR_MISC = ...
};


enum lib2ErrorCodes {
    ERR_NO_ERROR = 0,
    ERR_LIB1 = 101,
    ERR_FILE_EXISTS = 102,
    ...
    ERR_MISC = ...
}
mouviciel
I guess under this system I would need to actively manage which libraries get which error ranges, right?What about third-party libraries that I have no control over?
jparker
You guess right. About third-party libraries, I suppose that each has its own logic.
mouviciel
A point against this strategy is that it creates hidden dependencies between modules. Minimise coupling, I say.
Craig McQueen
A: 

I'd be more inclined to store the error code along with a flag bit to indicate which library it came from.

Vicky
+1  A: 

I tend to try to translate my errors at each layer so they make sense in that context. For example lets say you have a main program which calls a communications stack, which in turn calls an serial driver. The serial driver might have some errors like:

typedef enum
{
    SER_NO_ERR,
    SER_TX_ERR,
    SER_BAD_ARGS,
    ...
} SER_ERR;

The communications stack might have:

typedef enum
{
    COMM_OK,
    COMM_TIMEOUT,
    COMM_TX_ERR,
    ...
} COMM_ERR;

In your comm stack you might some code that looks like:

// lets assume you are using the global variables serErr and commErr 
// to store error codes for their respective libraries.
serial_transmit(someDataPtr, someSize);
switch(serErr)
{
    case SER_NO_ERR:
        commErr = COMM_OK;
        break;
    case SER_BAD_ARGS:
    case SER_TX_ERR:
        commErr = COMM_TX_ERR;
        break;
    ...
}

I realize that might introduce a fair amount of code but I like that it maintains the abstraction between the layers. You can also define some functions to handle the translations, like:

void comm_handle_serial_error(SER_ERR err)
{
    //copy in the switch statement from above
}

but the errors may mean different things in different contexts, so I recommend keeping that in mind as you design the system.

Now at your highest level, all you have to do is worry about handling all of the errors defined by COMM_ERR when you call a communications function. Note that all of these examples are a bit contrived, but hopefully you get the idea.

Gabe
+3  A: 

First, unless you have 8-bit architecture I would use a bigger variable to hold all the errors.

Secondly I wouldn't make the system that complicated. I'd just have an enum for all the errors and whenever an error happens, I would call an error handler that reports it forward. Often when you get one error you get many and if you have just one error code per library you run the risk of loosing some of them, propably the one tha mattered the most (the first one) unless you are very vigilant in not allowing anything else to happen after an error has occured. This can also get quite complicated.

Makis
+2  A: 

In my experience, the more information you are able to report about the error, the better off you are.

One way I've solved these types of problems is to come up with a central "Status Manager" that all packages can interface with, should they discover a problem. At a bare minimum, the Status Manager can be called with an enum indicating a problem as well as another enum/integer to help you locate where the problem is. -- On some systems (usually 16-bit+ systems with at least 128K of RAM), I've even been known to store text strings to describe the problem(s).

I'm also assuming you can establish a back-door interface and/or proprietary interface that can be used to extract this data.

Nate
A: 

depending on your program space/ram/speed limitations there are frameworks for C++ like exception handling in C. This is an excellent article that i've used in the past to solve such a problem on a very low end platform where dynamic memory allocation what not available. Used appropriately this can break you free of the issues associated with handing error codes up the call tree.

On the other hand i've used architectures similar to the other provided answers in some situations as well. The most common being a centralized error state module that would accept error codes and make calls to appropriate handling functions.

Mark
A: 

Use __FILE__, __FUNCTION__ and __LINE__ macros to identify the location. No need for unique error codes then since the module is identified. Consider the following:

#define ERROR_PRINTF( code, fmt, ... ) error_printf( "\n%s::%s(%u) Error %u : " fmt "\n", __FILE__, __FUNCTION__, __LINE__, code, __VA_ARGS__ )

int error_printf( const char* fmt, ... )
{
    va_list args ;
    va_start (args, fmt) ;
    vprintf (fmt, args) ;
    va_end (args) ;
}

Then say the following line appears in main.c, line 20 in the main() function:

ERROR_PRINTF( ERR_NO_CONNECTION, "Connection failed on port %d", port ) ;

The the following text would appear:

main.c::main(20) : Error 1 : Connection failed on port 2

Personally, since this allows expressive error messages and precise location, I would not bother with the error code business at all. Every error is uniquely identified by its location, which is far more useful.

Clifford