Dear List,

A few days ago I was looking at some of the code in the engine on the
master branch, and I realized that we currently are doing
some questionable things with exceptions and destructors that can cause the
program to crash.

https://github.com/wesnoth/wesnoth/blob/46299c6a110b09ebfa9f9e3044021ece2568ea4d/src/playmp_controller.cpp#L426

At this line, we signal a "quit" from a client event loop state
`end_level_exception (QUIT)`. This is not uncommon in the engine,
we currently use "end_level_exception" and "end_turn_exception" to signal
when a turn has ended or a level had ended, and in mp
we sometimes throw "network::error" when we want to disconnect from the
current game.

It's debatable whehter using exceptions for flow control is good or bad,
and I guess many people have opinions about it.
However, in this case, it actually leads to crash, because we also have an
object on the stack `turn_info_send` which sends
networking data in it's destructor:

https://github.com/wesnoth/wesnoth/blob/afbf5c6e5804ef5a31a40b4bcb59a41f389e5a40/src/playturn.hpp#L91

What this means is that if the networking module has an error, like, it
loses connection with the server, instead of handling
that network::error exception normally, and jumping here:

https://github.com/wesnoth/wesnoth/blob/afbf5c6e5804ef5a31a40b4bcb59a41f389e5a40/src/playsingle_controller.cpp#L589

which would tell the user "a network error has occurred, would you like to
save the game?", the program will *immediately crash
to desktop*. (Because, as I understand, in C++, if an exception is thrown,
the callstack is unwound until a handler for the
exception is reached, or the program is terminated, and in this process all
destructors are called for any objects on the stack.
If any of those destructors throws an exception, the program is immediately
terminated -- you cannot handle multiple exceptions
at once.)

I'm not sure because there is a lot of code involved in synchroniation and
networking, but I'm concerned that issues like this
from somewhere in the code could be  causing instability on 1.12 branch.
For instance we got some bug reports like this:

https://gna.org/bugs/?21873

"We played a couple multiplayer matches, each one ended with him losing but
everytime he lost the game reported him as having disconnected and when he
got back he said that the game had forcibly shut down. This would happen in
the middle of the post-game screen even before I hit end scenario."

That's pretty much exactly what I would expect if we were getting some kind
of double exception issues.

---

There are two morals that I take away from this:

1.) Don't use exceptions for flow control.
2.) Don't throw exceptions from the destructor.

As I understand there are some C++ developers out there who will argue on
behalf of both of these things, but if used in concert they will apparently
lead to very poor results. It is sometimes said, it takes two aggressive
drivers to make an accident, but at this point I think we have had an
accident and now should add remarks to "coding standards" on the wiki
disallowing both of these.

---

A bit more on each --

(2) After reading about this (I learned in school that you shouldn't do
this but I long ago forgot why and had to read about it...), it sounds like
there are some C++ developers that actually defend this practice of
throwing from a destructor in some cases. In C++98 if you can somehow
guarantee that there is no exception actively being unwound, then throwing
from the destructor won't immediately crash the program. However:
- It will prevent destructors of children being called and can therefore
lead to memory leaks that are very difficult if not impossible to correct
after the throw,
- In C++11 the language is more strict about this -- if a destructor ever
throws an exception, the program is immediately terminated.
So it sounds like it's not something that we want to do. In fact, it might
be a good idea if we tell people that if they do anything nontrivial in a
destructor, they must wrap it in a try { stuff } catch (...) {} block that
will swallow all exceptions. Even writing to a logger in a destructor could
in some cases lead to a crash I think:

a.) Suppose that at some time in the future, there is a bug our
UTF-conversion facilities. A bad string conversion happens while someone is
writing to a logger object -- the logger chokes and throws some kind of
exception which propogates up the play_controller stack.

If someone somewhere wrote a destructor X::~X() { logger << "foo bar\n" ;
}, if the logger is already broken it could generate a second exception,
immediately crashing the program rather than reporting the error.

b.) gfgtdf and I have written a "smart enum" macro which is supposed to
help us when parsing WML files to C++ objects, when there are only a few
valid options for strings. The enums have streaming operations defined so
that they can be converted to strings and back, and these ops have built-in
error reporting -- if an enum can't be parsed or written, and the game is
running in debug mode, it will generate a t_wml_exception, which launches a
transient gui window explaining to the user that they have an error in
their WML. There are several places high up in play_controller where these
exceptions are hanlded and the gui window is actually launched.

However, suppose someone wrote a destructor that logged some debugging
output based on converting some more enums, or similarly calls lexical cast
in some context. In that case if we encounter a bad enum value and throw an
exception, then when X is destroyed we might encounter it _a second time_,
throw a _second_ exception while writing the debugging, and crash instantly
because of it.

So I think it should be a coding standards issue that destructors that do
more than, delete pointers, or call a C library "release resource"
function, should be wrapped in a try-catch-all block. If the programmer
doesn't like this because it would interfere with normal exception
handling, then they should consider not putting their code into a
destructor.

However I'm not a C++ expert by any means, and I normally try to stay away
from destructors anyways, so there are likely more things I don't know
about all of this -- please chime in if you have something to add.


(1) I guess that this is somewhat controversial, and there are some C++
developers who think that using exceptions for flow control is fine esp. if
it leads to less typing.

However, my viewpoint is that, the reason we were tempted to start doing
(2) to handle networking ops in the engine, is because we were doing (1)
and had no way to guarantee that we are staying in sync unless networking
was happening in destructors. So I think (1) is in some way the root cause
of this.

I have started a branch here, which attempts to incrementally push all of
the flow control exceptions out of the play_controller object at least, and
down further in the call stack into replay execution and event handling.
The goal of this is to simplify the logic that is happening in play
controller, so that we can properly reason about it and ensure networking
synchronization is happening correctly without using exceptions or
destructors.

https://github.com/wesnoth/wesnoth/pull/191

I view this branch as having been fairly productive, just the review
process has led me to find some other bugs elsewhere in 1.12 branch that
could be corrected, and it also clarifies exactly where the end level and
end turn signals are coming from so that we can better understand exactly
what is going on.

The experience has led me to think that, we should add a line to the coding
standards page:
http://wiki.wesnoth.org/CodingStandards#Write_exception-safe_code

"Do not use exceptions for normal flow control. This is generally
unnecessary, leads to less maintainable code, and can interfere with the
proper handling of truly exceptional scenarios."

The purpose of the coding standards remarks is not to criticize other
programmers, but rather to make sure that new programmers actually know
about these things and that they can cause crashes -- the rules about
exceptions and destructors are fairly unique to C++ anyways and not
everyone knows them intimately...

Best Regards,
Chris Beck
_______________________________________________
Wesnoth-dev mailing list
Wesnoth-dev@gna.org
https://mail.gna.org/listinfo/wesnoth-dev

Reply via email to