On 11/1/2025 9:31 AM, Euler Taveira wrote:
On Sat, Nov 1, 2025, at 1:40 AM, Bryan Green wrote:
v5 patch attached, incorporating all of Euler's feedback with a caveat
around unicode.


Thanks for sharing another patch.


Thanks for the continued review. v6 patch attached with your latest suggestions incorporated.

The most interesting aspect turned out to be the encoding conversion for
symbol names and file paths. Initially I tried using the generic
SYMBOL_INFO and SymFromAddr functions as Euler suggested, but ran into a
subtle issue: on PostgreSQL's Windows builds, these become SYMBOL_INFOA
and SymFromAddrA (the ANSI versions), which return strings in whatever
Windows ANSI codepage happens to be active (CP1252, etc). This doesn't
necessarily match the database encoding.


Odd. Does the build define UNICODE? (Don't have a Windows machine right now to
explore this case.)


PostgreSQL's Windows builds don't define UNICODE globally. I verified this by checking that SYMBOL_INFO resolves to the ANSI version - when I tried using it with wchar2char(), the conversion failed because the input wasn't actually wide characters.
The solution was to use the explicit Unicode versions (SYMBOL_INFOW and
SymFromAddrW), which reliably return UTF-16 strings that wchar2char()
can properly convert to the database encoding. This handles both UTF-8
and non-UTF-8 databases correctly, and wchar2char() gracefully returns
-1 on conversion failure rather than throwing errors during error
handling. Of course this also necessitated using IMAGEHLP_LINEW64 and
SymGetLineFromAddrW64.


Works for me.

This patch also adds a check for zero frames returned by backtrace() on
Unix/Linux platforms, which can occur in certain circumstances such as
ARM builds without unwind tables.


Please create a separate patch.

Removed the check - I'll submit that separately.


+                                       if (result != (size_t) -1)
+                                       {
+                                               appendStringInfo(&errtrace,
+                                                                                
"\n%s+0x%llx [0x%llx] [%s:%lu]",
+                                                                               
 symbol_name,
+                                                                               
 (unsigned long long) displacement,
+                                                                               
 (unsigned long long) address,
+                                                                               
 filename,
+                                                                               
 (unsigned long) line.LineNumber);
+                                       }
+                                       else
+                                       {
+                                               /* Filename conversion failed, 
omit it */
+                                               appendStringInfo(&errtrace,
+                                                                                
"\n%s+0x%llx [0x%llx]",
+                                                                               
 symbol_name,
+                                                                               
 (unsigned long long) displacement,
+                                                                               
 (unsigned long long) address);
+                                       }
+                               }
+                               else
+                               {
+                                       /* No line info available */
+                                       appendStringInfo(&errtrace,
+                                                                        
"\n%s+0x%llx [0x%llx]",
+                                                                        
symbol_name,
+                                                                        
(unsigned long long) displacement,
+                                                                        
(unsigned long long) address);
+                               }

One last suggestion is to have a single code path for these last two conditions
to avoid duplication. Move this if to outside the "if SymGetLineFromAddrW64".
Merge the comment to reflect both conditions (file conversion failed and no
line info).



Done. Now the code prints symbol+offset+address first, then conditionally appends line info if both SymGetLineFromAddrW64 succeeds and the filename conversion succeeds. This eliminates the duplication.

Also added the backtrace_cleanup() function per Jakub's request to properly release DbgHelp resources via SymCleanup() on process exit. This inadvertently was removed when the TAP tests were removed.

Tested with both debug and release builds. With PDB files present, backtraces now show symbol names, offsets, addresses, filenames, and line numbers. Without PDB files (release build), exported function names are still resolved.

Bryan
From 10cdd598f15d5bcc73cd65cf08138134acdfeda6 Mon Sep 17 00:00:00 2001
From: Bryan Green <[email protected]>
Date: Sat, 1 Nov 2025 23:04:24 -0600
Subject: [PATCH v6] Add backtrace support for Windows using DbgHelp API

Previously, backtrace generation on Windows would return an "unsupported"
message. This patch implements Windows backtrace support using
CaptureStackBackTrace() for capturing the call stack and the DbgHelp API
(SymFromAddrW, SymGetLineFromAddrW64) for symbol resolution.

The implementation provides symbol names, offsets, and addresses. When PDB
files are available, it also includes source file names and line numbers.
Symbol names and file paths are converted from UTF-16 to the database
encoding using wchar2char(), which properly handles both UTF-8 and non-UTF-8
databases on Windows. When symbol information is unavailable or encoding
conversion fails, it falls back to displaying raw addresses.

The implementation uses the explicit Unicode versions of the DbgHelp
functions (SYMBOL_INFOW, SymFromAddrW, IMAGEHLP_LINEW64, SymGetLineFromAddrW64)
rather than the generic versions. This is necessary because the generic
SYMBOL_INFO becomes SYMBOL_INFOA on PostgreSQL's Windows builds (which don't
define UNICODE), providing strings in the Windows ANSI codepage rather than
a predictable encoding that can be reliably converted to the database
encoding.

Symbol handler initialization (SymInitialize) is performed once per process
and cached. If initialization fails, a warning is logged and no backtrace is
generated. The symbol handler is cleaned up via on_proc_exit() to release
DbgHelp resources.

Author: Bryan Green
Reviewed-by: Euler Taveira, Jakub Wartak
---
 src/backend/meson.build        |   5 ++
 src/backend/utils/error/elog.c | 145 ++++++++++++++++++++++++++++++++-
 2 files changed, 148 insertions(+), 2 deletions(-)

diff --git a/src/backend/meson.build b/src/backend/meson.build
index b831a54165..eeb69c4079 100644
--- a/src/backend/meson.build
+++ b/src/backend/meson.build
@@ -1,6 +1,11 @@
 # Copyright (c) 2022-2025, PostgreSQL Global Development Group
 
 backend_build_deps = [backend_code]
+
+if host_system == 'windows' and cc.get_id() == 'msvc'
+  backend_build_deps += cc.find_library('dbghelp')
+endif
+
 backend_sources = []
 backend_link_with = [pgport_srv, common_srv]
 
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index 29643c5143..8baeab6240 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -66,11 +66,15 @@
 #include <execinfo.h>
 #endif
 
+#ifdef _MSC_VER
+#include <dbghelp.h>
+#include <windows.h>
+#endif
+
 #include "access/xact.h"
 #include "common/ip.h"
 #include "libpq/libpq.h"
 #include "libpq/pqformat.h"
-#include "mb/pg_wchar.h"
 #include "miscadmin.h"
 #include "nodes/miscnodes.h"
 #include "pgstat.h"
@@ -140,6 +144,11 @@ static void write_syslog(int level, const char *line);
 static void write_eventlog(int level, const char *line, int len);
 #endif
 
+#ifdef _MSC_VER
+static bool backtrace_symbols_initialized = false;
+static HANDLE backtrace_process = NULL;
+#endif
+
 /* We provide a small stack of ErrorData records for re-entrant cases */
 #define ERRORDATA_STACK_SIZE  5
 
@@ -1116,11 +1125,30 @@ errbacktrace(void)
        return 0;
 }
 
+#ifdef _MSC_VER
+/*
+ * Cleanup function for DbgHelp resources.
+ * Called via on_proc_exit() to release resources allocated by SymInitialize().
+ */
+static void
+backtrace_cleanup(int code, Datum arg)
+{
+       SymCleanup(backtrace_process);
+}
+#endif
+
 /*
  * Compute backtrace data and add it to the supplied ErrorData.  num_skip
  * specifies how many inner frames to skip.  Use this to avoid showing the
  * internal backtrace support functions in the backtrace.  This requires that
  * this and related functions are not inlined.
+ *
+ * Platform-specific implementations:
+ * - Unix/Linux: Uses backtrace() and backtrace_symbols()
+ * - Windows: Uses CaptureStackBackTrace() with DbgHelp for symbol resolution
+ *      (requires PDB files; falls back to exported functions/raw addresses if
+ *      unavailable)
+ * - Other: Returns unsupported message
  */
 static void
 set_backtrace(ErrorData *edata, int num_skip)
@@ -1147,11 +1175,124 @@ set_backtrace(ErrorData *edata, int num_skip)
                        appendStringInfoString(&errtrace,
                                                                   
"insufficient memory for backtrace generation");
        }
+#elif defined(_MSC_VER)
+       {
+               void               *buf[100];
+               int                             nframes;
+               char                    buffer[sizeof(SYMBOL_INFOW) + 
MAX_SYM_NAME * sizeof(wchar_t)];
+               PSYMBOL_INFOW   symbol;
+
+               if (!backtrace_symbols_initialized)
+               {
+                       backtrace_process = GetCurrentProcess();
+
+                       SymSetOptions(SYMOPT_UNDNAME |
+                                                 SYMOPT_DEFERRED_LOADS |
+                                                 SYMOPT_LOAD_LINES |
+                                                 SYMOPT_FAIL_CRITICAL_ERRORS);
+
+                       if (SymInitialize(backtrace_process, NULL, TRUE))
+                       {
+                               backtrace_symbols_initialized = true;
+                               on_proc_exit(backtrace_cleanup, 0);
+                       }
+                       else
+                       {
+                               elog(WARNING, "could not initialize the symbol 
handler: error code %lu",
+                                        GetLastError());
+                               edata->backtrace = errtrace.data;
+                               return;
+                       }
+               }
+
+               nframes = CaptureStackBackTrace(num_skip, lengthof(buf), buf, 
NULL);
+
+               if (nframes == 0)
+               {
+                       appendStringInfoString(&errtrace, "\nNo stack frames 
captured");
+                       edata->backtrace = errtrace.data;
+                       return;
+               }
+
+               symbol = (PSYMBOL_INFOW) buffer;
+               symbol->MaxNameLen = MAX_SYM_NAME;
+               symbol->SizeOfStruct = sizeof(SYMBOL_INFOW);
+
+               for (int i = 0; i < nframes; i++)
+               {
+                       DWORD64         address = (DWORD64)buf[i];
+                       DWORD64         displacement = 0;
+                       BOOL            sym_result;
+
+                       sym_result = SymFromAddrW(backtrace_process,
+                                                                        
address,
+                                                                        
&displacement,
+                                                                        
symbol);
+                       if (sym_result)
+                       {
+                               char            symbol_name[MAX_SYM_NAME];
+                               size_t          result;
+
+                               /*
+                               * Convert symbol name from UTF-16 to database 
encoding using
+                               * wchar2char(), which handles both UTF-8 and 
non-UTF-8 databases
+                               * correctly on Windows.
+                               */
+                               result = wchar2char(symbol_name, (const wchar_t 
*) symbol->Name,
+                                                                       
sizeof(symbol_name), NULL);
+
+                               if (result == (size_t) -1)
+                               {
+                                       /* Conversion failed, use address only 
*/
+                                       appendStringInfo(&errtrace,
+                                                                               
"\n[0x%llx]",
+                                                                               
(unsigned long long) address);
+                               }
+                               else
+                               {
+                                       IMAGEHLP_LINEW64 line;
+                                       DWORD           line_displacement = 0;
+                                       char            filename[MAX_PATH];
+
+                                       line.SizeOfStruct = 
sizeof(IMAGEHLP_LINEW64);
+
+                                       /* Start with the common part: 
symbol+offset [address] */
+                                       appendStringInfo(&errtrace,
+                                                                               
"\n%s+0x%llx [0x%llx]",
+                                                                               
symbol_name,
+                                                                               
(unsigned long long) displacement,
+                                                                               
(unsigned long long) address);
+
+                                       /* Try to append line info if available 
*/
+                                       if 
(SymGetLineFromAddrW64(backtrace_process,
+                                                                               
                address,
+                                                                               
                &line_displacement,
+                                                                               
                &line))
+                                       {
+                                               result = wchar2char(filename, 
(const wchar_t *) line.FileName,
+                                                                               
        sizeof(filename), NULL);
+
+                                               if (result != (size_t) -1)
+                                               {
+                                                       
appendStringInfo(&errtrace,
+                                                                               
                " [%s:%lu]",
+                                                                               
                filename,
+                                                                               
                (unsigned long) line.LineNumber);
+                                               }
+                                       }
+                               }
+                       }
+                       else
+                       {
+                               elog(WARNING, "symbol lookup failed: error code 
%lu",
+                                               GetLastError());
+                       }
+               }
+       }
 #else
        appendStringInfoString(&errtrace,
                                                   "backtrace generation is not 
supported by this installation");
 #endif
-
        edata->backtrace = errtrace.data;
 }
 
-- 
2.46.0.windows.1

Reply via email to