Skip to content

Commit cb843d3

Browse files
committed
Add win32 backend
1 parent c93d57e commit cb843d3

5 files changed

Lines changed: 305 additions & 4 deletions

File tree

‎CMakeLists.txt‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@ if(PROJECT_IS_TOP_LEVEL)
2424
set(CMAKE_CXX_SCAN_FOR_MODULES OFF)
2525
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
2626
set(CMAKE_VISIBILITY_INLINES_HIDDEN ON)
27-
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT
28-
"$<$<CONFIG:Debug,RelWithDebInfo>:ProgramDatabase>"
29-
)
27+
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT ProgramDatabase)
28+
if(MSVC)
29+
string(APPEND CMAKE_EXE_LINKER_FLAGS " /DEBUG")
30+
string(APPEND CMAKE_SHARED_LINKER_FLAGS " /DEBUG")
31+
endif()
3032

3133
option(BUILD_EXAMPLES "Build the examples" ON)
3234
include(CTest)
@@ -52,6 +54,7 @@ endif()
5254
set(EGGS_STACKTRACE_BACKEND_EXPORT_null Null)
5355
set(EGGS_STACKTRACE_BACKEND_EXPORT_execinfo Execinfo)
5456
set(EGGS_STACKTRACE_BACKEND_EXPORT_libbacktrace Libbacktrace)
57+
set(EGGS_STACKTRACE_BACKEND_EXPORT_win32 Win32)
5558

5659
# -- Common OBJECT library ----
5760

@@ -158,13 +161,19 @@ if(EGGS_STACKTRACE_HAVE_LIBBACKTRACE)
158161
)
159162
endif()
160163

164+
if(WIN32)
165+
eggs_stacktrace_add_backend(win32 DbgHelp)
166+
endif()
167+
161168
# -- Main alias ----------------------------------------------------------------
162169
# Note: ALIAS cannot alias another ALIAS, so we alias the internal target name.
163170

164171
if(TARGET _eggs_stacktrace_libbacktrace)
165172
set(EGGS_STACKTRACE_DEFAULT_BACKEND libbacktrace)
166173
elseif(TARGET _eggs_stacktrace_execinfo)
167174
set(EGGS_STACKTRACE_DEFAULT_BACKEND execinfo)
175+
elseif(TARGET _eggs_stacktrace_win32)
176+
set(EGGS_STACKTRACE_DEFAULT_BACKEND win32)
168177
else()
169178
set(EGGS_STACKTRACE_DEFAULT_BACKEND null)
170179
endif()
@@ -231,7 +240,6 @@ if(ENABLE_INSTALL)
231240
FILES
232241
"${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>/eggs_stacktrace_${name}.pdb"
233242
DESTINATION ${CMAKE_INSTALL_LIBDIR}
234-
CONFIGURATIONS Debug RelWithDebInfo
235243
OPTIONAL
236244
)
237245
endif()
@@ -246,6 +254,7 @@ if(ENABLE_INSTALL)
246254
eggs_stacktrace_install_backend(null)
247255
eggs_stacktrace_install_backend(execinfo)
248256
eggs_stacktrace_install_backend(libbacktrace)
257+
eggs_stacktrace_install_backend(win32)
249258

250259
install(
251260
FILES

‎README.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ Replace `dev-gcc` with `dev-clang`, `dev-clang-libcxx`, or `dev-msvc` as appropr
2828

2929
`Eggs::Stacktrace` aliases the best available backend on the current platform.
3030

31+
> **Note:** DbgHelp is not thread-safe. The win32 backend serializes its own
32+
> `Sym*` calls, but not those made elsewhere in the process.
33+
3134
## Usage
3235

3336
```cpp

‎src/stacktrace_win32.cpp‎

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// Eggs.Stacktrace
2+
//
3+
// Copyright (c) 2026 Agustin Berge
4+
//
5+
// Distributed under the Boost Software License, Version 1.0.
6+
// See accompanying file LICENSE.txt or copy at
7+
// http://www.boost.org/LICENSE_1_0.txt
8+
9+
#include <eggs/stacktrace.hpp>
10+
11+
#include <algorithm>
12+
#include <cstddef>
13+
#include <cstdint>
14+
#include <limits>
15+
#include <mutex>
16+
#include <string>
17+
#include <vector>
18+
19+
#include "detail/backend.hpp"
20+
21+
#define WIN32_LEAN_AND_MEAN
22+
#define NOMINMAX
23+
#include <windows.h>
24+
// <dbghelp.h> requires <windows.h> to be included first.
25+
#include <dbghelp.h>
26+
27+
namespace eggs {
28+
namespace detail {
29+
30+
namespace {
31+
32+
// DbgHelp is not thread-safe; serialize the Sym* calls made by this backend.
33+
// DbgHelp state is process-global, so this does not protect against Sym*
34+
// calls made elsewhere in the process.
35+
std::mutex& dbg_mutex() noexcept
36+
{
37+
static std::mutex m;
38+
return m;
39+
}
40+
41+
// One-time initialization, must run before any other Sym* call.
42+
bool dbg_init() noexcept
43+
{
44+
static bool const ok = []() noexcept -> bool {
45+
::SymSetOptions(
46+
SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS | SYMOPT_LOAD_LINES
47+
);
48+
if (::SymInitialize(::GetCurrentProcess(), nullptr, TRUE) != FALSE)
49+
return true;
50+
// Already initialized by some other module in the process.
51+
return ::GetLastError() == ERROR_INVALID_PARAMETER;
52+
}();
53+
return ok;
54+
}
55+
56+
// Sym* lookups only see modules known at the last enumeration; retry once
57+
// after a refresh to pick up modules loaded since.
58+
template <typename F>
59+
auto with_module_refresh(F const& f) noexcept -> decltype(f())
60+
{
61+
auto result = f();
62+
if (!result) {
63+
::SymRefreshModuleList(::GetCurrentProcess());
64+
result = f();
65+
}
66+
return result;
67+
}
68+
69+
// Captured addresses are return addresses; subtract 1 to point into the
70+
// call instruction itself.
71+
IMAGEHLP_LINE64 get_line_info(void* address) noexcept
72+
{
73+
IMAGEHLP_LINE64 line = {};
74+
line.SizeOfStruct = sizeof(IMAGEHLP_LINE64);
75+
if (!address || !dbg_init()) return line;
76+
77+
DWORD displacement = 0;
78+
DWORD64 const addr = reinterpret_cast<DWORD64>(address) - 1;
79+
80+
std::lock_guard<std::mutex> const lock(dbg_mutex());
81+
bool const found = with_module_refresh([&]() noexcept {
82+
return ::SymGetLineFromAddr64(
83+
::GetCurrentProcess(), addr, &displacement, &line
84+
) != FALSE;
85+
});
86+
if (!found) return IMAGEHLP_LINE64{};
87+
return line;
88+
}
89+
90+
} // namespace
91+
92+
void capture(
93+
std::vector<stacktrace_entry>& frames, std::size_t skip,
94+
std::size_t max_depth
95+
) noexcept
96+
{
97+
if (max_depth == 0) return;
98+
99+
// +1 to skip this capture() frame itself in the backtrace.
100+
constexpr std::size_t overhead = 1;
101+
if (skip >
102+
static_cast<std::size_t>(std::numeric_limits<ULONG>::max()) - overhead)
103+
return;
104+
ULONG const first = static_cast<ULONG>(skip + overhead);
105+
106+
constexpr std::size_t kInitial = 128;
107+
// The captured count is reported as USHORT.
108+
constexpr std::size_t kHardLimit = std::numeric_limits<USHORT>::max();
109+
110+
void* stack_buf[kInitial];
111+
void** buf = stack_buf;
112+
std::size_t cap = kInitial;
113+
USHORT got =
114+
::CaptureStackBackTrace(first, static_cast<ULONG>(cap), buf, nullptr);
115+
116+
std::vector<void*> heap_buf;
117+
try {
118+
while (static_cast<std::size_t>(got) == cap && cap < kHardLimit) {
119+
if (static_cast<std::size_t>(got) >= max_depth) break;
120+
121+
cap = std::min(cap * 2, kHardLimit);
122+
heap_buf.resize(cap);
123+
buf = heap_buf.data();
124+
got = ::CaptureStackBackTrace(
125+
first, static_cast<ULONG>(cap), buf, nullptr
126+
);
127+
}
128+
} catch (...) {
129+
return;
130+
}
131+
132+
std::size_t const take = std::min(static_cast<std::size_t>(got), max_depth);
133+
try {
134+
for (std::size_t i = 0; i < take; ++i)
135+
frames.push_back(capture_helper::make(buf[i]));
136+
} catch (...) {
137+
// Keep the frames captured so far.
138+
}
139+
}
140+
141+
std::string symbolize_description(void* address)
142+
{
143+
if (!address || !dbg_init()) return {};
144+
145+
// SYMBOL_INFO::Name is a trailing array; make room for kMaxNameLen chars.
146+
constexpr ULONG kMaxNameLen = 256;
147+
148+
struct
149+
{
150+
SYMBOL_INFO sym;
151+
char overflow[kMaxNameLen - 1];
152+
} buf = {};
153+
154+
buf.sym.SizeOfStruct = sizeof(SYMBOL_INFO);
155+
buf.sym.MaxNameLen = kMaxNameLen;
156+
157+
std::lock_guard<std::mutex> const lock(dbg_mutex());
158+
DWORD64 const addr = reinterpret_cast<DWORD64>(address) - 1;
159+
bool const found = with_module_refresh([&]() noexcept {
160+
return ::SymFromAddr(::GetCurrentProcess(), addr, nullptr, &buf.sym) !=
161+
FALSE;
162+
});
163+
if (!found) return {};
164+
return buf.sym.Name;
165+
}
166+
167+
std::string symbolize_source_file(void* address)
168+
{
169+
IMAGEHLP_LINE64 const line = get_line_info(address);
170+
return line.FileName ? std::string(line.FileName) : std::string{};
171+
}
172+
173+
std::uint_least32_t symbolize_source_line(void* address)
174+
{
175+
IMAGEHLP_LINE64 const line = get_line_info(address);
176+
return static_cast<std::uint_least32_t>(line.LineNumber);
177+
}
178+
179+
} // namespace detail
180+
} // namespace eggs

‎test/CMakeLists.txt‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,29 @@ if(TARGET Eggs::Stacktrace::Execinfo)
129129
PROPERTIES LABELS "unit"
130130
)
131131
endif()
132+
133+
# -- win32 backend test ---------------------------------------------------------
134+
135+
if(TARGET Eggs::Stacktrace::Win32)
136+
add_executable(eggs.stacktrace.test.stacktrace_win32 stacktrace_win32.cpp)
137+
target_sources(
138+
eggs.stacktrace.test.stacktrace_win32
139+
PRIVATE
140+
FILE_SET private_headers
141+
TYPE HEADERS
142+
BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}
143+
FILES support.hpp
144+
)
145+
target_link_libraries(
146+
eggs.stacktrace.test.stacktrace_win32
147+
PRIVATE Eggs::Stacktrace::Win32
148+
)
149+
add_test(
150+
NAME eggs.stacktrace.stacktrace_win32
151+
COMMAND eggs.stacktrace.test.stacktrace_win32
152+
)
153+
set_tests_properties(
154+
eggs.stacktrace.stacktrace_win32
155+
PROPERTIES LABELS "unit"
156+
)
157+
endif()

‎test/stacktrace_win32.cpp‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Eggs.Stacktrace
2+
//
3+
// Copyright (c) 2026 Agustin Berge
4+
//
5+
// Distributed under the Boost Software License, Version 1.0.
6+
// See accompanying file LICENSE.txt or copy at
7+
// http://www.boost.org/LICENSE_1_0.txt
8+
9+
#include <eggs/stacktrace.hpp>
10+
11+
#include <cstddef>
12+
#include <limits>
13+
#include <string>
14+
15+
#include "support.hpp"
16+
17+
// PDBs are generated for every configuration, so this always resolves.
18+
__declspec(noinline) void eggs_stacktrace_win32_sentinel()
19+
{
20+
auto st = eggs::stacktrace::current(0);
21+
if (!EGGS_STACKTRACE_CHECK(!st.empty())) {
22+
EGGS_STACKTRACE_TRACE(st.size());
23+
return;
24+
}
25+
26+
// Frame 0 must be this function.
27+
std::string const desc = st[0].description();
28+
if (!EGGS_STACKTRACE_CHECK(!desc.empty())) {
29+
EGGS_STACKTRACE_TRACE(desc);
30+
}
31+
if (!EGGS_STACKTRACE_CHECK(
32+
desc.find("eggs_stacktrace_win32_sentinel") != std::string::npos
33+
)) {
34+
EGGS_STACKTRACE_TRACE(desc);
35+
}
36+
37+
if (!EGGS_STACKTRACE_CHECK(!st[0].source_file().empty())) {
38+
EGGS_STACKTRACE_TRACE(st[0].source_file());
39+
}
40+
if (!EGGS_STACKTRACE_CHECK(st[0].source_line() != 0U)) {
41+
EGGS_STACKTRACE_TRACE(st[0].source_line());
42+
}
43+
}
44+
45+
int main()
46+
{
47+
// -- current() returns non-empty --------------------------------------------
48+
49+
auto const st = eggs::stacktrace::current(0);
50+
if (!EGGS_STACKTRACE_CHECK(!st.empty())) {
51+
EGGS_STACKTRACE_TRACE(st.size());
52+
}
53+
54+
// -- skip reduces frame count by exactly 1 ----------------------------------
55+
56+
auto const st0 = eggs::stacktrace::current(0);
57+
auto const st1 = eggs::stacktrace::current(1);
58+
if (!EGGS_STACKTRACE_CHECK(st1.size() + 1 == st0.size())) {
59+
EGGS_STACKTRACE_TRACE(st0.size());
60+
EGGS_STACKTRACE_TRACE(st1.size());
61+
}
62+
63+
// -- max_depth caps the result -----------------------------------------------
64+
65+
auto const st_capped = eggs::stacktrace::current(0, 1);
66+
if (!EGGS_STACKTRACE_CHECK(st_capped.size() == 1)) {
67+
EGGS_STACKTRACE_TRACE(st_capped.size());
68+
}
69+
70+
// -- description and source info resolve via PDB ----------------------------
71+
72+
eggs_stacktrace_win32_sentinel();
73+
74+
// -- skip beyond the representable range returns empty ----------------------
75+
76+
auto const st_max_skip =
77+
eggs::stacktrace::current(std::numeric_limits<std::size_t>::max());
78+
if (!EGGS_STACKTRACE_CHECK(st_max_skip.empty())) {
79+
EGGS_STACKTRACE_TRACE(st_max_skip.size());
80+
}
81+
82+
return eggs::test_support::report();
83+
}

0 commit comments

Comments
 (0)