Helios Engine
A modular ECS based data-oriented C++23 game engine framework
Loading...
Searching...
No Matches
stacktrace.cpp
Go to the documentation of this file.
1#include <pch.hpp>
2
5
6#include <algorithm>
7#include <cctype>
8#include <format>
9#include <iterator>
10#include <string>
11#include <vector>
12
13#ifdef HELIOS_USE_STL_STACKTRACE
14#include <stacktrace>
15#else
16#include <boost/stacktrace.hpp>
17#endif
18
19namespace {
20
21constexpr size_t kEntryReserveSize = 512;
22
24 std::vector<std::string> frames;
25 bool capture_error = false;
26};
27
28[[nodiscard]] constexpr bool IsValidPrecedingChar(char ch) noexcept {
29 return ch == ' ' || ch == '\t' || ch == '/' || ch == '\\' || ch == ':';
30}
31
32[[nodiscard]] constexpr bool IsValidFollowingChar(char ch) noexcept {
33 return ch == ' ' || ch == '\t' || ch == ')' || ch == '\n' || ch == '\r';
34}
35
36[[nodiscard]] bool IsExactSourceMatch(const std::string& stacktrace_entry,
37 const std::string& target_pattern) {
38 size_t pos = stacktrace_entry.find(target_pattern);
39 if (pos == std::string::npos) [[likely]] {
40 return false;
41 }
42
43 while (pos != std::string::npos) {
44 const bool valid_start =
45 (pos == 0) || IsValidPrecedingChar(stacktrace_entry[pos - 1]);
46 const size_t end_pos = pos + target_pattern.length();
47 const bool valid_end = (end_pos == stacktrace_entry.length()) ||
48 IsValidFollowingChar(stacktrace_entry[end_pos]);
49
50 if (valid_start && valid_end) [[unlikely]] {
51 return true;
52 }
53
54 pos = stacktrace_entry.find(target_pattern, pos + 1);
55 }
56
57 return false;
58}
59
60[[nodiscard]] std::string NormalizePathSeparators(std::string_view path) {
61 std::string normalized(path);
62 std::ranges::replace(normalized, '\\', '/');
63 return normalized;
64}
65
66[[nodiscard]] bool ContainsLineNumberToken(const std::string& stacktrace_entry,
67 uint_least32_t line) {
68 if (line == 0) {
69 return true;
70 }
71
72 const std::string line_token = std::to_string(line);
73 size_t pos = stacktrace_entry.find(line_token);
74 while (pos != std::string::npos) {
75 const bool valid_start =
76 (pos == 0) ||
77 !std::isdigit(static_cast<unsigned char>(stacktrace_entry[pos - 1]));
78 const size_t end_pos = pos + line_token.size();
79 const bool valid_end =
80 (end_pos == stacktrace_entry.size()) ||
81 !std::isdigit(static_cast<unsigned char>(stacktrace_entry[end_pos]));
82
83 if (valid_start && valid_end) {
84 return true;
85 }
86
87 pos = stacktrace_entry.find(line_token, pos + 1);
88 }
89
90 return false;
91}
92
93[[nodiscard]] size_t ClampStartFrame(size_t start_frame,
94 size_t stack_size) noexcept {
95 if (stack_size == 0) [[unlikely]] {
96 return 0;
97 }
98 return std::min(start_frame, stack_size - 1);
99}
100
101template <typename Trace>
102[[nodiscard]] std::string ToEntryString(const Trace& stack_trace,
103 size_t index) {
104#ifdef HELIOS_USE_STL_STACKTRACE
105 return std::to_string(stack_trace[index]);
106#else
107 const auto& frame = stack_trace[index];
108
109 std::string function_name;
110 std::string source_file;
111 size_t source_line = 0;
112
113 try {
114 function_name = frame.name();
115 } catch (...) {
116 }
117
118 try {
119 source_file = frame.source_file();
120 source_line = frame.source_line();
121 } catch (...) {
122 }
123
124 if (!function_name.empty() && !source_file.empty() && source_line != 0) {
125 return std::format("{} at {}:{}", function_name, source_file, source_line);
126 }
127
128 if (!function_name.empty() && !source_file.empty()) {
129 return std::format("{} in {}", function_name, source_file);
130 }
131
132 if (!function_name.empty()) {
133 return function_name;
134 }
135
136 return boost::stacktrace::to_string(frame);
137#endif
138}
139
140template <typename Trace>
141[[nodiscard]] size_t FindStartingFrame(const Trace& stack_trace,
142 const helios::StacktraceConfig& config) {
143 size_t start_frame = ClampStartFrame(config.start_frame, stack_trace.size());
144
145 const bool has_source_file = !config.source_file.empty();
146 const bool has_source_function = !config.source_function.empty();
147 if (!has_source_file && !has_source_function) [[likely]] {
148 return start_frame;
149 }
150
151 std::string entry_str;
152 std::string normalized_source_file;
153 std::string source_file_name;
154 size_t first_file_only_match = stack_trace.size();
155
156 try {
157 if (has_source_file) {
158 normalized_source_file = NormalizePathSeparators(config.source_file);
159 source_file_name =
160 std::string(helios::utils::GetFileName(config.source_file));
161 }
162 entry_str.reserve(kEntryReserveSize);
163 } catch (...) {
164 return start_frame;
165 }
166
167 for (size_t index = start_frame; index < stack_trace.size(); ++index) {
168 try {
169 entry_str.clear();
170 entry_str = ToEntryString(stack_trace, index);
171
172 if (has_source_function && entry_str.contains(config.source_function))
173 [[unlikely]] {
174 return index;
175 }
176
177 if (!has_source_file) {
178 continue;
179 }
180
181 const std::string normalized_entry = NormalizePathSeparators(entry_str);
182 const bool matches_full_path =
183 !normalized_source_file.empty() &&
184 normalized_entry.contains(normalized_source_file);
185 const bool matches_file_name =
186 !source_file_name.empty() &&
187 IsExactSourceMatch(normalized_entry, source_file_name);
188 const bool matches_file = matches_full_path || matches_file_name;
189 if (!matches_file) {
190 continue;
191 }
192
193 if (ContainsLineNumberToken(entry_str, config.source_line)) [[unlikely]] {
194 return index;
195 }
196
197 if (first_file_only_match == stack_trace.size()) {
198 first_file_only_match = index;
199 }
200 } catch (...) {
201 continue;
202 }
203 }
204
205 if (first_file_only_match != stack_trace.size()) {
206 return first_file_only_match;
207 }
208
209 return start_frame;
210}
211
212template <typename Trace>
213[[nodiscard]] size_t ComputeEndFrame(const Trace& stack_trace,
214 const helios::StacktraceConfig& config,
215 size_t start_frame) {
216 const size_t stack_size = stack_trace.size();
217 size_t end_frame = stack_size;
218
219 if (config.max_depth > 0) {
220 end_frame = std::min(end_frame, start_frame + config.max_depth);
221 }
222
223 if (config.max_frames > 0) {
224 end_frame = std::min(end_frame, start_frame + config.max_frames);
225 }
226
227 if (config.stop_before.empty()) [[likely]] {
228 return end_frame;
229 }
230
231 for (size_t index = start_frame; index < end_frame; ++index) {
232 try {
233 const std::string entry_str = ToEntryString(stack_trace, index);
234 if (entry_str.contains(config.stop_before)) {
235 return index;
236 }
237 } catch (...) {
238 continue;
239 }
240 }
241
242 return end_frame;
243}
244
245template <typename Trace>
247 const Trace& stack_trace, const helios::StacktraceConfig& config) {
249 if (stack_trace.size() <= 1 || config.max_frames == 0) {
250 return result;
251 }
252
253 const size_t initial_start =
254 FindStartingFrame(stack_trace, config) + config.skip_frames;
255 const size_t start_frame = ClampStartFrame(initial_start, stack_trace.size());
256 const size_t end_frame = ComputeEndFrame(stack_trace, config, start_frame);
257
258 if (start_frame >= end_frame) {
259 return result;
260 }
261
262 result.frames.reserve(end_frame - start_frame);
263
264 for (size_t index = start_frame; index < end_frame; ++index) {
265 try {
266 const std::string entry = ToEntryString(stack_trace, index);
267 if (!config.include_frames_containing.empty() &&
268 !entry.contains(config.include_frames_containing)) {
269 continue;
270 }
271 if (!config.exclude_frames_containing.empty() &&
272 entry.contains(config.exclude_frames_containing)) {
273 continue;
274 }
275 result.frames.push_back(entry);
276 } catch (...) {
277 result.capture_error = true;
278 }
279 }
280
281 return result;
282}
283
284} // namespace
285
286namespace helios {
287
289 try {
290#ifdef HELIOS_USE_STL_STACKTRACE
291 const std::stacktrace stack_trace = std::stacktrace::current();
292#else
293 const boost::stacktrace::stacktrace stack_trace;
294#endif
295
296 auto capture_result = CaptureFrames(stack_trace, config);
297 return {std::move(capture_result.frames), capture_result.capture_error};
298 } catch (...) {
299 return {{}, true};
300 }
301}
302
303std::string Stacktrace::ToString(bool include_header) const {
304 if (frames_.empty()) {
305 if (capture_error_) {
306 if (include_header) {
307 return "Stack trace: <error>";
308 }
309 return "<error>";
310 }
311 if (include_header) {
312 return "Stack trace: <empty>";
313 }
314 return "<empty>";
315 }
316
317 std::string result;
318 constexpr size_t estimated_frame_size = 64;
319 result.reserve((frames_.size() * estimated_frame_size) +
320 (include_header ? 16 : 0));
321
322 if (include_header) {
323 result.append("Stack trace:");
324 }
325
326 for (size_t index = 0; index < frames_.size(); ++index) {
327 std::format_to(std::back_inserter(result), "\n {}: {}", index + 1,
328 frames_[index]);
329 }
330
331 return result;
332}
333
334} // namespace helios
std::string ToString(bool include_header=true) const
Converts stacktrace to a formatted string.
static Stacktrace Capture(const StacktraceConfig &config={})
Captures stacktrace using provided config.
constexpr Stacktrace()=default
bool ContainsLineNumberToken(const std::string &stacktrace_entry, uint_least32_t line)
constexpr bool IsValidPrecedingChar(char ch) noexcept
std::string NormalizePathSeparators(std::string_view path)
size_t ClampStartFrame(size_t start_frame, size_t stack_size) noexcept
size_t FindStartingFrame(const Trace &stack_trace, const helios::StacktraceConfig &config)
CapturedFramesResult CaptureFrames(const Trace &stack_trace, const helios::StacktraceConfig &config)
bool IsExactSourceMatch(const std::string &stacktrace_entry, const std::string &target_pattern)
constexpr bool IsValidFollowingChar(char ch) noexcept
std::string ToEntryString(const Trace &stack_trace, size_t index)
size_t ComputeEndFrame(const Trace &stack_trace, const helios::StacktraceConfig &config, size_t start_frame)
constexpr std::string_view GetFileName(std::string_view path)
Extracts the file name from a given path.
Configuration for stacktrace capture and formatting.
size_t max_frames
Maximum number of frames to emit after filtering.
std::string_view source_file
Source file used to infer a relevant starting frame.
std::string_view stop_before
Marker to stop capture before frames that contain this token.
std::string_view exclude_frames_containing
Drop frames that contain this token.
std::string_view include_frames_containing
Keep only frames that contain this token.
size_t skip_frames
Additional number of frames to skip from the computed start.
size_t max_depth
Maximum number of frames to scan from start frame.
uint_least32_t source_line
Source line used to infer a relevant starting frame.
std::string_view source_function
Source function used to infer a relevant starting frame.