Using C++ as a scripting language, part 7

fwsGonzo
3 min readJan 8, 2023

--

Handling unhandled C++ exceptions from the games script

With a modicum of defensive programming, it is possible to write C++ that mostly throws exceptions when something goes really wrong. That, with a combination of sanity-checking assertions, and you can be reasonably sure that the games script is running properly.

That said, we don’t always care too much if the script crashes, as we can just reset the script and recover. It’s only really a problem if it prevents progress in the game. Still, whenever an exception does happen, it is nice if we can get all the things we need. C++ doesn’t always let you do that, but you can get pretty close.

The script is C++20 compiled with the GNU RISC-V toolchain. It still has everything, just like ~3 years ago when I started. Handling the first exception takes a lot of instructions, but it happens rarely. Like many things, it’s not a problem. Instead, I think C++ exceptions are a net benefit.

Handling unhandled exceptions

C++11 added std::set_terminate, which lets you provide a function that gets called whenever terminate is called. The documentation is fairly neat: https://en.cppreference.com/w/cpp/error/terminate. Apparently it gets called for exceptions thrown in global constructors and destructors too.

So, if we combine that with re-throwing the current exception, we can re-discover the exception being thrown, and handle it ourselves. Something like this:

__attribute__((constructor))
void handle_unhandled_exceptions()
{
std::set_terminate(
[] {
try {
std::rethrow_exception(std::current_exception());
}
catch (const GameplayException& ge) {
char source_buf[500];
strf::to(source_buf)(
ge.location().file_name(), ":", ge.location().line(), ", ",
ge.location().function_name());
sys_exception(typeid(ge).name(), ge.what(), source_buf);
}
catch (const std::exception& e) {
sys_exception(typeid(e).name(), e.what(), "Unknown source");
}
});
}

This is just the first iteration I have written and I realize there may be calls to terminate without an active exception. Regardless, this will handle the current exception for me.

As you can see, we also pass the typeid of the exception, which we can demangle in the engine: typeid(e).name() and get the name of the exception.

What I really would like is the ability to get a source_location for any and all exceptions thrown, but we will have to do with my custom GameplayException instead:

struct GameplayException : public std::exception
{
explicit GameplayException(
const std::string& message,
std::source_location sl = std::source_location::current())
: m_msg(message), m_location(sl)
{}

virtual ~GameplayException() throw() {}

const auto& location() const throw()
{
return m_location;
}

const char* what() const throw() override
{
return m_msg.c_str();
}

private:
const std::string m_msg;
const std::source_location m_location;
};

It uses C++20 std::source_location to give me the filename, function name and line number where the exception was thrown from. Very handy, and notice how the current source location is a default argument, simplifying throwing the exception.

Results

In the game engine, we just print out the details from these exceptions:

int main()
{
...

button.set_callback(
[i] {
print("Hello, this is button ", i, "!!\n");
throw GameplayException("HELP!");
});
}
}

Now clicking that button should show us that line 18 in this source file throws an unhandled exception.

[gui] says: Hello, this is button 5!!
[gui] Unhandled GameplayException: HELP!
-> from engine/scripts/src/gui.cpp:18, main()::<lambda()>
Program page: [0x0000000000401100] Readable: [x] Writable: [ ] Executable: [x]
Stack page: [0x000000000151AD00] Readable: [x] Writable: [x] Executable: [ ]
Function call 'api::GUI::Widget::set_callback(Function<void ()>)::{lambda(unsigned int, void*)#1}::_FUN(unsigned int, void*)', addr=4009d4
-> [0] 0x00000000004001F4 + 0xf0c: exit
-> [1] 0x0000000000401108 + 0x364: handle_unhandled_exceptions()::{lambda()#1}::operator()() const [clone .constprop.0]
-> [-] 4009d4 + 0: api::GUI::Widget::set_callback(Function<void ()>)::{lambda(unsigned int, void*)#1}::_FUN(unsigned int, void*)

Which it does! Apparently it’s a GameplayException!

Using this method we can also forward select exceptions that we care about, however I have no use for that. It’s more important to display the message and show where the exception happened.

Notice that I also record the name of the function that gets called in the script at the start, which has helped me narrow down many problems early. The key is to just try to provide useful information automatically, because when problems inevitably occur having to start adding print statements can quickly eat up time.

Demangling might not work across the boundary on all compilers and systems, but I don’t want to demangle it in the script (while possible), as it will bloat the script binaries. I am perfectly happy with disabling it on systems with different demangling rules.

-gonzo

--

--

fwsGonzo
fwsGonzo

No responses yet