Skip to content

Commit 65ab53c

Browse files
committed
Mark ffi interface functions as noexcept
Mark Rust ffi function as noexcept to prevent exception propagation to Rust code. Any unhandled exceptions will result in direct call to terminate(). This commit adds a custom terminate handler that provides detailed diagnostics when std::terminate() is called. This helps debug issues where exceptions escape noexcept functions or cross FFI boundaries. The handler is installed via monad_set_terminate_handler() which should be called early in main() before any exception-throwing code. This code was generated using Claude Sonnet 4.5
1 parent f1ca632 commit 65ab53c

15 files changed

+375
-80
lines changed

category/core/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ add_library(
7474
"assert.h"
7575
"backtrace.cpp"
7676
"backtrace.hpp"
77+
"terminate_handler.cpp"
78+
"terminate_handler.h"
7779
"basic_formatter.hpp"
7880
"blake3.hpp"
7981
"byte_string.hpp"

category/core/config.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@
3535
} \
3636
MONAD_NAMESPACE_END
3737

38+
// Macro for marking C++ functions as noexcept in C++ mode, empty in C mode
39+
// This is primarily used for extern "C" functions that should not throw
40+
#ifdef __cplusplus
41+
#define MONAD_NOEXCEPT noexcept
42+
#else
43+
#define MONAD_NOEXCEPT
44+
#endif
45+
3846
static_assert(CHAR_BIT == 8);
3947

4048
static_assert(
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// Copyright (C) 2025 Category Labs, Inc.
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU General Public License as published by
5+
// the Free Software Foundation, either version 3 of the License, or
6+
// (at your option) any later version.
7+
//
8+
// This program is distributed in the hope that it will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU General Public License
14+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
16+
#include <category/core/terminate_handler.h>
17+
18+
#include <cxxabi.h>
19+
#include <exception>
20+
#include <stdio.h>
21+
#include <stdlib.h>
22+
#include <string.h>
23+
#include <typeinfo>
24+
#include <unistd.h>
25+
26+
extern char const *__progname; // NOLINT(bugprone-reserved-identifier)
27+
28+
extern "C" void monad_stack_backtrace_capture_and_print(
29+
char *buffer, size_t size, int fd, unsigned indent,
30+
bool print_async_unsafe_info);
31+
32+
namespace
33+
{
34+
void monad_terminate_handler_impl() noexcept
35+
{
36+
char buffer[16384];
37+
ssize_t written = 0;
38+
39+
// Print header
40+
written = snprintf(
41+
buffer,
42+
sizeof(buffer),
43+
"\n"
44+
"=================================================================="
45+
"==============\n"
46+
"%s: std::terminate() called\n"
47+
"=================================================================="
48+
"==============\n",
49+
__progname);
50+
51+
if (written > 0 && (size_t)written < sizeof(buffer)) {
52+
if (write(STDERR_FILENO, buffer, (size_t)written) == -1) {
53+
// Suppress warning
54+
}
55+
}
56+
57+
// Try to get exception information
58+
std::type_info *exception_type = abi::__cxa_current_exception_type();
59+
if (exception_type != nullptr) {
60+
char const *exception_name = exception_type->name();
61+
62+
// Try to demangle the name
63+
int status = 0;
64+
char *demangled =
65+
abi::__cxa_demangle(exception_name, nullptr, nullptr, &status);
66+
char const *display_name = (status == 0 && demangled != nullptr)
67+
? demangled
68+
: exception_name;
69+
70+
written = snprintf(
71+
buffer,
72+
sizeof(buffer),
73+
"Reason: Uncaught exception\n"
74+
"Exception type: %s\n",
75+
display_name);
76+
77+
if (written > 0 && (size_t)written < sizeof(buffer)) {
78+
if (write(STDERR_FILENO, buffer, (size_t)written) == -1) {
79+
// Suppress warning
80+
}
81+
}
82+
83+
// Try to get exception message if it's a std::exception
84+
try {
85+
std::rethrow_exception(std::current_exception());
86+
}
87+
catch (std::exception const &e) {
88+
written = snprintf(
89+
buffer,
90+
sizeof(buffer),
91+
"Exception message: %s\n",
92+
e.what());
93+
94+
if (written > 0 && (size_t)written < sizeof(buffer)) {
95+
if (write(STDERR_FILENO, buffer, (size_t)written) == -1) {
96+
// Suppress warning
97+
}
98+
}
99+
}
100+
catch (...) {
101+
// Not a std::exception, no message available
102+
char const *msg = "Exception message: <not a std::exception>\n";
103+
if (write(STDERR_FILENO, msg, strlen(msg)) == -1) {
104+
// Suppress warning
105+
}
106+
}
107+
108+
if (demangled != nullptr) {
109+
free(demangled);
110+
}
111+
}
112+
else {
113+
// No active exception - std::terminate() was called for another
114+
// reason.
115+
char const *msg = "No active exception detected\n";
116+
if (write(STDERR_FILENO, msg, strlen(msg)) == -1) {
117+
// Suppress warning
118+
}
119+
}
120+
121+
char const *separator = "----------------------------------------------"
122+
"----------------------------------\n"
123+
"Stack trace:\n"
124+
"----------------------------------------------"
125+
"----------------------------------\n";
126+
if (write(STDERR_FILENO, separator, strlen(separator)) == -1) {
127+
// Suppress warning
128+
}
129+
130+
monad_stack_backtrace_capture_and_print(
131+
buffer, sizeof(buffer), STDERR_FILENO, 3, true);
132+
133+
char const *footer = "================================================="
134+
"===============================\n"
135+
"Aborting process...\n"
136+
"================================================="
137+
"===============================\n";
138+
if (write(STDERR_FILENO, footer, strlen(footer)) == -1) {
139+
// Suppress warning
140+
}
141+
abort();
142+
}
143+
}
144+
145+
extern "C" void monad_set_terminate_handler()
146+
{
147+
std::set_terminate(monad_terminate_handler_impl);
148+
}

category/core/terminate_handler.h

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (C) 2025 Category Labs, Inc.
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU General Public License as published by
5+
// the Free Software Foundation, either version 3 of the License, or
6+
// (at your option) any later version.
7+
//
8+
// This program is distributed in the hope that it will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU General Public License
14+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
16+
#pragma once
17+
18+
#ifdef __cplusplus
19+
extern "C"
20+
{
21+
#endif
22+
23+
/// Install custom terminate handler that prints exception info and backtrace
24+
/// before aborting. This should be called early in main() to catch exceptions
25+
/// that escape noexcept functions (e.g., FFI boundaries).
26+
void monad_set_terminate_handler();
27+
28+
#ifdef __cplusplus
29+
}
30+
#endif

category/core/test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,5 @@ monad_add_test(monad_exception_test "monad_exception.cpp")
3636
monad_add_test(path_util_test "path_util.cpp")
3737
monad_add_test(priority_pool_test "priority_pool_test.cpp")
3838
set_tests_properties(priority_pool_test PROPERTIES RUN_SERIAL TRUE)
39+
monad_add_test(terminate_handler_test "terminate_handler_test.cpp")
3940
monad_add_test(unordered_map_test "unordered_map.cpp")
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Copyright (C) 2025 Category Labs, Inc.
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU General Public License as published by
5+
// the Free Software Foundation, either version 3 of the License, or
6+
// (at your option) any later version.
7+
//
8+
// This program is distributed in the hope that it will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU General Public License
14+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
16+
#include <category/core/terminate_handler.h>
17+
18+
#include <gtest/gtest.h>
19+
20+
#include <exception>
21+
#include <stdexcept>
22+
23+
namespace
24+
{
25+
26+
void throwing_function()
27+
{
28+
throw std::runtime_error("Test exception from throwing_function");
29+
}
30+
31+
// NOLINTNEXTLINE(bugprone-exception-escape)
32+
void noexcept_function() noexcept
33+
{
34+
// This will call std::terminate because we're throwing from noexcept
35+
throwing_function();
36+
}
37+
38+
} // namespace
39+
40+
// Death tests must be named with DeathTest suffix for proper test ordering
41+
TEST(TerminateHandlerDeathTest, ExceptionEscapingNoexcept)
42+
{
43+
// Install the custom terminate handler
44+
monad_set_terminate_handler();
45+
46+
// Verify that calling noexcept_function causes termination
47+
// and that the output contains expected exception information
48+
EXPECT_DEATH(
49+
{ noexcept_function(); },
50+
// Regex pattern matching expected output:
51+
// - Should contain "std::terminate" or "terminate()"
52+
// - Should contain the exception type "runtime_error"
53+
// - Should contain the exception message
54+
// - Should contain "Stack trace"
55+
"std::terminate.*"
56+
".*runtime_error.*"
57+
".*Test exception from throwing_function.*"
58+
".*Stack trace.*");
59+
}
60+
61+
TEST(TerminateHandlerDeathTest, DirectTerminateCall)
62+
{
63+
// Install the custom terminate handler
64+
monad_set_terminate_handler();
65+
66+
// Verify that directly calling std::terminate works
67+
EXPECT_DEATH(
68+
{ std::terminate(); },
69+
// Should indicate no active exception
70+
"std::terminate.*"
71+
".*No active exception detected.*"
72+
".*Stack trace.*");
73+
}
74+
75+
#ifdef __clang__
76+
#pragma clang diagnostic push
77+
#pragma clang diagnostic ignored "-Wexceptions"
78+
#else
79+
#pragma GCC diagnostic push
80+
#pragma GCC diagnostic ignored "-Wterminate"
81+
#endif
82+
83+
// Helper function for testing different exception type
84+
// NOLINTNEXTLINE(bugprone-exception-escape)
85+
[[noreturn]] void throw_logic_error_noexcept() noexcept
86+
{
87+
throw std::logic_error("Logic error test");
88+
}
89+
90+
#ifdef __clang__
91+
#pragma clang diagnostic pop
92+
#else
93+
#pragma GCC diagnostic pop
94+
#endif
95+
96+
TEST(TerminateHandlerDeathTest, ExceptionTypeInOutput)
97+
{
98+
// Install the custom terminate handler
99+
monad_set_terminate_handler();
100+
101+
// Test with a different exception type
102+
EXPECT_DEATH(
103+
{ throw_logic_error_noexcept(); },
104+
"std::terminate.*"
105+
".*logic_error.*"
106+
".*Logic error test.*");
107+
}

0 commit comments

Comments
 (0)