Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ If it doesn't, consider reporting the issue, or ask for help:
# main buildsystem setup entry point
project(openage CXX)

# Objective-C++ is needed on Apple platforms to build the .mm files that
# interoperate with Foundation (e.g. dumping NSException details from the
# terminate handler). Enabled only on Apple so other platforms are unaffected.
if(APPLE)
enable_language(OBJCXX)
endif()

# C++ standard requirement
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
Expand Down
10 changes: 10 additions & 0 deletions libopenage/error/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ add_sources(libopenage
stackanalyzer.cpp
)

# Objective-C++ source for Apple-specific exception handling (NSException
# dump from the terminate handler). Only built on Apple platforms; the .mm is
# compiled as Objective-C++ via the OBJCXX language enabled in the top-level
# CMakeLists.txt.
if(APPLE)
add_sources(libopenage
objc_exception.mm
)
endif()

pxdgen(
backtrace.h
error.h
Expand Down
28 changes: 25 additions & 3 deletions libopenage/error/handlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

#include "handlers.h"

#include <cstdlib>
#include <cstring>
#include <exception>
#include <iostream>
Expand All @@ -27,6 +28,7 @@
#include "util/signal.h"

#include "error/error.h"
#include "error/objc_exception.h"
#include "error/stackanalyzer.h"


Expand All @@ -52,12 +54,16 @@ sighandler_t old_sigsegv_handler;
util::OnInit install_handlers([]() {
old_sigsegv_handler = signal(SIGSEGV, sigsegv_handler);
old_terminate_handler = std::set_terminate(terminate_handler);
// Install the ObjC exception tracker so the terminate handler can report
// NSExceptions that cross the C++ boundary (Apple only; no-op elsewhere).
install_objc_exception_tracker();
exit_ok = true;
atexit(exit_handler);
});


util::OnDeInit restore_handlers([]() {
uninstall_objc_exception_tracker();
std::set_terminate(old_terminate_handler);
signal(SIGSEGV, old_sigsegv_handler);
});
Expand Down Expand Up @@ -88,6 +94,15 @@ util::OnDeInit restore_handlers([]() {
}
catch (...) {
std::cout << "non-standard exception object" << std::endl;

// On Apple platforms, an Objective-C NSException thrown across a
// C++ boundary is not derived from std::exception and lands here
// with its details otherwise lost. Dump the name/reason/callStack
// of the current in-flight NSException (if any).
std::string objc_dump = dump_objc_exception();
if (!objc_dump.empty()) {
std::cout << objc_dump << std::endl;
}
}
}

Expand All @@ -98,12 +113,19 @@ util::OnDeInit restore_handlers([]() {
backtrace.analyze();
std::cout << backtrace << std::endl;

// die again to enable debugger functionality.
// that maybe print some additional useful info that we forgot about.
// die again to enable debugger functionality (SIGABRT is catchable by a
// debugger) and to guarantee a non-zero exit status.
//
// We call std::abort() directly rather than std::terminate(): on macOS the
// default terminate handler is libobjc's _objc_terminate(), which calls
// __cxa_rethrow on the still-in-flight exception. The rethrow_exception +
// catch above does NOT clear the exception's __cxa "uncaught" state, so
// _objc_terminate would rethrow -> std::terminate -> _objc_terminate ...,
// looping forever instead of aborting. std::abort() terminates unconditionally.
// TODO: we maybe want to prevent that for end-users.
std::cout << "\x1b[33mhanding over to the system...\x1b[m\n"
<< std::endl;
std::terminate();
std::abort();
}


Expand Down
41 changes: 41 additions & 0 deletions libopenage/error/objc_exception.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright 2025 the openage authors. See copying.md for legal info.

#pragma once

#include <string>

namespace openage {
namespace error {

/**
* Install a process-wide Objective-C exception preprocessor (Apple platforms
* only; no-op elsewhere) that records the most recent NSException thrown on
* each thread, so the terminate handler can later report it.
*
* Must be paired with uninstall_objc_exception_tracker(). Typically called
* once at library load and uninstalled at library unload.
*
* The preprocessor only observes exceptions (it returns the exception
* unchanged) so it does not alter program behaviour.
*/
void install_objc_exception_tracker();

/**
* Undo install_objc_exception_tracker(). No-op on non-Apple platforms.
*/
void uninstall_objc_exception_tracker();

/**
* Return a dump (name, reason, callStackSymbols) of the most recent NSException
* recorded on the current thread, or an empty string if there was none.
*
* This does NOT rethrow or otherwise disturb the in-flight exception, so it is
* safe to call from a catch(...) arm of a terminate handler. (An earlier
* implementation rethrew via objc_exception_rethrow + @catch; that disturbed the
* C++ exception runtime and turned the subsequent std::terminate() into an
* infinite std::terminate <-> __terminate loop instead of aborting.)
*/
std::string dump_objc_exception();

} // namespace error
} // namespace openage
99 changes: 99 additions & 0 deletions libopenage/error/objc_exception.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#include "objc_exception.h"

#ifdef __APPLE__

#import <Foundation/Foundation.h>
#include <objc/objc-exception.h>
#include <sstream>
#include <string>

namespace openage {
namespace error {

namespace {

// The most recent NSException thrown on the current thread, captured by the
// preprocessor at throw time. Held as a raw pointer (non-ARC); while an
// exception is in flight and propagating to terminate(), the runtime keeps it
// alive via the C++ exception object, so this is valid to read in the
// terminate handler.
thread_local NSException *last_objc_exception = nil;

// Call stack captured at throw time as a plain string (independent of ARC
// reference counting). NSException's own callStackSymbols may not be populated
// yet when the preprocessor runs, so we grab [NSThread callStackSymbols]
// ourselves at the throw point.
thread_local std::string last_objc_callstack;

// Previous preprocessor, restored on uninstall so we do not clobber a host
// application's own ObjC exception preprocessor.
objc_exception_preprocessor previous_preprocessor = nullptr;

} // namespace

// Invoked by objc_exception_throw() for every NSException thrown. We record it
// per-thread (so the terminate handler, which runs on the throwing thread, can
// read it) and return the exception unchanged. Recording at throw time - rather
// than rethrowing inside catch(...) - means we never disturb the C++ exception
// runtime, so std::terminate()/abort() still terminates cleanly.
static id exception_preprocessor(id exception) {
if (exception != nil && [exception isKindOfClass:[NSException class]]) {
last_objc_exception = (NSException *)exception;

std::string s;
for (NSString *symbol in [NSThread callStackSymbols]) {
s += " ";
s += [symbol UTF8String];
s += "\n";
}
last_objc_callstack = std::move(s);
}
return exception;
}

void install_objc_exception_tracker() {
previous_preprocessor = objc_setExceptionPreprocessor(exception_preprocessor);
}

void uninstall_objc_exception_tracker() {
objc_setExceptionPreprocessor(previous_preprocessor);
}

std::string dump_objc_exception() {
NSException *exc = last_objc_exception;
if (exc == nil) {
return "";
}

std::ostringstream ss;
ss << "Objective-C NSException caught across C++ boundary:\n";
ss << " name: " << [[exc name] UTF8String] << "\n";
ss << " reason: " << [[exc reason] UTF8String] << "\n";
if (!last_objc_callstack.empty()) {
ss << " callStackSymbols (at throw):\n" << last_objc_callstack;
}
return ss.str();
}

} // namespace error
} // namespace openage

#else // !__APPLE__

namespace openage {
namespace error {

void install_objc_exception_tracker() {
}

void uninstall_objc_exception_tracker() {
}

std::string dump_objc_exception() {
return "";
}

} // namespace error
} // namespace openage

#endif // __APPLE__
Loading