See below…
> library writers; for example, should I do RaiseException or throw in my
> library?
No libraries in unmanaged languages (except those like STL or ATL which
are a set of sources rebuilt each time) should ever raise C++ exceptions.
It’s a more difficult call than this. I can use the same trick STL/ATL
use of putting too much in the .h file so things are rebuilt each time,
but it still isn’t pleasant.
Reason: C++ exception implementation depends not only on the compiler’s
brand, but on its version number and command-line parameters.
Actually, the good taste of programming in Windows is to only use a) C
exports b) COM as the library APIs. Both are well-defined and
language-independent.
COM comes with far too much baggage. Also, an MFC programmer gets a C++
interface to COM which does throws, and a plain interface to COM is just
far too ugly to use, whether in C or C++.
> do a throw, but as a general library writer, trying to support both
> languages, I’d be in trouble.
No troubles at all. Support C only, it will be automatically
C+±compatible.
> exceptions right is hard; Java is one of the few languages that actually
> got it right. Or at least righter than C++.
Correct.
Some additions:
- C# is also such, not only Java
C# doesn’t complain if you call a function that does a throw and you have
not specified a handler for it, and not included its exceptions in what
your function can (in effect) return. Java is far more uptight about
this, and I think this is good. If it compiles, I got everything right.
- C++ exceptions are just plain inadequate without RAII. And, usually,
wrapping everything to RAII (including stuff like tmp memory buffers for
IOCTLs and such, which are major fun for RAII) is considered to be much
larger effort then just fixing bugs in the code which does “return
NtStatus;” or “return hr;”.
Yep.
- MS’s C exceptions are a toy. SEH frames in the language which has no
destructors is just a punk-style fun. With C SEH, the amount of error
recovery code to be typed is the same as with “goto HandleError1; goto
HandleError2;” code.
Actually, it is a bit worse than this in terms of code quantity, but I
think that exceptions provide better documentation as to what is really
going on. I’ve worked in three systems that used exceptions that look
like C SEH (Microsoft C being the fourth). I was able to use exceptions
to advantage in each one of them.
Actually, looks like MS’s SEH was developed with the only real practical
purpose in mind: to allow touching ->UserBuffer in drivers without making
a copy of it.
Yes, you need SEH-style stuff for METHOD_BUFFERED too, but it can be
hidden in the kernel and not published to the outside world to avoid
confusion
this is what Linux does with __copy_to_user() (Linux does
not support METHOD_NEITHER so it is safe).
Also one another drawback of SEH of any kind: in usual style, if I see the
call without the if( !NT_SUCCESS(…) ) wrapper, then I know that this
call has no-fail guarantee.
In Java, the no-fail guarantee is enforced by the compiler, which I think
is one of its winning (albeit sometimes painful) features.
With SEH, it is not so. The “throws” spec helps only a bit: first, its
implementation on C++ is pathetic. Second, you need to look at the
function declaration to see whether is has no-fail guarantee.
Yep. This is high on my list of defects.
The presense of try/catch and try/except operators just plain does not
reduce your effort of proper error handing.
No. Sometimes it even makes it messier. But what it does is force you to
do error handling, not just write
DoSomething(…);
and get away with not checking the return value. Sure, lint and other
checking programs will complain, so you end up writing
(void)DoSomething(…);
but all this is just a copout to doing correct error handling.
> Note: COM interfaces in C++ throw exceptions
No COM interface can throw C++ exceptions. To begin with, the COM’s
remoting/marshalling layer cannot support them.
COM interfaces in MFC go through a layer that throws exceptions.
If the COM component throws across the interface boundary, then it is
broken.
There is ISupportErrorInfo/DISP_E_EXCEPTION OA exceptions, but they are
not C++ ones and require translation layer (there are macros in ATL for
this).
> Actually, free() CAN fail, but it does so catastrophically, making the
> handling of such an error next to impossible in C.
free() cannot fail. It just has no failure modes (C function, returns
“void”, has no callbacks) except crashing everything with impossible
recovery from it.
Well, I tend to call that “failure”. And if I’m trying to make something
bulletproof (I could even recover from parity errors, if they were
detected) I am not willing to accept anything that doesn’t return a value
but can take my app down.
Also note: recovering from the heap corruption is useless. The proper
thing is to fix the bugs which lead to heap corruption.
I agree. But in our case, the heap corruption occurred about once a day,
and nothing in the dumps allowed us to determine the cause. The heap was
definitely containing trash, but short of having our program fix the
hardware, I had no choice but to recover from it. Also, I want to make my
component independent of the other components that might be running, and I
want to be isolated from *their* bugs (ISAPI is one of the historical
interfaces where the confusion of heap corruption was not handled well;
ISAPI components should never have been allowed to share the general heap,
but there was no way to isolate the heap usage. Managed code, the current
approach, is far better, although it has its own problems).
> It is not clear why destruction calls should not be allowed to fail.
How will you recover from destructor failure? For instance, in C++ SEH, if
the RAII destructor raises itself, then abnormal_terminate() occurs. The
RAII/SEH machinery just cannot continue from such a failure.
I had to deal with destructor failure, when the heap was found to be
damaged. And yes, I was able to recover from it. So I don’t want some
other programmer telling me I can’t, because I already know that I can.
And this is correct. Destructors must not fail, this is the obvious law.
It is not at all obvious to me. A failing destructor may be indicative of
additional damage, and the recovery might be complex, but that decision
should not be made for me by someone else, who simply says “destructor
failure is fatal”.
> Any robust allocator is going to be checking for heap integrity on
> release
…if special debug mode is turned on (like what Verifier does) - then
yes.
There are many variants of this. Some, like debug mode, provide tons of
information about what might have gone wrong, where it went wrong, who
last allocated that block, etc. But I’ve written product (retail build)
allocators that did some fairly minimal error checking; for example, that
the “magic cookie” signature was in the block header, AND was at the end
of the block. Otherwise, we had an overwrite. And it is far more useful
to issue an error message in terms of the input instead of just failing
hard. Which makes more sense:
Starting compilation of module whatever.ext
000321 typedef enum {
…hundreds of symbols generated by a tool
004418 xyzzy,
***ERROR Internal error detected, heap corruption
***FATAL ERROR Compilation terminated
or
Your application has stopped. Do you want to send debug information to
Microsoft?
which would have been, in those days, done as
Starting compilation of module whatever.ext
FATAL ERROR A fatal internal error occurred
Actually, we discovered that somebody in our compiler group had
(erroneously) decided there could be no more than 4096 names in an enum
(equivalent; the language the compiler was processing wasn’t C, but the
idea is the same), but if you used 4097, instead of doing a bounds check,
it overwrote a heap block. Yes, it was a bug. Yes, we fixed it. The
fact that xyzzy appeared on line N, and the typedef appeared on line M,
and (N - M) > 4096, led us to the bug. The source code had been generated
by a YACC-like tool, so the symbols had silly names like
S_EXPR_PLUS
which were states (S_) derived from the grammar spec. Each non-identifier
terminal symbol had a transliteration, e.g., ‘+’ => _PLUS so you could
have operators like ‘+=’ which would generate a symbol S_PLUS_EQUAL, and
no, I not only had nothing to do with this, it was some third-party
product used by one of our customers.
The way I found the problem was to run in the “full debug” mode where a
heap integrity check was run on every allocation and deallocation request.
Performance hit? Well, I started it running, an hour later, I left for a
meeting, two hours later I had returned, it was still running, so I went
out to lunch, and when I came back it was still running, and about half an
hour after I returned, it hit. But we had useful data to start with, that
it only failed in long typedef-equivalents, so the source code was only
that typedef-equivalent, and none of us knew the (now former) programmer
had used a fixed-size table for enum names.
Production allocators should never do this due to very major perf hit.
No, it isn’t a “major” peformance hit; it involved comparing two integers.
It was cheap on 1980s mainframes, and it is essentially free on a
pipelined superscalar machine with speculative read. It was certainly not
our performance bottleneck, and we worked like crazy to get the compiler
to process 200 lines/min of source. Which made us faster [just barely]
than our competitors in that product domain, for the same language on the
same host. In fact, the allocator was not even visible in the performance
profiling; it was for all intents and purposes, “free”. We were working,
in those days, in a single-threaded environment, and we were considering
ways to make the compiler generate inline code to allocate, but when we
couldn’t even find the allocation cost, we dropped that optimization
effort. I labored long and hard to make that allocator have incredible
performance.
> integrity of the heap are important. How can you free something that is
> not a valid address?)
Crash the whole app and let the developer fix this bug.
See previous example. Identifying what was going on in the input is a BIG
help. Field support is a major cost center, and having the ability to
report bugs in terms of the customer problem space instead of the machine
implementation space is a major help!
joe
–
Maxim S. Shatskih
Windows DDK MVP
xxxxx@storagecraft.com
http://www.storagecraft.com
NTDEV is sponsored by OSR
For our schedule of WDF, WDM, debugging and other seminars visit:
http://www.osr.com/seminars
To unsubscribe, visit the List Server section of OSR Online at
http://www.osronline.com/page.cfm?name=ListServer