Hi Shao,
So '__FILE__', '__func__', '__LINE__' would satisfy that?
Ignoring those compilers that don't expand __FILE__ to the full path
of the file, this works. In an earlier implementation, I used a
hash of these three to generate the actual "error code".
The bigger problem is programmer discipline (something that I
obviously don't trust :> )
E.g., the programmer is under no compulsion to invoke MakeErrorCode
in a "meaningful place". So, he could choose to put all of these
"invocations" (for want of a better word) at the very top of the
file. In which case, the __LINE__ and _func_ don't evaluate to
anything "useful".
Similarly, there is nothing to force a developer to differentiate
between conditionals that result in a particular error code being
returned.
E.g.,
MakeErrorCode(foo...)
// Note that the FILE/LINE/func point to the above line, which may
// not be *any* of the following conditionals!
...
...
if (value > 27)
return foo;
...
if (value < 36)
return foo;
...
if (today == tuesday)
return foo;
Of course, there are countless ways of arranging control structures
to result in this same sort of behavior.
I had considered turning MakeErrorCode(error, ...) into a legitimate
*statement* that resolved to:
return error;
This would *ensure* that the error code identified the exact place
where the error was "returned". But, it still doesn't avoid the
above problem:
while (forever) {
if (value > 27)
break;
...
if (value < 36)
break;
...
if (today == tuesday)
break;
}
MakeErrorCode(foo, ...)
There are many other limitations that this imposes on coding style,
as well.
[I've not come up with a way to force developers to "do the right
thing"]
Oh, so a 'const char *' to a message right in the code-base doesn't
satisfy that. You want some other kind of index, then.
Correct. My first approach generated "static const char *" to embed
the error messages in the modules. The sizes grow too quickly. I
then tried parameterizing "MakeErrorCode" so that I could specify
*where* (segment) to store the messages. Same problem only different.
Finally, I decided the messages need not have any relationship to
the code *other* than the "error code" which acts as a handle/index
to tie everything together.
So, I could pull all of this text out of the "executable" and
load it as needed, on demand. Application starts up faster and
has a smaller footprint. "Errors are the exception, not the norm"
I'm not sure that I follow, here. Are you saying that this is what you
want or don't want? Are you saying that ideally, 'MakeErrorCode' would
be a macro or extension that would allow for a translation process to
Exactly ----------------------------------------^^^^^^^^^^^^^^^^^^^
associate 'Err_TooManyDecimalPoints' with the provided text and position
in the code as a one-to-one mapping, and have it possible for the
additional detail to be stored apart from the final executable and not
embedded within it, and for 'Err_TooManyDecimalPoints' to be a unique
value?
Yes. (More follows)
The most important thing is for this to be REALLY painless for the
developer. If he has to write code to manipulate structures he's
just going to take the easy road and have *every* error handled
by a single "error exit".
He still has to implement the functionality to check for those
error conditions (i.e., he can't allow a string of characters
with two decimal points to be accepted as a valid numeric value!).
But, if he has to do "something extra" to report this as a
*different* "error" than "Err_MissingSign" or "Err_MultipleSigns",
he's more likely to just settle on "Err_BadValue" and lump all
of those "situations" into a single error report:
"There's something wrong with the value you typed in"
[I think most people are lazy when it comes to anything beyond
"getting the code to work"]
As regarding "uniqueness", yes, all the error codes should exist
in a single namespace. In something like C++ you could easily
support multiple namespaces -- so each "library" could implement
its own "error code-space". You can do similarly in C (with a bit
of work). But, in each case, you would then have to qualify
Err_<whatever> with an indication of the namespace in which it
was defined.
I thought to tackle this with FILE/LINE/func but how does the
*developer* indicate that he wants the "Err_Overflow" that
is defined by "doubleMath.c/27/dadd" and not the one that is
defined by "networkDriver.c/85/enqueuePacket"?
It was easier to treat file/line/func as *data* that tags along
with the error code instead of making it *part* of the error code.
Stepping ahead to answer your question a few (of your) lines
further along:
In a single namespace, you run the risk of two (independant)
developers picking the same name for an error code. Just
like they could pick the same name for a function, global,
etc.
<someone> sets policy to ensure this doesn't happen. (e.g.,
all my names begin with "don_").
What I want from this error scheme (the question you pose below)
is for the mechanism to tell me if two people have chosen the
same name. I.e., if there are two error codes that I can't
distinguish *solely* based on their names. (since I want to
use *just* the name as the index into the "list of explanations",
etc.).
If you build a table of #defines (or equivalent), then the
compiler can tell you if you have redefined a symbol. But,
you may have split that table into smaller tables so that not
all of the error code "symbols" are in one place! You might
choose to do this to better manage the namespace (so you
can develop separate subsystems/libraries without having to
coordinate your choice of error names *in* a file). Or, as
an efficiency hack (so all files that refer to ANY error code
don't have to parse the entire list of error codes). Or, to
reduce the number of make dependencies (foo.c only depends
on matherrorcodes.h instead of allerrorcodes.h). Etc.
If you've split them, you need a special step to reconcile
them against each other to ferret out potential duplicates.
If you forget to do this, you run the risk of ambiguity in
an error report!
I'm not sure what "pack structs" means, exactly.
Building/populating data structures with explicit values
that can then be reported, etc. like your:
memset(status, 0, sizeof *status);
SetStatusMessage(status, "Bar failed while blargling");
SetStatusSourcePoint(status, __FILE__, __LINE__);
etc. What happens if the developer forgets to take some
or all of these steps? (Or, if he's just lazy!)
I don't follow here, either. Are you saying that, in this example, the
'Err_TooManyDecimalPoints' in this function would not collide with
another function using 'Err_TooManyDecimalPoints' in a similar fashion?
If so, what would a caller use to detect that error?
These were the questions I anticipated above.
He concentrates on writing the code to implement the functionality
desired (as specified in the contract), detecting the exceptions
that are encountered (also in the contract) and DESCRIBING those
exceptions -- *here* (where their nature is most apparent) without
requiring those descriptions to *remain* "here" (in the deployed
code).
Aha! This seems to answer one of the questions I had, above. You wish
for the additional detail associated with the exceptional condition to
be [possibly] stored _outside_ of the final resulting program. This
really seems like you could benefit from some extensions that allow you
to specify sections for things. Then a linker script can omit those
sections from the final resulting program and build something else from
those sections, used for error lookup.
Yes. As I said, I tried that in one incarnation of MakeErrorReport.
I have moved to the point where I now have different tools to
extract different bits of information from the sources. Much like
LP's tangle and weave
(see
http://en.wikipedia.org/wiki/Literate_programming)
Since the error *messages* don't usually need to be updated
(i.e., available in the run-time) during debugging, this lets
me skip a fair bit of processing while I am concentrating on
"getting the code right".
Am I following your meaning, here? Are you doing this already?
Do you mean that the programmer uses their convenient "default" language
with 'MakeErrorCode', but the entire blob of additional error code
detail can be swapped with one for another language, etc.?
MakeErrorCode will (eventually... I only speak one language -- and
poorly at that! :> ) allow a developer to add "tags" to each
message. I.e., "This is the en_US version of the short error
message for this error code." "This is the pt_BR version of
the long error message for that same error code."
In theory, you could do this "elsewhere" -- i.e., have someone
browse the "master error message table" and add columns for
each supported language, doing the translation *there*. But,
you would then probably want a mechanism for "back porting"
that information to the source files (so that the source files
become the authoritative reference for ALL versions of the
messages). You'd hate to have to rehire that same translator
*next* project to tell you, AGAIN, what the spanish phrase for
"out of memory" is...
[Note that you could always configure the "translation process(or)"
(your term) to only extract en_US messages (for a US market) or
some *combination* for an international market.]
[Grrrr... I was just distracted by a friend's (banal) question
which caused me to forget what I was going to write! :< ]
As I said, I already *have* all this. Now, I am trying to have it
EFFICIENTLY! :>
Do you mean, you want a rebuild process to rebuild only those things
whose detail/dependencies have changed, and not the entire code-base?
Yes! I'm using this in *really* big projects and can't afford to
wait a day to "make world" each time some new error code is created,
deleted, changed, etc.
Again, people are lazy (I am a people! :> ) If you waste a day
because of some mechanism, you will quickly learn NOT to use it!
This sure reminds me of iPXE's logic for dealing with similar challenges.
But that (speaking from ignorance, here) handled the problem by
partitioning the error code. I.e., math routines could use
errors 0x2340 thru 0x234F; disk I/O routines could use 0x2350 - 0x2357,
etc. (?)
So, an arbitrary source file could #include (or manually synthesize!)
a particular "SUBSYSTEMerrorcode.h" without concern for how or where
the errors were being signaled in the modules that made up that
SUBSYSTEM.
Well I figured you could use macros and other tricks to hide the
details, if you like. I was just trying to demonstrate how it might look
underneath macros.
Understood. But, that would intimidate the developer. He'd quickly
rationalize why he "doesn't need to report error details". If,
instead, all he has to do is *differentiate* individual error
"instances" and give a description of what they signify/indicate,
it gets hard for him to justify NOT doing this. Especially when
it is easy for all of his peers to do so. Reliably!
"Gee, Bob, how come those aspects of the product/program that
you coded all give 'error 27' messages and nothing more? Alice's
part gives all sorts of detail explaining the exact nature of
each DIFFERENT problem! People are having a hard time using
*your* features..."
So what are you saying, that that's what you want to be able to type,
without using the 'MakeErrorCode' at all? Or are you saying that the
'MakeErrorCode' inclusion is about as far away from this type of pattern
as you're willing to get?
I'm saying that the above is the bare minimum that a developer
implementing a "parse string for numeric value" function would
have to code (well, he could technically treat ALL errors as
BAD_VALUE -- which might be a *tiny* bit less complex).
Then, I'm saying "compare this BARE MINIMUM to what I am proposing".
What I am *imposing* on the developer really doesn't look like a
whole helluvalot!
"You walked all the way to the grocery store and you
couldn't bring back a LOTTERY TICKET for me???"
The less I impose, the harder it is for folks to rationalize
not complying.
I.e., if "Alice's" (above) parts of the project are slicker'n snot
BUT TOOK HER MONTHS LONGER THAN NECESSARY to add that extra
reporting functionality, then Bob can point this out as yet
another justification for NOT doing things that way! That
"lottery ticket" doesn't really take up much space in your pocket
as you walk back from the grocery store! :>
[your code elided as I fear I am already reaching the post-length
limit :< ]
Would it be difficult to wrap this up with a macro and call that macro
'MakeErrorCode'? Or is that what you're doing?
MakeErrorCode just shows the "translation processor" where the
information regarding the error code resides in the source file.
(I.e., it makes my life -- in writing that processor -- easier).
It doesn't generate *any* code (in the examples I've shown so
far). All it does (for the source file) is ensure that
"Err_Whatever" can be resolved by the compiler.
It provides information to the translation processor(s) that
allow them to harvest the various error messages, file/line/func
markers, error code name, etc. for use elsewhere (wherever I
opt to store the error messages... maybe a file full of
const char *'s!)
Yes, sorry. cons glues the two arguments together (into a tuple,
of sorts). car extracts the first element from the tuple.
cdr extracts the second element.
I.e., result is a *list*. The list can be seen as an element
followed by a (shorter) list... which can be seen as another
element followed by a (even shorter!) list...
So taking the example of 'Err_TooManyDecimalPlaces':
#1 Before that is ever used in any code anywhere, you build your code.
Then one day you come along and realize it's an exceptional condition
that you'd like to be able to document and catch. So are you saying
you'd like to be able to make a minimal change to 'whatever.c' where you
introduce the exception and associate additional detail (documentation)
with it, and not have to recompile any of the other translation units
(roughly: .c files) files but simply be able to re-link and have the
"additional detail database" updated accordingly?
Correct. Before that "one day", you can grep the sources and
never turn up a reference to Err_TooManyDecimalPlaces (I should
have picked a shorter name! :< ).
On that "one day", you add a MakeErrorCode(Err_TooManyDecimalPlaces,...)
to whatever.c. Of course, whatever.c has now changed so *it* has to
be recompiled (presumably, you also referred to Err_TooManyDecimalPlaces
somewhere inside whatever.c -- in addition to "defining" it.
But, since no one else references Err_TooMany..., no one else should
NEED to be recompiled!
Yet, any functions that call the function in whatever.c that can
*return* Err_TooMany... will now recognize that this is STILL an
error code (i.e., whateverFunction() has returned yet another
particular error code!) AND, if asked to expound on the nature
of that error, will be able to lookup the message associated
with Err_TooMany... and provide it to the user.
#2 You realize that a function in 'caller.c' that calls the function in
'whatever.c' ought to catch the 'Err_TooManyDecimalPlaces' condition and
add some valuable detail to how that condition is relevant. You want to
be able to test the return-value (or some result) of the called function
with 'Err_TooManyDecimalPlaces' without having to add
'Err_TooManyDecimalPlaces' to a header where every other TU #including
that header will require recompilation?
Correct. caller.c should just be able to refer to Err_TooMany...
without a whole lot of fuss.
If so, perhaps you could satisfy
this by having a one-to-one relationship between exceptional condition
and header file.
Did you consider "address constants" at all? This avoids a progressive
ordering of strict numeric codes, while still allowing for a symbolic
identifier and comparison for equality.
EXACTLY!!!!! This is the approach I am now trying.
In essence, the Err_ are treated as extern's. So, the compiler
doesn't care that they aren't (yet) defined. They just have
to be resolvable at linkage time.
Instead, the linkage editor resolves and binds them to their actual
values (I have to fabricate a dummy errorcodes.c that instantiates
all of the Err_'s as "addresses").
But, it (appears to?) satisfy those goals of minimizing recompiles,
etc. Though I still have to "process" the source file to determine
which Err_'s are defined within (or, force the developer to explicitly
declare them in global scope). Duplicate names are detected by
the linker, etc. And, I can get a cross reference map showing what
is referenced, where.
Unfortunately (?) it means sizeof(result_t) is now the same as
that of a void *. I guess I could artificially map them anywhere
in the address space and play pointer math games to ensure
SUCCESS, warn and error are easily derived from the resulting
values. That way I could shrink them to sizeof(short int), for
example...
I'm still chewing on this approach (busy coding something else
at the moment) but I can't see any insurmountable problems so
far...