On 10/31/2025 2:07 PM, Bryan Green wrote:
On 10/31/2025 1:46 PM, Euler Taveira wrote:
On Thu, Oct 30, 2025, at 11:51 AM, Bryan Green wrote:
I had reservations about the value the tests were adding, and
considering I am getting more concern around having the tests than not
having them for this initial release I have decided to remove them.  v4
patch is attached.  It is the same as the initial 0001-* patch.


I spent some time on this patch. Here are some comments and suggestions.


Thanks for the review.

+#ifdef _MSC_VER
+#include <windows.h>
+#include <dbghelp.h>
+static bool win32_backtrace_symbols_initialized = false;
+static HANDLE win32_backtrace_process = NULL;
+#endif

We usually a different style. Headers go to the top on the same section as system headers and below postgres.h. It is generally kept sorted. The same

Will fix. I will rework the style (error and otherwise) to follow the project tradition.

applies to variables. Add them near the other static variables. BTW does it need
the win32_ prefix for a Window-only variable?

+        wchar_t        buffer[sizeof(SYMBOL_INFOW) + MAX_SYM_NAME * sizeof(wchar_t)];
+        PSYMBOL_INFOW symbol;

According to [1], SYMBOL_INFO is an alias that automatically selects ANSI vs
UNICODE. Shouldn't we use it instead of SYMBOL_INFOW?


Good point. I was being overly explicit about wanting wide chars, but you're right that the generic versions are the way to go.

+                elog(WARNING, "SymInitialize failed with error %lu", error);

Is there a reason to continue if SymInitialize failed? It should return after

None at all.  Will return immediately.

printing the message. Per the error message style guide [2], my suggestion is "could not initialize the symbol handler: error code %lu". You can also use
GetLastError() directly in the elog call.

+        symbol = (PSYMBOL_INFOW) buffer;
+        symbol      ->MaxNameLen = MAX_SYM_NAME;
+        symbol      ->SizeOfStruct = sizeof(SYMBOL_INFOW);

We generally don't add spaces between variable and a member.

+        DWORD        i;

I'm curious why did you declare this variable as DWORD? Shouldn't int be
sufficient? The CaptureStackBackTrace function returns an unsigned short
(UShort). You can also declare it in the for loop.


Out of habit.  I will change it to int.

+            DWORD64        address = (DWORD64) (stack[i]);

The parenthesis around stack is superfluous. The code usually doesn't contain
additional parenthesis (unless it improves readability).


I will remove the parenthesis.

+        if (frames == 0)
+        {
+            appendStringInfoString(&errtrace, "\nNo stack frames captured");
+            edata->backtrace = errtrace.data;
+            return;
+        }

It seems CaptureStackBackTrace function may return zero frames under certain conditions. It is a good point having this additional message. I noticed that the current code path (HAVE_BACKTRACE_SYMBOLS) doesn't have this block. IIUC, in certain circumstances (ARM vs unwind-tables flag), the backtrace() also returns zero frames. Should we add this block for the backtrace() code path?


Probably, though that seems like separate cleanup. Want me to include it
here or handle separately (in another patch)?

+            sym_result = SymFromAddrW(win32_backtrace_process,
+                                      address,
+                                      &displacement,
+                                      symbol);

You should use SymFromAddr, no? [3] I saw that you used the Unicode functions
instead of the generic functions [4].

+                    /* Convert symbol name to UTF-8 */
+                    utf8_len = WideCharToMultiByte(CP_UTF8, 0, symbol->Name, -1,
+                                                   NULL, 0, NULL, NULL);
+                    if (utf8_len > 0)
+                    {
+                        char       *filename_utf8;
+                        int            filename_len;
+
+                        utf8_buffer = palloc(utf8_len);
+                        WideCharToMultiByte(CP_UTF8, 0, symbol->Name, -1, +                                            utf8_buffer, utf8_len, NULL, NULL);

+                    /* Convert symbol name to UTF-8 */
+                    utf8_len = WideCharToMultiByte(CP_UTF8, 0, symbol->Name, -1,
+                                                   NULL, 0, NULL, NULL);

You are calling WideCharToMultiByte twice. The reason is to allocate the exact memory size. However, you can adopt another logic to avoid the first call.

maxlen = symbol->NameLen * pg_database_encoding_max_length();
symbol_name = palloc(maxlen + 1);

(I suggest symbol_name instead of ut8_buffer.)

You are considering only the UTF-8 case. Shouldn't it use wcstombs or
wcstombs_l? Maybe (re)use wchar2char -- see pg_locale_libc.c.


Hmm, you're probably right. I was thinking these Windows API strings needed special handling, but wchar2char should handle the conversion to database encoding correctly. Let me test that approach.

+                        if (filename_len > 0)
+                        {
+                            filename_utf8 = palloc(filename_len);
+                            WideCharToMultiByte(CP_UTF8, 0, line.FileName, -1, +                                                filename_utf8, filename_len,
+                                                NULL, NULL);
+
+                            appendStringInfo(&errtrace,
+                                             "\n%s+0x%llx [%s:%lu]",
+                                             utf8_buffer,
+                                             (unsigned long long) displacement,
+                                             filename_utf8,
+                                             (unsigned long) line.LineNumber);
+
+                            pfree(filename_utf8);
+                        }
+                        else
+                        {
+                            appendStringInfo(&errtrace,
+                                             "\n%s+0x%llx [0x%llx]",
+                                             utf8_buffer,
+                                             (unsigned long long) displacement, +                                             (unsigned long long) address);
+                        }

Maybe I missed something but is there a reason for not adding the address in the
first condition?


No particular reason. I was trying to keep it concise when we have file/ line info, but for consistency it probably should be there.

Will send v5 with these fixes.


[1] https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/ns- dbghelp-symbol_infow
[2] https://www.postgresql.org/docs/current/error-style-guide.html
[3] https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/nf- dbghelp-symfromaddrw [4] https://learn.microsoft.com/en-us/windows/win32/intl/conventions- for-function-prototypes



Thanks again for the review,
Bryan
Hi all,

v5 patch attached, incorporating all of Euler's feedback with a caveat around unicode.

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.

When I tried converting these with wchar2char(), it failed because the
input wasn't actually wide characters - leading to backtraces showing only raw addresses even though symbols were present.

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.

Tested with both UTF-8 and WIN1252 databases - backtraces now show proper symbol names in both cases.

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.

Bryan
From ba3bf14d523e4c656be1cf3d8bdb80afaa3355ed Mon Sep 17 00:00:00 2001
From: Bryan Green <[email protected]>
Date: Fri, 31 Oct 2025 20:27:38 -0600
Subject: [PATCH v5] 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, SymGetLineFromAddr64) for symbol resolution.

The implementation provides symbol names, offsets, addresses, and when PDB
files are available, 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) 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
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.

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.

Author: Bryan Green <[email protected]>
Reviewed-by: Euler Taveira <[email protected]>
Reviewed-by: Jakub Wartak <[email protected]>
---
 src/backend/meson.build        |   5 ++
 src/backend/utils/error/elog.c | 151 ++++++++++++++++++++++++++++++++-
 2 files changed, 155 insertions(+), 1 deletion(-)

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..17a3ba256a 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
 
@@ -1121,6 +1130,13 @@ errbacktrace(void)
  * 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)
@@ -1136,6 +1152,14 @@ set_backtrace(ErrorData *edata, int num_skip)
                char      **strfrms;
 
                nframes = backtrace(buf, lengthof(buf));
+
+               if (nframes == 0)
+               {
+                       appendStringInfoString(&errtrace, "\nNo stack frames 
captured");
+                       edata->backtrace = errtrace.data;
+                       return;
+               }
+
                strfrms = backtrace_symbols(buf, nframes);
                if (strfrms != NULL)
                {
@@ -1147,6 +1171,131 @@ 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))
+                       {
+                               elog(WARNING, "could not initialize the symbol 
handler: error code %lu",
+                                        GetLastError());
+                               edata->backtrace = errtrace.data;
+                               return;
+                       }
+                       backtrace_symbols_initialized = true;
+               }
+
+               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)
+                       {
+                               IMAGEHLP_LINEW64 line;
+                               DWORD           line_displacement = 0;
+                               char            symbol_name[MAX_SYM_NAME];
+                               size_t          result;
+
+                               line.SizeOfStruct = sizeof(IMAGEHLP_LINE64);
+
+                               /*
+                                * 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);
+                                       continue;
+                               }
+
+                               if (SymGetLineFromAddrW64(backtrace_process,
+                                                                               
 address,
+                                                                               
 &line_displacement,
+                                                                               
 &line))
+                               {
+                                       char    filename[MAX_PATH];
+
+                                       /* Convert filename from UTF-16 to 
database encoding */
+                                       result = wchar2char(filename, (const 
wchar_t *) line.FileName,
+                                                                               
sizeof(filename), NULL);
+
+                                       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);
+                               }
+                       }
+                       else
+                       {
+                               elog(WARNING, "symbol lookup failed: error code 
%lu",
+                                               GetLastError());
+                               edata->backtrace = errtrace.data;
+                       }
+               }
+       }
 #else
        appendStringInfoString(&errtrace,
                                                   "backtrace generation is not 
supported by this installation");
-- 
2.46.0.windows.1

Reply via email to