Exceptions: Difference between revisions
(copy edit) |
|||
Line 1: | Line 1: | ||
This is a discussion document for ideas about how to use static analysis to help refactor Mozilla to use C++ exceptions instead of nsresults. In particular, this is about how to modify the call sites of methods so that the code uses exception handling and is exception safe. | This is a discussion document for ideas about how to use static analysis to help refactor Mozilla to use C++ exceptions instead of nsresults. In particular, this is about how to modify the call sites of methods so that the code uses exception handling and is exception safe. | ||
The main goal of the rewriting is to make the code use exceptions instead of nsresults without modifying the behavior of the code. This breaks down into: | |||
* Ensuring that methods that don't crash now don't crash by throwing an unhandled exception. | |||
* Ensuring that the error handling code at the call sites runs under the same conditions and does the same thing. (Effectively, we are assuming that the current error handling is correct, and that replicating that behavior with exceptions will result in exception-safe code.) | |||
== Call sites that ignore return values == | == Call sites that ignore return values == | ||
One case where we don't necessarily want to keep current behavior is at call sites that ignore their return values: | |||
callSomeMethodThatCanFail(); | |||
This is probably a bug, so we want a tool that can find and flag all of these results. After the bugs are fixed, any remaining call sites that really should ignore failures can be rewritten automatically to ignore exceptions. | |||
=== Analysis to detect ignored return values === | === Analysis to detect ignored return values === | ||
Line 11: | Line 19: | ||
Roughly, the analysis should flag all call sites where: | Roughly, the analysis should flag all call sites where: | ||
* the method is not nothrow | * the method is not guaranteed nothrow (i.e., the method can fail) | ||
* there is no branch on NS_FAILED(rv) or NS_SUCCEEDED(rv) | * there is no branch on NS_FAILED(rv) or NS_SUCCEEDED(rv) | ||
Line 43: | Line 51: | ||
callMethod(); | callMethod(); | ||
} except (nserror e) {} | } except (nserror e) {} | ||
The latter pattern can be expressed with a macro. (dmandelin: But macros are bad, right? I'd vote for no macros here.) | |||
== Making call sites exception safe == | == Making call sites exception safe == | ||
Most call sites do check for errors and respond somehow. This section is about how to rewrite those call sites. | |||
We'll start with the assumption that call sites adhere to a simple pattern that's relatively easy to rewrite, namely: | We'll start with the assumption that call sites adhere to a simple pattern that's relatively easy to rewrite, namely: | ||
Line 72: | Line 82: | ||
maybe return; // if original returns 0 | maybe return; // if original returns 0 | ||
} | } | ||
The '''thrower''' tool handles some instances of this pattern. (dmandelin: I don't know exactly what thrower can do at this point.) | |||
But there are at least two big practical problems with this rewrite. | But there are at least two big practical problems with this rewrite. |
Revision as of 18:38, 21 December 2007
This is a discussion document for ideas about how to use static analysis to help refactor Mozilla to use C++ exceptions instead of nsresults. In particular, this is about how to modify the call sites of methods so that the code uses exception handling and is exception safe.
The main goal of the rewriting is to make the code use exceptions instead of nsresults without modifying the behavior of the code. This breaks down into:
- Ensuring that methods that don't crash now don't crash by throwing an unhandled exception.
- Ensuring that the error handling code at the call sites runs under the same conditions and does the same thing. (Effectively, we are assuming that the current error handling is correct, and that replicating that behavior with exceptions will result in exception-safe code.)
Call sites that ignore return values
One case where we don't necessarily want to keep current behavior is at call sites that ignore their return values:
callSomeMethodThatCanFail();
This is probably a bug, so we want a tool that can find and flag all of these results. After the bugs are fixed, any remaining call sites that really should ignore failures can be rewritten automatically to ignore exceptions.
Analysis to detect ignored return values
Roughly, the analysis should flag all call sites where:
- the method is not guaranteed nothrow (i.e., the method can fail)
- there is no branch on NS_FAILED(rv) or NS_SUCCEEDED(rv)
The easy way is to look for if statements where the condition contains NS_FAILED(rv) or NS_SUCCEEDED(rv). But that's not perfect, because of things like:
- Calling NS_FAILED directly on the function without a variable
- Assigning the rv or the NS_FAILED test result to a variable then branching later
- Overwriting the value of rv before the test (so the rv isn't actually branched on)
The current thrower tool catches many of these cases but not all. It's believed that (a) thrower only produces false positives, not false negatives, and (b) doesn't produce too many false positives on Mozilla.
Rewriting ignored return values
If it turns out most of these are bugs, we can just flag them for manual repair.
On the other hand, if almost all of these sites are correct code, then we can handle an ignore site like this:
callMethod();
by rewriting it to this:
nsresult rv = callMethod(); if (NS_FAILED(rv)) {}
and letting the full rewriting algorithm rewrite this to use exceptions. Or, we could rewrite it directly as:
try { callMethod(); } except (nserror e) {}
The latter pattern can be expressed with a macro. (dmandelin: But macros are bad, right? I'd vote for no macros here.)
Making call sites exception safe
Most call sites do check for errors and respond somehow. This section is about how to rewrite those call sites.
We'll start with the assumption that call sites adhere to a simple pattern that's relatively easy to rewrite, namely:
nsresult rv = callSomeMethod(); // note: no code between call & rv test // note: also accept NS_SUCCEEDED(rv) with code in opposite then/else branches if (NS_FAILED(rv)) { maybe fixup(); return rv OR return NS_ERROR_FOO or return 0; } else { maybe do_stuff(); }
This requires a static analysis that finds all call sites that don't match this pattern so they can be flagged for manual rewrite.
Given the simple pattern, the rewrite looks like this:
try { callSomeMethod(); maybe do_stuff(); } except (nserror e) { maybe fixup(); maybe throw NS_ERROR_FOO; // if original returns new error code maybe return; // if original returns 0 }
The thrower tool handles some instances of this pattern. (dmandelin: I don't know exactly what thrower can do at this point.)
But there are at least two big practical problems with this rewrite.
First, when the original code failure case ends with 'return rv', the natural thing to do with exceptions is let the exception propagate. (If we use the rewrite above, we're cutting off the stack trace, which is bad for debugging.) If there is no 'fixup' code, then this is easy: just have no try block. If there is 'fixup' code, then we should rewrite to use a compensator object:
// class created to run fixup class SomeCompensator { ~SomeCompensator() { fixup(); } };
// call site SomeCompensator(); callSomeMethod();
This is the style recommended by C++ authorities. The problem here is that the code is no longer equivalent to the original nsresult code: the fixup is run on success as well as failure. We still might be able to produce this rewriting automatically, but we need to know more about how the fixups appear in our code and how they can be identified.
Besides fixups, the current nsresult code often logs errors or runs assertions. With exception stack traces, logging should be less important, but assertions are a real issue. We need to decide out how the assertion sites should behave with exceptions and then figure out how to implement that in C++.
The second problem is that the if condition is often a little more complicated than what our pattern allows. We also have:
- NS_FAILED(rv) || foo
- NS_FAILED(rv) && foo
- NS_FAILED(rv1) || NS_FAILED(rv2)
- NS_FAILED(rv1) && NS_SUCCEEDED(rv2)
Above, we said we'd flag these for manual fix, but some of them, like the first two, are pretty common and we might want to fix them automatically. The first two cases seem fairly easy to handle, but we need more information on how they're being used.
nothrow methods
Methods that never fail are the easiest to handle: exception safety at call sites is free. These are called nothrow methods following the Effective C++ terminology. We can remove all exception handlers protecting nothrow call sites, making the code shorter and easier to read.
We'll assume that any method implemented in JavaScript can fail and throw an exception, so the nothrow analysis is restricted to methods implemented in C++. The prime example is property retrieval methods.
Microsoft C++ nothrow attribute syntax
nothrow analysis
There are three ways a method can fail:
- By returning a non-zero (non-NS_OK) value,
- By calling another nsresult method that can fail (i.e., is not nothrow),
- By using a C++ feature that can throw an exception. The primary example is allocating memory with new. new badness
The static analysis must be designed to detect any of these conditions. Because it's hard to know what method will be called in C++, a good starting point for the analysis would be to say that a method is nothrow if it can only return 0, does not invoke new, and does not call any other method. If this doesn't pick up enough nothrow methods, we can either do it by hand or augment the analysis with a simple call graph construction.