[PATCH] Add Windows support for backtrace_functions (MSVC only)

Started by Bryan Green3 months ago24 messages
#1Bryan Green
dbryan.green@gmail.com
1 attachment(s)

Hi,

I've implemented Windows support for the backtrace_functions configuration
parameter using the DbgHelp API. This brings Windows debugging capabilities
closer to parity with Unix/Linux platforms.

Currently, backtrace_functions only works on Unix-like systems. This patch
adds equivalent functionality for Windows MSVC builds using:
- CaptureStackBackTrace() for capturing the call stack
- SymFromAddrW() and SymGetLineFromAddrW64() for symbol resolution
- UTF-8 conversion to handle international file paths correctly

When PDB symbol files are available, backtraces show function names,
offsets,
file paths, and line numbers. Without PDB files, raw memory addresses
are shown.

Testing performed:
- MSVC build with PDB files: Backtraces include function names, offsets,
  source files, and line numbers
- MSVC build without PDB files: Backtraces show memory addresses only
- Verified backtraces appear in server logs when backtrace_functions is set
- Confirmed no crashes or memory leaks
- Passed all tests with Cirrus CI for all defined platforms.

The implementation is MSVC-specific. MinGW builds will use the existing
fallback message ("backtrace generation is not supported by this
installation").

The patch is attached. I'm happy to make revisions based on feedback.

Best regards,
Bryan Green

Attachments:

windows-backtrace-v1.patchtext/plain; charset=UTF-8; name=windows-backtrace-v1.patchDownload
From 798866a250bf8643757e6a57beed40f286484f77 Mon Sep 17 00:00:00 2001
From: Bryan Green <dbryan.green@gmail.com>
Date: Mon, 6 Oct 2025 16:29:55 -0500
Subject: [PATCH] Add Windows support for backtrace_functions (MSVC only)

Implement backtrace generation on Windows using the DbgHelp API.
This provides similar functionality to the existing Unix/Linux backtrace
support, but only for MSVC builds. MinGW builds are not supported due to
differences in the debugging infrastructure.

When PDB debug symbols are available, backtraces will include function
names and source file locations. Without PDB files, only raw addresses
are displayed. The implementation uses the Unicode (wide character)
variants of DbgHelp functions and converts paths to UTF-8 to properly
handle international characters in file paths.

This adds a dependency on dbghelp.lib for Windows MSVC builds.

Author: Bryan Green <dbryan.green@gmail.com>
---
 src/backend/meson.build        |   6 ++
 src/backend/utils/error/elog.c | 167 ++++++++++++++++++++++++++++++++-
 2 files changed, 172 insertions(+), 1 deletion(-)

diff --git a/src/backend/meson.build b/src/backend/meson.build
index b831a54165..849e457da3 100644
--- a/src/backend/meson.build
+++ b/src/backend/meson.build
@@ -1,6 +1,12 @@
 # Copyright (c) 2022-2025, PostgreSQL Global Development Group
 
 backend_build_deps = [backend_code]
+
+# Add dbghelp for backtrace capability
+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 b7b9692f8c..d57ca54274 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -140,6 +140,13 @@ static void write_syslog(int level, const char *line);
 static void write_eventlog(int level, const char *line, int len);
 #endif
 
+#ifdef _MSC_VER
+#include <windows.h>
+#include <dbghelp.h>
+static bool win32_backtrace_symbols_initialized = false;
+static HANDLE win32_backtrace_process = NULL;
+#endif
+
 /* We provide a small stack of ErrorData records for re-entrant cases */
 #define ERRORDATA_STACK_SIZE  5
 
@@ -1112,6 +1119,12 @@ 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 raw addresses if unavailable)
+ * - Other: Returns unsupported message
  */
 static void
 set_backtrace(ErrorData *edata, int num_skip)
@@ -1138,6 +1151,159 @@ set_backtrace(ErrorData *edata, int num_skip)
 			appendStringInfoString(&errtrace,
 								   "insufficient memory for backtrace generation");
 	}
+#elif defined(_MSC_VER)
+	{
+		void	   *stack[100];
+		DWORD		frames;
+		DWORD		i;
+		wchar_t		buffer[sizeof(SYMBOL_INFOW) + MAX_SYM_NAME * sizeof(wchar_t)];
+		PSYMBOL_INFOW symbol;
+		char	   *utf8_buffer;
+		int			utf8_len;
+
+		if (!win32_backtrace_symbols_initialized)
+		{
+			win32_backtrace_process = GetCurrentProcess();
+
+			SymSetOptions(SYMOPT_UNDNAME |
+						  SYMOPT_DEFERRED_LOADS |
+						  SYMOPT_LOAD_LINES |
+						  SYMOPT_FAIL_CRITICAL_ERRORS);
+
+			if (SymInitialize(win32_backtrace_process, NULL, TRUE))
+			{
+				win32_backtrace_symbols_initialized = true;
+			}
+			else
+			{
+				DWORD		error = GetLastError();
+				elog(WARNING, "SymInitialize failed with error %lu", error);
+			}
+		}
+
+		frames = CaptureStackBackTrace(num_skip, lengthof(stack), stack, NULL);
+
+		if (frames == 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 (i = 0; i < frames; i++)
+		{
+			DWORD64		address = (DWORD64) (stack[i]);
+			DWORD64		displacement = 0;
+			BOOL		sym_result;
+
+			sym_result = SymFromAddrW(win32_backtrace_process,
+									  address,
+									  &displacement,
+									  symbol);
+
+			if (sym_result)
+			{
+				IMAGEHLP_LINEW64 line;
+				DWORD		line_displacement = 0;
+
+				line.SizeOfStruct = sizeof(IMAGEHLP_LINEW64);
+
+				if (SymGetLineFromAddrW64(win32_backtrace_process,
+										  address,
+										  &line_displacement,
+										  &line))
+				{
+					/* 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 file name to UTF-8 */
+						filename_len = WideCharToMultiByte(CP_UTF8, 0,
+														   line.FileName, -1,
+														   NULL, 0, NULL, NULL);
+						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);
+						}
+
+						pfree(utf8_buffer);
+					}
+					else
+					{
+						/* Conversion failed, use address only */
+						appendStringInfo(&errtrace,
+										 "\n[0x%llx]",
+										 (unsigned long long) address);
+					}
+				}
+				else
+				{
+					/* No line info, convert symbol name only */
+					utf8_len = WideCharToMultiByte(CP_UTF8, 0, symbol->Name, -1,
+												   NULL, 0, NULL, NULL);
+					if (utf8_len > 0)
+					{
+						utf8_buffer = palloc(utf8_len);
+						WideCharToMultiByte(CP_UTF8, 0, symbol->Name, -1,
+											utf8_buffer, utf8_len, NULL, NULL);
+
+						appendStringInfo(&errtrace,
+										 "\n%s+0x%llx [0x%llx]",
+										 utf8_buffer,
+										 (unsigned long long) displacement,
+										 (unsigned long long) address);
+
+						pfree(utf8_buffer);
+					}
+					else
+					{
+						/* Conversion failed, use address only */
+						appendStringInfo(&errtrace,
+										 "\n[0x%llx]",
+										 (unsigned long long) address);
+					}
+				}
+			}
+			else
+			{
+				appendStringInfo(&errtrace,
+								 "\n[0x%llx]",
+								 (unsigned long long) address);
+			}
+		}
+	}
 #else
 	appendStringInfoString(&errtrace,
 						   "backtrace generation is not supported by this installation");
@@ -1145,7 +1311,6 @@ set_backtrace(ErrorData *edata, int num_skip)
 
 	edata->backtrace = errtrace.data;
 }
-
 /*
  * errmsg_internal --- add a primary error message text to the current error
  *
-- 
2.46.0.windows.1

#2Jakub Wartak
jakub.wartak@enterprisedb.com
In reply to: Bryan Green (#1)
Re: [PATCH] Add Windows support for backtrace_functions (MSVC only)

On Thu, Oct 9, 2025 at 5:50 PM Bryan Green <dbryan.green@gmail.com> wrote:

Hi Bryan!

I've implemented Windows support for the backtrace_functions configuration parameter using the DbgHelp API. This brings Windows debugging capabilities closer to parity with Unix/Linux platforms.

Cool, thanks for working on this. Win32 is a bit alien to me, but I've
got access to win32 so I've played with the patch a little bit. With
simple: SET backtrace_functions = 'typenameType'; CREATE TABLE tab (id
invalidtype);

I've got
2025-10-20 08:47:25.440 CEST [25700] ERROR: type "invalidtype"
does not exist at character 22
2025-10-20 08:47:25.440 CEST [25700] BACKTRACE:
typenameType+0x86
[C:\git\postgres-master\src\backend\parser\parse_type.c:270]
transformColumnDefinition+0x1df
[C:\git\postgres-master\src\backend\parser\parse_utilcmd.c:649]
transformCreateStmt+0x306
[C:\git\postgres-master\src\backend\parser\parse_utilcmd.c:279]
[..]
main+0x4f8 [C:\git\postgres-master\src\backend\main\main.c:218]
__scrt_common_main_seh+0x10c
[D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288]
BaseThreadInitThunk+0x17 [0x7ffc5f06e8d7]
RtlUserThreadStart+0x2c [0x7ffc5ff08d9c]
2025-10-20 08:47:25.440 CEST [25700] STATEMENT: CREATE TABLE tab
(id invalidtype);

Without PDB files, raw memory addresses are shown.

and after removing PDB files, so it's how the real distribution of
Win32 are shipped, it's still showing function names (but MSVC
shouldn't be putting symbols into binaries, right? I mean it's better
than I expected):
2025-10-20 09:49:03.061 CEST [22832] ERROR: type "invalidtype"
does not exist at character 22
2025-10-20 09:49:03.061 CEST [22832] BACKTRACE:
typenameType+0x86 [0x7ff71e6e3426]
transformAlterTableStmt+0xb7f [0x7ff71e6e5eef]
transformCreateStmt+0x306 [0x7ff71e6e78a6]
[..]

meanwhile on Linux:
2025-10-20 08:49:11.758 CEST [4310] ERROR: type "invalidtype"
does not exist at character 22
2025-10-20 08:49:11.758 CEST [4310] BACKTRACE:
postgres: postgres postgres [local] CREATE
TABLE(typenameType+0x86) [0x560f082eeb06]
postgres: postgres postgres [local] CREATE
TABLE(+0x36ae37) [0x560f082efe37]
postgres: postgres postgres [local] CREATE
TABLE(transformCreateStmt+0x386) [0x560f082ef676]
[..]

Clearly the MSVC version by default seems to be more reliable in terms
of symbols resolution. Anyway:

1. Should outputs be aligned in terms of process title, so should we
aim with this $patch to also include the full process name (like
`postgres: postgres postgres [local]` above and often the operation
`CREATE TABLE` too) which seems to be coming from setproctitle(3bsd)
and does depend on update_process_title GUC. However on win32 this is
off (so nobody will touch it), because docs say 'This setting defaults
to on on most platforms, but it defaults to off on Windows due to that
platform's larger overhead for updating the process title'. IMHO it is
good as it is, which is to have something rather than nothing.
Personally for me it is pretty strange that original
backtrace_symbols(3) includes './progname' in the output on Linux, but
it is what we have today.

2. Code review itself:

a. nitpicking nearby:
+ * - Unix/Linux/: Uses backtrace() and backtrace_symbols() <--
git diff shows there's unnecessary empty space at the end

Confirmed no crashes or memory leaks

b. Shouldn't we call SymCleanup(process) at some point to free
resources anyway? (I'm wondering about the scenario in case a very
long-lived process hits $backtrace_functions every couple of seconds -
wouldn't that cause a leak over a very long term without SymCleanup()
?)

-J.

#3Michael Paquier
michael@paquier.xyz
In reply to: Jakub Wartak (#2)
Re: [PATCH] Add Windows support for backtrace_functions (MSVC only)

On Mon, Oct 20, 2025 at 10:10:25AM +0200, Jakub Wartak wrote:

Cool, thanks for working on this. Win32 is a bit alien to me, but I've
got access to win32 so I've played with the patch a little bit. With
simple: SET backtrace_functions = 'typenameType'; CREATE TABLE tab (id
invalidtype);

Perhaps it would be possible to add some WIN32-specific tests that
check some log patterns based on the backtraces printed? (I have not
read the patch in details, just an idea while passing by.)
--
Michael

#4Jakub Wartak
jakub.wartak@enterprisedb.com
In reply to: Michael Paquier (#3)
Re: [PATCH] Add Windows support for backtrace_functions (MSVC only)

On Mon, Oct 20, 2025 at 10:40 AM Michael Paquier <michael@paquier.xyz> wrote:

On Mon, Oct 20, 2025 at 10:10:25AM +0200, Jakub Wartak wrote:

Cool, thanks for working on this. Win32 is a bit alien to me, but I've
got access to win32 so I've played with the patch a little bit. With
simple: SET backtrace_functions = 'typenameType'; CREATE TABLE tab (id
invalidtype);

Perhaps it would be possible to add some WIN32-specific tests that
check some log patterns based on the backtraces printed? (I have not
read the patch in details, just an idea while passing by.)

Hi Michael,

thanks for stepping in; It looks like the original 71a8a4f6e3654 had
no tests and sadly the simpler idea - even lowest client_min_messages
settings do not make it possible to send the backtrace log to the
frontend (send_message_to_server_log() logs edata->backtrace, but
send_message_to_frontend() does not).

Also is it worth it to test that setting backtrace_funciton=FOO really
emits .*FOO.* in log message cross-platform way?

Because we would have to accept at least 3 valid results 'not
supported' (e.g. mingw) + 'FOO' + what if just the address is returned
without function name due to use of some wild compiler options? Just
an idea, maybe just setting backtrace_funcitons and *ignoring* output
in log, but proving the server did not crash afterwards would be also
considered as acceptable? (and have a better testing surface that we
have today), but from a different angle it might look like
bloatware...

-J.

#5Bryan Green
dbryan.green@gmail.com
In reply to: Jakub Wartak (#2)
1 attachment(s)
Re: [PATCH] Add Windows support for backtrace_functions (MSVC only)

On 10/20/2025 3:10 AM, Jakub Wartak wrote:

On Thu, Oct 9, 2025 at 5:50 PM Bryan Green <dbryan.green@gmail.com> wrote:

Hi Bryan!

I've implemented Windows support for the backtrace_functions configuration parameter using the DbgHelp API. This brings Windows debugging capabilities closer to parity with Unix/Linux platforms.

Cool, thanks for working on this. Win32 is a bit alien to me, but I've
got access to win32 so I've played with the patch a little bit. With
simple: SET backtrace_functions = 'typenameType'; CREATE TABLE tab (id
invalidtype);

I've got
2025-10-20 08:47:25.440 CEST [25700] ERROR: type "invalidtype"
does not exist at character 22
2025-10-20 08:47:25.440 CEST [25700] BACKTRACE:
typenameType+0x86
[C:\git\postgres-master\src\backend\parser\parse_type.c:270]
transformColumnDefinition+0x1df
[C:\git\postgres-master\src\backend\parser\parse_utilcmd.c:649]
transformCreateStmt+0x306
[C:\git\postgres-master\src\backend\parser\parse_utilcmd.c:279]
[..]
main+0x4f8 [C:\git\postgres-master\src\backend\main\main.c:218]
__scrt_common_main_seh+0x10c
[D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288]
BaseThreadInitThunk+0x17 [0x7ffc5f06e8d7]
RtlUserThreadStart+0x2c [0x7ffc5ff08d9c]
2025-10-20 08:47:25.440 CEST [25700] STATEMENT: CREATE TABLE tab
(id invalidtype);

Without PDB files, raw memory addresses are shown.

and after removing PDB files, so it's how the real distribution of
Win32 are shipped, it's still showing function names (but MSVC
shouldn't be putting symbols into binaries, right? I mean it's better
than I expected):

MSVC does put function names into release builds. You should see
function names, raw addresses, NO file paths or line numbers in a
release build. PDB files would give you the file paths and line
numbers.> 2025-10-20 09:49:03.061 CEST [22832] ERROR: type
"invalidtype"

does not exist at character 22
2025-10-20 09:49:03.061 CEST [22832] BACKTRACE:
typenameType+0x86 [0x7ff71e6e3426]
transformAlterTableStmt+0xb7f [0x7ff71e6e5eef]
transformCreateStmt+0x306 [0x7ff71e6e78a6]
[..]

meanwhile on Linux:
2025-10-20 08:49:11.758 CEST [4310] ERROR: type "invalidtype"
does not exist at character 22
2025-10-20 08:49:11.758 CEST [4310] BACKTRACE:
postgres: postgres postgres [local] CREATE
TABLE(typenameType+0x86) [0x560f082eeb06]
postgres: postgres postgres [local] CREATE
TABLE(+0x36ae37) [0x560f082efe37]
postgres: postgres postgres [local] CREATE
TABLE(transformCreateStmt+0x386) [0x560f082ef676]
[..]

Clearly the MSVC version by default seems to be more reliable in terms
of symbols resolution. Anyway:

1. Should outputs be aligned in terms of process title, so should we
aim with this $patch to also include the full process name (like
`postgres: postgres postgres [local]` above and often the operation
`CREATE TABLE` too) which seems to be coming from setproctitle(3bsd)
and does depend on update_process_title GUC. However on win32 this is
off (so nobody will touch it), because docs say 'This setting defaults
to on on most platforms, but it defaults to off on Windows due to that
platform's larger overhead for updating the process title'. IMHO it is
good as it is, which is to have something rather than nothing.
Personally for me it is pretty strange that original
backtrace_symbols(3) includes './progname' in the output on Linux, but
it is what we have today.

I think it is good as is.> 2. Code review itself:

a. nitpicking nearby:
+ * - Unix/Linux/: Uses backtrace() and backtrace_symbols() <--
git diff shows there's unnecessary empty space at the end

Thanks for catching this.>> Confirmed no crashes or memory leaks

b. Shouldn't we call SymCleanup(process) at some point to free
resources anyway? (I'm wondering about the scenario in case a very
long-lived process hits $backtrace_functions every couple of seconds -
wouldn't that cause a leak over a very long term without SymCleanup()
?)

-J.

Yes. We should call cleanup at the backend shutdown, as initialize is
called once. I have put together a new patch (for better patch naming)
and added the cleanup code.

BG

Attachments:

0001-Add-Windows-support-for-backtrace_functions.patchtext/plain; charset=UTF-8; name=0001-Add-Windows-support-for-backtrace_functions.patchDownload
From 81ee688f490fa37b5eb30d1d88123e3d0a8423f2 Mon Sep 17 00:00:00 2001
From: Bryan Green <dbryan.green@gmail.com>
Date: Tue, 21 Oct 2025 19:31:29 -0500
Subject: [PATCH] Add Windows support for backtrace_functions

Implement backtrace generation on Windows using the DbgHelp API, providing
similar functionality to the existing Unix/Linux support. When PDB files
are available, backtraces include function names and source locations.
Without PDB files, raw addresses are shown.

DbgHelp is initialized once per backend and cleaned up via on_proc_exit().
This adds a dependency on dbghelp.lib for MSVC builds.
---
 src/backend/meson.build        |   5 +
 src/backend/utils/error/elog.c | 174 +++++++++++++++++++++++++++++++++
 2 files changed, 179 insertions(+)

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..fc421ce444 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -140,6 +140,13 @@ static void write_syslog(int level, const char *line);
 static void write_eventlog(int level, const char *line, int len);
 #endif
 
+#ifdef _MSC_VER
+#include <windows.h>
+#include <dbghelp.h>
+static bool win32_backtrace_symbols_initialized = false;
+static HANDLE win32_backtrace_process = NULL;
+#endif
+
 /* We provide a small stack of ErrorData records for re-entrant cases */
 #define ERRORDATA_STACK_SIZE  5
 
@@ -1116,6 +1123,18 @@ 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
+win32_backtrace_cleanup(int code, Datum arg)
+{
+	SymCleanup(win32_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
@@ -1147,6 +1166,161 @@ set_backtrace(ErrorData *edata, int num_skip)
 			appendStringInfoString(&errtrace,
 								   "insufficient memory for backtrace generation");
 	}
+#elif defined(_MSC_VER)
+	{
+		void	   *stack[100];
+		DWORD		frames;
+		DWORD		i;
+		wchar_t		buffer[sizeof(SYMBOL_INFOW) + MAX_SYM_NAME * sizeof(wchar_t)];
+		PSYMBOL_INFOW symbol;
+		char	   *utf8_buffer;
+		int			utf8_len;
+
+		if (!win32_backtrace_symbols_initialized)
+		{
+			win32_backtrace_process = GetCurrentProcess();
+
+			SymSetOptions(SYMOPT_UNDNAME |
+						  SYMOPT_DEFERRED_LOADS |
+						  SYMOPT_LOAD_LINES |
+						  SYMOPT_FAIL_CRITICAL_ERRORS);
+
+			if (SymInitialize(win32_backtrace_process, NULL, TRUE))
+			{
+				win32_backtrace_symbols_initialized = true;
+				on_proc_exit(win32_backtrace_cleanup, 0);
+			}
+			else
+			{
+				DWORD		error = GetLastError();
+
+				elog(WARNING, "SymInitialize failed with error %lu", error);
+			}
+		}
+
+		frames = CaptureStackBackTrace(num_skip, lengthof(stack), stack, NULL);
+
+		if (frames == 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 (i = 0; i < frames; i++)
+		{
+			DWORD64		address = (DWORD64) (stack[i]);
+			DWORD64		displacement = 0;
+			BOOL		sym_result;
+
+			sym_result = SymFromAddrW(win32_backtrace_process,
+									  address,
+									  &displacement,
+									  symbol);
+
+			if (sym_result)
+			{
+				IMAGEHLP_LINEW64 line;
+				DWORD		line_displacement = 0;
+
+				line.SizeOfStruct = sizeof(IMAGEHLP_LINEW64);
+
+				if (SymGetLineFromAddrW64(win32_backtrace_process,
+										  address,
+										  &line_displacement,
+										  &line))
+				{
+					/* 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 file name to UTF-8 */
+						filename_len = WideCharToMultiByte(CP_UTF8, 0,
+														   line.FileName, -1,
+														   NULL, 0, NULL, NULL);
+						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);
+						}
+
+						pfree(utf8_buffer);
+					}
+					else
+					{
+						/* Conversion failed, use address only */
+						appendStringInfo(&errtrace,
+										 "\n[0x%llx]",
+										 (unsigned long long) address);
+					}
+				}
+				else
+				{
+					/* No line info, convert symbol name only */
+					utf8_len = WideCharToMultiByte(CP_UTF8, 0, symbol->Name, -1,
+												   NULL, 0, NULL, NULL);
+					if (utf8_len > 0)
+					{
+						utf8_buffer = palloc(utf8_len);
+						WideCharToMultiByte(CP_UTF8, 0, symbol->Name, -1,
+											utf8_buffer, utf8_len, NULL, NULL);
+
+						appendStringInfo(&errtrace,
+										 "\n%s+0x%llx [0x%llx]",
+										 utf8_buffer,
+										 (unsigned long long) displacement,
+										 (unsigned long long) address);
+
+						pfree(utf8_buffer);
+					}
+					else
+					{
+						/* Conversion failed, use address only */
+						appendStringInfo(&errtrace,
+										 "\n[0x%llx]",
+										 (unsigned long long) address);
+					}
+				}
+			}
+			else
+			{
+				appendStringInfo(&errtrace,
+								 "\n[0x%llx]",
+								 (unsigned long long) address);
+			}
+		}
+	}
 #else
 	appendStringInfoString(&errtrace,
 						   "backtrace generation is not supported by this installation");
-- 
2.46.0.windows.1

#6Jakub Wartak
jakub.wartak@enterprisedb.com
In reply to: Bryan Green (#5)
Re: [PATCH] Add Windows support for backtrace_functions (MSVC only)

On Wed, Oct 22, 2025 at 3:09 AM Bryan Green <dbryan.green@gmail.com> wrote:

Hi Bryan,
[..]

Yes. We should call cleanup at the backend shutdown, as initialize is
called once. I have put together a new patch (for better patch naming)
and added the cleanup code.

I've played a little time with this and this looks good to me ,
including 5-min pgbench runs with backtrace_functions set (it behaves
stable even for pgbench -C which stresses it much). Cfbot is also
green.

One thing i've I think I've noticed (but I've double-checked that's
not related to backtrace_functions set) - so it's that apparently
backends on windows leak(?) a tiny bit of memory - it's like 5
backends leak like 5 * <1kB / second @ ~3k TPS total as seen by
Resource Manager, yet i have no time to investigate that), anyway it's
does not seem to be connected to $topic.

Maybe one outstanding question is the answer to Michael's earlier question.

-J.

#7Bryan Green
dbryan.green@gmail.com
In reply to: Michael Paquier (#3)
Re: [PATCH] Add Windows support for backtrace_functions (MSVC only)

On 10/20/2025 3:40 AM, Michael Paquier wrote:

On Mon, Oct 20, 2025 at 10:10:25AM +0200, Jakub Wartak wrote:

Cool, thanks for working on this. Win32 is a bit alien to me, but I've
got access to win32 so I've played with the patch a little bit. With
simple: SET backtrace_functions = 'typenameType'; CREATE TABLE tab (id
invalidtype);

Perhaps it would be possible to add some WIN32-specific tests that
check some log patterns based on the backtraces printed? (I have not
read the patch in details, just an idea while passing by.)
--
Michael

Michael,
Thanks for even glancing at this. I did not add any regression
tests because the output goes to the server log and not the client.

BG

#8Euler Taveira
euler@eulerto.com
In reply to: Bryan Green (#7)
Re: [PATCH] Add Windows support for backtrace_functions (MSVC only)

On Mon, Oct 27, 2025, at 2:58 PM, Bryan Green wrote:

Thanks for even glancing at this. I did not add any regression
tests because the output goes to the server log and not the client.

Since Michael said WIN32-specific tests and mentioned log pattern, he is
referring to TAP tests. You can add src/test/modules/test_backtrace that
exercises this code path.

I didn't test your patch but I'm wondering if we could add an abstraction here.
I mean invent pg_backtrace() and pg_backtrace_symbols() that maps to the
current functions (Unix-like). On Windows these functions are mapped to
win32_backtrace() (that encapsulates CaptureStackBackTrace()) and a new
win32_backtrace_symbols() (that encapsulates the rest of the code). There
Windows-specific functions should be added to src/port -- my suggestion is
win32backtrace.c. Besides that you have to adjust backtrace_symbols in both
configure.ac and meson.build so it runs the code path inside
HAVE_BACKTRACE_SYMBOLS.

--
Euler Taveira
EDB https://www.enterprisedb.com/

#9Álvaro Herrera
alvherre@kurilemu.de
In reply to: Euler Taveira (#8)
Re: [PATCH] Add Windows support for backtrace_functions (MSVC only)

On 2025-Oct-27, Euler Taveira wrote:

On Mon, Oct 27, 2025, at 2:58 PM, Bryan Green wrote:

Thanks for even glancing at this. I did not add any regression
tests because the output goes to the server log and not the client.

Since Michael said WIN32-specific tests and mentioned log pattern, he is
referring to TAP tests. You can add src/test/modules/test_backtrace that
exercises this code path.

Hmm, are we really sure this is necessary?

I didn't test your patch but I'm wondering if we could add an
abstraction here. I mean invent pg_backtrace() and
pg_backtrace_symbols() that maps to the current functions (Unix-like).

Do we really need this? I don't think we're going to add support for
backtraces anywhere else any time soon, so this looks premature. What
other programs do you think we have where this would be useful? I have
a really hard time imagining that things like psql and pg_dump would be
improved by having backtrace-reporting support. And if we have a single
place in the code using a facility, then ISTM the platform-specific code
can live there with no damage.

If somebody is interested in adding backtracing other programs in the
future, they can introduce the abstraction then -- we will probably now
exactly what sort of API would be useful when we have more than one user
than now that we have just the backend, with very specific needs.

--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
"La primera ley de las demostraciones en vivo es: no trate de usar el sistema.
Escriba un guión que no toque nada para no causar daños." (Jakob Nielsen)

#10Euler Taveira
euler@eulerto.com
In reply to: Álvaro Herrera (#9)
Re: [PATCH] Add Windows support for backtrace_functions (MSVC only)

On Tue, Oct 28, 2025, at 1:51 PM, Álvaro Herrera wrote:

On 2025-Oct-27, Euler Taveira wrote:

On Mon, Oct 27, 2025, at 2:58 PM, Bryan Green wrote:

Thanks for even glancing at this. I did not add any regression
tests because the output goes to the server log and not the client.

Since Michael said WIN32-specific tests and mentioned log pattern, he is
referring to TAP tests. You can add src/test/modules/test_backtrace that
exercises this code path.

Hmm, are we really sure this is necessary?

Good question. We are testing an external API. Maybe a test in this thread is
enough to convince someone that the code is correct.

I didn't test your patch but I'm wondering if we could add an
abstraction here. I mean invent pg_backtrace() and
pg_backtrace_symbols() that maps to the current functions (Unix-like).

Do we really need this? I don't think we're going to add support for
backtraces anywhere else any time soon, so this looks premature. What
other programs do you think we have where this would be useful? I have
a really hard time imagining that things like psql and pg_dump would be
improved by having backtrace-reporting support. And if we have a single
place in the code using a facility, then ISTM the platform-specific code
can live there with no damage.

It was just a suggestion; not a requirement. As you said it would avoid rework
in the future if or when someone decides to use this code in other parts of the
software. I wouldn't propose this change if I knew it was complicated to have a
simple and clean abstraction.

--
Euler Taveira
EDB https://www.enterprisedb.com/

#11Bryan Green
dbryan.green@gmail.com
In reply to: Euler Taveira (#10)
1 attachment(s)
Re: [PATCH] Add Windows support for backtrace_functions (MSVC only)

On 10/29/2025 6:53 AM, Euler Taveira wrote:

On Tue, Oct 28, 2025, at 1:51 PM, Álvaro Herrera wrote:

On 2025-Oct-27, Euler Taveira wrote:

On Mon, Oct 27, 2025, at 2:58 PM, Bryan Green wrote:

Thanks for even glancing at this. I did not add any regression
tests because the output goes to the server log and not the client.

Since Michael said WIN32-specific tests and mentioned log pattern, he is
referring to TAP tests. You can add src/test/modules/test_backtrace that
exercises this code path.

Hmm, are we really sure this is necessary?

Good question. We are testing an external API. Maybe a test in this thread is
enough to convince someone that the code is correct.

Right, attached is v2 with TAP tests added. The test checks whether
postgres.pdb exists on disk to figure out what kind of output to expect,
then verifies the actual backtrace format matches. This ought to catch
the case where the PDB is there but DbgHelp can't load it for some
reason, which seems like the most likely failure mode.

It also runs a bunch of errors in quick succession to make sure we don't
crash or anything. (Can't really detect memory leaks without external
tools, so didn't try.)>>> I didn't test your patch but I'm wondering if
we could add an

abstraction here. I mean invent pg_backtrace() and
pg_backtrace_symbols() that maps to the current functions (Unix-like).

Do we really need this? I don't think we're going to add support for
backtraces anywhere else any time soon, so this looks premature. What
other programs do you think we have where this would be useful? I have
a really hard time imagining that things like psql and pg_dump would be
improved by having backtrace-reporting support. And if we have a single
place in the code using a facility, then ISTM the platform-specific code
can live there with no damage.

It was just a suggestion; not a requirement. As you said it would avoid rework
in the future if or when someone decides to use this code in other parts of the
software. I wouldn't propose this change if I knew it was complicated to have a
simple and clean abstraction.

I am not convinced that we need an abstraction considering our backtrace
logic for both platforms is in one place at the moment and not spread
throughout the source. I have a hard time imagining this being used
anywhere but where it currently is... I am more than happy to do as the
community wishes though.

The latest patch has tap tests now and I look forward to any guidance
the community wishes to provide.

Bryan Green

Attachments:

v2-0001-Add-Windows-support-for-backtrace_functions.patchtext/plain; charset=UTF-8; name=v2-0001-Add-Windows-support-for-backtrace_functions.patchDownload
From 1aed9a7572bcdfbd789a69eea5d4a7c7a47b300a Mon Sep 17 00:00:00 2001
From: Bryan Green <dbryan.green@gmail.com>
Date: Tue, 21 Oct 2025 19:31:29 -0500
Subject: [PATCH v2] Add Windows support for backtrace_functions.

backtrace_functions has been Unix-only up to now, because we relied on
glibc's backtrace() or similar platform facilities.  Windows doesn't have
anything equivalent in its standard C library, but it does have the DbgHelp
API, which can do the same thing if you ask it nicely.

The tricky bit is that DbgHelp needs to be initialized with SymInitialize()
before you can use it, and that's a fairly expensive operation.  We don't
want to do that every time we generate a backtrace.  Fortunately, it turns
out that we can initialize once per process and reuse the handle, which is
safe since we're holding an exclusive lock during error reporting anyway.
So the code just initializes lazily on first use.

If SymInitialize fails, we don't consider that an error; we just silently
decline to generate backtraces.  This seems reasonable since backtraces are
a debugging aid, not critical to operation.  It also matches the behavior
on platforms where backtrace() isn't available.

Symbol resolution quality depends on whether PDB files are present.  If they
are, DbgHelp can give us source file paths and line numbers, which is great.
If not, it can still give us function names by reading the export table, and
that turns out to be good enough because postgres.exe exports thousands of
functions.  (You get export symbols on Windows whether you like it or not,
unless you go out of your way to suppress them.  Might as well take advantage
of that.)  Fully stripped binaries would only show addresses, but that's not
a scenario that applies to Postgres, so we don't worry about it.

The TAP test verifies that symbol resolution works correctly in both the
with-PDB and without-PDB cases.  We have to use TAP because backtraces go
to the server log, not to psql.  The test figures out which case should
apply by checking whether postgres.pdb exists on disk, then parses the
backtrace output to see what we actually got.  If those don't match, that's
a bug.  This should catch the case where the PDB exists but DbgHelp fails
to load it, which seems like the most likely way this could break.

The test also verifies that we can generate a bunch of backtraces in quick
succession without crashing, which is really just a basic sanity check.  It
doesn't attempt to detect memory leaks, since that would require external
tools we don't want to depend on.

Author: Bryan Green
---
 src/backend/meson.build                       |   5 +
 src/backend/utils/error/elog.c                | 174 +++++++
 src/test/modules/meson.build                  |   1 +
 src/test/modules/test_backtrace/Makefile      |  33 ++
 src/test/modules/test_backtrace/README        | 224 +++++++++
 src/test/modules/test_backtrace/meson.build   |  13 +
 .../test_backtrace/t/t_windows_backtrace.pl   | 428 ++++++++++++++++++
 .../test_backtrace/test_backtrace--1.0.sql    |  66 +++
 .../test_backtrace/test_backtrace.control     |   5 +
 9 files changed, 949 insertions(+)
 create mode 100644 src/test/modules/test_backtrace/Makefile
 create mode 100644 src/test/modules/test_backtrace/README
 create mode 100644 src/test/modules/test_backtrace/meson.build
 create mode 100644 src/test/modules/test_backtrace/t/t_windows_backtrace.pl
 create mode 100644 src/test/modules/test_backtrace/test_backtrace--1.0.sql
 create mode 100644 src/test/modules/test_backtrace/test_backtrace.control

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..fc421ce444 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -140,6 +140,13 @@ static void write_syslog(int level, const char *line);
 static void write_eventlog(int level, const char *line, int len);
 #endif
 
+#ifdef _MSC_VER
+#include <windows.h>
+#include <dbghelp.h>
+static bool win32_backtrace_symbols_initialized = false;
+static HANDLE win32_backtrace_process = NULL;
+#endif
+
 /* We provide a small stack of ErrorData records for re-entrant cases */
 #define ERRORDATA_STACK_SIZE  5
 
@@ -1116,6 +1123,18 @@ 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
+win32_backtrace_cleanup(int code, Datum arg)
+{
+	SymCleanup(win32_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
@@ -1147,6 +1166,161 @@ set_backtrace(ErrorData *edata, int num_skip)
 			appendStringInfoString(&errtrace,
 								   "insufficient memory for backtrace generation");
 	}
+#elif defined(_MSC_VER)
+	{
+		void	   *stack[100];
+		DWORD		frames;
+		DWORD		i;
+		wchar_t		buffer[sizeof(SYMBOL_INFOW) + MAX_SYM_NAME * sizeof(wchar_t)];
+		PSYMBOL_INFOW symbol;
+		char	   *utf8_buffer;
+		int			utf8_len;
+
+		if (!win32_backtrace_symbols_initialized)
+		{
+			win32_backtrace_process = GetCurrentProcess();
+
+			SymSetOptions(SYMOPT_UNDNAME |
+						  SYMOPT_DEFERRED_LOADS |
+						  SYMOPT_LOAD_LINES |
+						  SYMOPT_FAIL_CRITICAL_ERRORS);
+
+			if (SymInitialize(win32_backtrace_process, NULL, TRUE))
+			{
+				win32_backtrace_symbols_initialized = true;
+				on_proc_exit(win32_backtrace_cleanup, 0);
+			}
+			else
+			{
+				DWORD		error = GetLastError();
+
+				elog(WARNING, "SymInitialize failed with error %lu", error);
+			}
+		}
+
+		frames = CaptureStackBackTrace(num_skip, lengthof(stack), stack, NULL);
+
+		if (frames == 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 (i = 0; i < frames; i++)
+		{
+			DWORD64		address = (DWORD64) (stack[i]);
+			DWORD64		displacement = 0;
+			BOOL		sym_result;
+
+			sym_result = SymFromAddrW(win32_backtrace_process,
+									  address,
+									  &displacement,
+									  symbol);
+
+			if (sym_result)
+			{
+				IMAGEHLP_LINEW64 line;
+				DWORD		line_displacement = 0;
+
+				line.SizeOfStruct = sizeof(IMAGEHLP_LINEW64);
+
+				if (SymGetLineFromAddrW64(win32_backtrace_process,
+										  address,
+										  &line_displacement,
+										  &line))
+				{
+					/* 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 file name to UTF-8 */
+						filename_len = WideCharToMultiByte(CP_UTF8, 0,
+														   line.FileName, -1,
+														   NULL, 0, NULL, NULL);
+						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);
+						}
+
+						pfree(utf8_buffer);
+					}
+					else
+					{
+						/* Conversion failed, use address only */
+						appendStringInfo(&errtrace,
+										 "\n[0x%llx]",
+										 (unsigned long long) address);
+					}
+				}
+				else
+				{
+					/* No line info, convert symbol name only */
+					utf8_len = WideCharToMultiByte(CP_UTF8, 0, symbol->Name, -1,
+												   NULL, 0, NULL, NULL);
+					if (utf8_len > 0)
+					{
+						utf8_buffer = palloc(utf8_len);
+						WideCharToMultiByte(CP_UTF8, 0, symbol->Name, -1,
+											utf8_buffer, utf8_len, NULL, NULL);
+
+						appendStringInfo(&errtrace,
+										 "\n%s+0x%llx [0x%llx]",
+										 utf8_buffer,
+										 (unsigned long long) displacement,
+										 (unsigned long long) address);
+
+						pfree(utf8_buffer);
+					}
+					else
+					{
+						/* Conversion failed, use address only */
+						appendStringInfo(&errtrace,
+										 "\n[0x%llx]",
+										 (unsigned long long) address);
+					}
+				}
+			}
+			else
+			{
+				appendStringInfo(&errtrace,
+								 "\n[0x%llx]",
+								 (unsigned long long) address);
+			}
+		}
+	}
 #else
 	appendStringInfoString(&errtrace,
 						   "backtrace generation is not supported by this installation");
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 14fc761c4c..ccb63f2b57 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -14,6 +14,7 @@ subdir('plsample')
 subdir('spgist_name_ops')
 subdir('ssl_passphrase_callback')
 subdir('test_aio')
+subdir('test_backtrace')
 subdir('test_binaryheap')
 subdir('test_bitmapset')
 subdir('test_bloomfilter')
diff --git a/src/test/modules/test_backtrace/Makefile b/src/test/modules/test_backtrace/Makefile
new file mode 100644
index 0000000000..3e3112cd74
--- /dev/null
+++ b/src/test/modules/test_backtrace/Makefile
@@ -0,0 +1,33 @@
+# src/test/modules/test_backtrace/Makefile
+#
+# Makefile for Windows backtrace testing module
+
+MODULE_big = test_backtrace
+OBJS = test_backtrace.o
+
+EXTENSION = test_backtrace
+DATA = test_backtrace--1.0.sql
+
+# Only TAP tests - no SQL regression tests since backtraces
+# go to server logs, not to client output
+TAP_TESTS = 1
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_backtrace
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
+
+# Platform-specific test handling
+ifeq ($(PORTNAME),win32)
+    # Run all tests on Windows
+    PROVE_FLAGS += -v
+else
+    # Skip tests on non-Windows platforms
+    TAP_TESTS = 0
+endif
diff --git a/src/test/modules/test_backtrace/README b/src/test/modules/test_backtrace/README
new file mode 100644
index 0000000000..7388320ad9
--- /dev/null
+++ b/src/test/modules/test_backtrace/README
@@ -0,0 +1,224 @@
+================================================================================
+                         Windows Backtrace Tests
+================================================================================
+
+TAP tests for the Windows backtrace implementation, which uses the DbgHelp API
+to capture and format stack traces.
+
+--------------------------------------------------------------------------------
+Why TAP Tests?
+--------------------------------------------------------------------------------
+
+Backtraces appear in the server log, not in psql output.  So we can't use SQL
+tests to validate them; we need TAP tests that can read the actual log files.
+
+--------------------------------------------------------------------------------
+Test File
+--------------------------------------------------------------------------------
+
+t_windows_backtrace.pl
+
+  Tests backtrace generation in two scenarios:
+
+  (1) WITH PDB FILE: postgres.pdb exists alongside postgres.exe
+      Expected: Function names, source files, line numbers, addresses
+      Validates: DbgHelp loads the PDB and resolves full symbols
+
+  (2) WITHOUT PDB FILE: postgres.pdb is absent
+      Expected: Function names and addresses (from export table)
+      Validates: We can still get useful backtraces without debug info
+
+  The test figures out which scenario applies by checking whether postgres.pdb
+  exists, then validates that the actual backtrace format matches expectations.
+  If the PDB is present but we don't get source files, that's a bug.
+
+  What gets tested:
+
+    - Basic functionality (backtraces appear in logs)
+    - Symbol resolution (correct format for scenario)
+    - Various error types (div-by-zero, constraints, etc)
+    - PL/pgSQL and trigger integration
+    - Stability (20 rapid errors don't crash anything)
+    - DbgHelp initialization doesn't fail
+
+  What doesn't get tested:
+
+    - Memory leaks (would need Dr. Memory or similar)
+    - Symbol servers (would need network setup)
+    - Stripped binaries (not relevant for Postgres)
+
+--------------------------------------------------------------------------------
+Running the Tests
+--------------------------------------------------------------------------------
+
+Prerequisites:
+
+  - Windows (test skips on other platforms)
+  - MSVC build (needs DbgHelp)
+  - Perl with PostgreSQL::Test modules
+
+From the PostgreSQL build directory:
+
+  meson test --suite test_backtrace --verbose
+
+Or with prove:
+
+  set PERL5LIB=C:\path\to\postgres\src\test\perl
+  prove src/test/modules/test_backtrace/t/t_windows_backtrace.pl
+
+To test both scenarios:
+
+  (1) WITH PDB: Just run the test normally
+      - PDB should be in the same directory as postgres.exe
+      - Test expects to find source file information
+
+  (2) WITHOUT PDB: Delete the PDB file and re-run
+      - del build_dir\tmp_install\...\bin\postgres.pdb
+      - Test expects export symbols only
+
+--------------------------------------------------------------------------------
+Expected Output
+--------------------------------------------------------------------------------
+
+With PDB:
+
+  PDB file found: C:\...\postgres.pdb
+  EXPECTED: Scenario 1 (full PDB symbols)
+  ACTUAL: Scenario 1 (found source files and symbols)
+  ok - Scenario matches expectation: Scenario 1
+
+  ERROR:  division by zero
+  BACKTRACE:
+  int4div+0x2a [C:\postgres\src\backend\utils\adt\int.c:841] [0x00007FF6...]
+  ExecInterpExpr+0x1b3 [C:\postgres\src\backend\executor\execExprInterp.c:2345]
+  ...
+
+Without PDB:
+
+  PDB file not found: C:\...\postgres.pdb
+  EXPECTED: Scenario 2 (export symbols only)
+  ACTUAL: Scenario 2 (found symbols but no source files)
+  ok - Scenario matches expectation: Scenario 2
+
+  ERROR:  division by zero
+  BACKTRACE:
+  int4div+0x2a [0x00007FF6...]
+  ExecInterpExpr+0x1b3 [0x00007FF6...]
+  ...
+
+Note: Postgres exports ~11,000 functions, so even without a PDB, you get
+function names.  Fully stripped binaries would only show addresses, but
+that's not a scenario we care about for Postgres.
+
+Failure (PDB exists but doesn't load):
+
+  PDB file found: C:\...\postgres.pdb
+  EXPECTED: Scenario 1 (full PDB symbols)
+  ACTUAL: Scenario 2 (found symbols but no source files)
+  not ok - PDB file exists but symbols not loading!
+
+  This means DbgHelp couldn't load the PDB.  Possible causes: corrupted PDB,
+  mismatched PDB (from different build), or DbgHelp initialization failed.
+
+--------------------------------------------------------------------------------
+How It Works
+--------------------------------------------------------------------------------
+
+The test validates expected vs actual:
+
+  1. Check if postgres.pdb exists on disk
+     -> If yes, expect full PDB symbols
+     -> If no, expect export symbols only
+
+  2. Parse the backtrace output
+     -> If source files present, got PDB symbols
+     -> If no source files, got exports only
+
+  3. Compare expected to actual
+     -> Pass if they match
+     -> Fail if they don't (indicates a problem)
+
+This catches the case where the PDB exists but DbgHelp fails to load it,
+which is the most likely failure mode.
+
+--------------------------------------------------------------------------------
+Configuration
+--------------------------------------------------------------------------------
+
+The test configures the server with:
+
+  backtrace_functions = 'int4div,int4in,ExecInterpExpr'
+  log_error_verbosity = verbose
+  logging_collector = on
+  log_destination = 'stderr'
+  log_min_messages = error
+
+Nothing fancy.  Just enough to generate backtraces and make them easy to find
+in the logs.
+
+--------------------------------------------------------------------------------
+Limitations
+--------------------------------------------------------------------------------
+
+This test verifies basic functionality.  It does not:
+
+  - Detect memory leaks (would need Dr. Memory, ASAN, or similar)
+  - Test symbol server scenarios (would need network setup and config)
+  - Validate symbol accuracy in detail (just checks format)
+  - Test performance or memory usage
+  - Validate path remapping (future work)
+
+The test ensures the feature works and doesn't crash.  That's about it.
+
+--------------------------------------------------------------------------------
+Troubleshooting
+--------------------------------------------------------------------------------
+
+"PDB file exists but symbols not loading!"
+
+  The PDB is there but DbgHelp couldn't use it.
+
+  Check:
+    - Is the PDB corrupted?
+    - Does the PDB match the executable? (same build)
+    - Are there SymInitialize errors in the log?
+
+"Scenario mismatch" (other than PDB not loading)
+
+  Something weird happened.  Look at the test output to see what was expected
+  vs what was found, and figure out what's going on.
+
+No backtraces at all
+
+  Check:
+    - Is backtrace_functions configured?
+    - Is logging_collector enabled?
+    - Are there SymInitialize failures in the log?
+
+Only addresses, no function names (even without PDB)
+
+  This would be very strange, since Postgres exports thousands of functions.
+  DbgHelp should be able to get them from the export table.  Check that
+  postgres.exe was linked normally.
+
+Test hangs
+
+  Probably a logging issue.  Check that logging_collector is working and
+  log files are appearing in the data directory.
+
+--------------------------------------------------------------------------------
+Symbol Servers
+--------------------------------------------------------------------------------
+
+This test doesn't try to exercise symbol server functionality.  It just checks
+whether a local PDB file gets used.  Symbol servers are a deployment concern,
+not a functionality test.
+
+For deployments, you'd typically either:
+  - Ship PDB files alongside executables (development/staging)
+  - Don't ship PDB files (production, smaller footprint)
+
+In the latter case, you still get useful backtraces from the export table.
+Whether that's sufficient depends on your debugging needs.
+
+================================================================================
diff --git a/src/test/modules/test_backtrace/meson.build b/src/test/modules/test_backtrace/meson.build
new file mode 100644
index 0000000000..b8b1b8e198
--- /dev/null
+++ b/src/test/modules/test_backtrace/meson.build
@@ -0,0 +1,13 @@
+# TAP tests - only run on Windows
+if host_system == 'windows'
+  tests += {
+    'name': 'test_backtrace',
+    'sd': meson.current_source_dir(),
+    'bd': meson.current_build_dir(),
+    'tap': {
+      'tests': [
+        't/t_windows_backtrace.pl',
+      ],
+    },
+  }
+endif
diff --git a/src/test/modules/test_backtrace/t/t_windows_backtrace.pl b/src/test/modules/test_backtrace/t/t_windows_backtrace.pl
new file mode 100644
index 0000000000..7609da99eb
--- /dev/null
+++ b/src/test/modules/test_backtrace/t/t_windows_backtrace.pl
@@ -0,0 +1,428 @@
+#!/usr/bin/perl
+
+# Copyright (c) 2025, PostgreSQL Global Development Group
+
+# Test Windows backtrace generation using DbgHelp API.
+#
+# The main thing we need to verify is that the DbgHelp integration actually
+# works, and that we can resolve symbols both with and without PDB files
+# present.  We don't try to test symbol server scenarios or anything fancy;
+# just check that if you have a PDB, you get source file info, and if you
+# don't, you still get function names from the export table (which works
+# because postgres.exe exports thousands of functions).
+#
+# The test automatically detects which situation applies by checking whether
+# postgres.pdb exists alongside postgres.exe.  This avoids the need to know
+# how the executable was built or what build type we're testing.  If the PDB
+# is there but we don't get source info, that's a bug.  If it's not there
+# but we somehow get source info anyway, that's weird and worth investigating.
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Skip if not Windows
+if ($^O ne 'MSWin32')
+{
+	plan skip_all => 'Windows-specific backtrace tests';
+}
+
+# Initialize node
+my $node = PostgreSQL::Test::Cluster->new('backtrace_test');
+$node->init;
+
+# Configure for detailed logging with backtraces
+$node->append_conf('postgresql.conf', qq{
+backtrace_functions = 'int4div,int4in,ExecInterpExpr'
+log_error_verbosity = verbose
+logging_collector = on
+log_destination = 'stderr'
+log_min_messages = error
+});
+
+$node->start;
+
+# Helper to get recent log content.
+# Backtraces go to the server log, not to psql output, so we need to read
+# the actual log files to validate anything.
+sub get_recent_log_content
+{
+	my $log_dir = $node->data_dir . '/log';
+	my @log_files = glob("$log_dir/*.log $log_dir/*.csv");
+
+	# Get the most recent log file
+	my $latest_log = (sort { -M $a <=> -M $b } @log_files)[0];
+
+	return '' unless defined $latest_log && -f $latest_log;
+
+	my $content = '';
+	open(my $fh, '<', $latest_log) or return '';
+	{
+		local $/;
+		$content = <$fh>;
+	}
+	close($fh);
+
+	return $content;
+}
+
+###############################################################################
+# First, verify basic functionality and figure out what scenario we're in.
+#
+# We trigger an error and check that (a) it actually generates a backtrace,
+# and (b) we can tell from the backtrace format whether we have PDB symbols
+# or just exports.  Then we compare that to what we should have based on
+# whether postgres.pdb exists on disk.
+###############################################################################
+
+note('');
+note('=== PART 1: Basic Error Tests & Scenario Detection ===');
+
+### Test 1: Division by zero
+my ($ret, $stdout, $stderr) = $node->psql('postgres', "SELECT 1/0;");
+ok($ret != 0, 'division by zero error occurred');
+like($stderr, qr/division by zero/i, 'division by zero error message in psql');
+
+sleep 1;
+my $log_content = get_recent_log_content();
+like($log_content, qr/ERROR:.*division by zero/i, 'error logged to server log');
+like($log_content, qr/BACKTRACE:/i, 'BACKTRACE header in log');
+
+### Test 2: Detect scenario and validate it matches expectations
+#
+# The backtrace format tells us what DbgHelp actually gave us.  Source file
+# paths mean we got PDB symbols; lack thereof means export symbols only.
+#
+# We then check whether postgres.pdb exists on disk.  If it does, we should
+# have gotten PDB symbols; if not, we should have gotten exports only.
+# Mismatches indicate a problem with DbgHelp initialization or PDB loading.
+my $has_source_files = ($log_content =~ /\[[\w:\\\/]+\.\w+:\d+\]/);  # [file.c:123]
+my $has_symbols = ($log_content =~ /\w+\+0x[0-9a-fA-F]+/);           # function+0xABC
+my $has_addresses = ($log_content =~ /\[0x[0-9a-fA-F]+\]/);          # [0xABCDEF]
+
+ok($has_symbols || $has_addresses, 'backtrace has valid format');
+
+# Determine EXPECTED scenario based on PDB file existence.
+#
+# We need to find where postgres.exe actually lives.  The TAP test framework
+# creates a temporary install, and we can find the bin directory by looking
+# at the parent of the data directory.  If that doesn't work, search PATH.
+# (This is a bit ugly but there's no direct way to ask the test node for
+# its bin directory.)
+my $datadir = $node->data_dir;
+my $postgres_exe;
+
+# Try to find the bin directory relative to data directory
+# Typical structure: .../tmp_install/PORT_XXX/data and .../tmp_install/PORT_XXX/bin
+if ($datadir =~ /^(.+)[\\\/][^\\\/]+$/)
+{
+	my $base = $1;  # Get parent directory
+	$postgres_exe = "$base/bin/postgres.exe";
+}
+
+# Fallback: try to construct from test environment
+if (!defined($postgres_exe) || !-f $postgres_exe)
+{
+	# Try using PATH - the test sets up PATH to include the bin directory
+	my $path_dirs = $ENV{PATH};
+	foreach my $dir (split(/;/, $path_dirs))
+	{
+		my $candidate = "$dir/postgres.exe";
+		if (-f $candidate)
+		{
+			$postgres_exe = $candidate;
+			last;
+		}
+	}
+}
+
+# If still not found, just use the command name and note that we couldn't find it
+if (!defined($postgres_exe))
+{
+	$postgres_exe = 'postgres.exe';
+}
+
+my $postgres_pdb = $postgres_exe;
+$postgres_pdb =~ s/\.exe$/.pdb/i;
+
+my $expected_scenario;
+if (-f $postgres_pdb)
+{
+	$expected_scenario = 1;  # PDB exists, we SHOULD have full symbols
+	note("PDB file found: $postgres_pdb");
+	note("EXPECTED: Scenario 1 (full PDB symbols)");
+}
+else
+{
+	$expected_scenario = 2;  # No PDB, we SHOULD have export symbols only
+	note("PDB file not found: $postgres_pdb");
+	note("EXPECTED: Scenario 2 (export symbols only)");
+}
+
+# Determine ACTUAL scenario from log output
+my $actual_scenario;
+if ($has_source_files && $has_symbols)
+{
+	$actual_scenario = 1;  # Full PDB symbols
+	note('ACTUAL: Scenario 1 (found source files and symbols)');
+}
+elsif ($has_symbols && !$has_source_files)
+{
+	$actual_scenario = 2;  # Export symbols only
+	note('ACTUAL: Scenario 2 (found symbols but no source files)');
+}
+else
+{
+	$actual_scenario = 0;  # Unknown/invalid
+	fail('Unable to determine scenario - PostgreSQL should always have export symbols');
+	note('Expected either Scenario 1 (with PDB) or Scenario 2 (without PDB)');
+}
+
+# CRITICAL TEST: Validate actual matches expected.
+#
+# This is the main point of the test.  We need to verify that DbgHelp is
+# actually loading symbols correctly.  If the PDB exists but we don't get
+# source files, that's a bug.  If the PDB doesn't exist but we somehow get
+# source files anyway, that's bizarre and worth investigating.
+if ($actual_scenario == $expected_scenario)
+{
+	pass("Scenario matches expectation: Scenario $actual_scenario");
+	note('');
+	if ($actual_scenario == 1) {
+		note('*** SCENARIO 1: Full PDB symbols (build WITH .pdb file) ***');
+		note('Build type: Release/Debug/DebugOptimized WITH .pdb file');
+		note('Format: function+offset [file.c:line] [0xaddress]');
+	}
+	elsif ($actual_scenario == 2) {
+		note('*** SCENARIO 2: Export symbols only (build WITHOUT .pdb file) ***');
+		note('Build type: Release WITHOUT .pdb file');
+		note('Format: function+offset [0xaddress]');
+	}
+	note('');
+}
+elsif ($expected_scenario == 1 && $actual_scenario == 2)
+{
+	fail('PDB file exists but symbols not loading!');
+	note("PDB file found at: $postgres_pdb");
+	note('Expected: Full PDB symbols with source files');
+	note('Actual: Only export symbols (no source files)');
+	note('This indicates PDB is not being loaded by DbgHelp');
+}
+elsif ($expected_scenario == 2 && $actual_scenario == 1)
+{
+	fail('Found PDB symbols but PDB file does not exist!');
+	note("PDB file not found at: $postgres_pdb");
+	note('Expected: Export symbols only');
+	note('Actual: Full PDB symbols with source files');
+	note('This is unexpected - where are the symbols coming from?');
+}
+else
+{
+	fail("Scenario mismatch: expected $expected_scenario, got $actual_scenario");
+}
+
+my $scenario = $actual_scenario;
+
+###############################################################################
+# Now validate the backtrace format matches what we expect for this scenario.
+#
+# If we have PDB symbols, check for function names, source files, and addresses.
+# If we only have exports, check that source files are absent (as expected).
+# The point is to verify that the format is sane, not just that it exists.
+###############################################################################
+
+note('');
+note('=== PART 2: Format Validation ===');
+
+if ($scenario == 1)
+{
+	# Scenario 1: Full PDB symbols
+	like($log_content, qr/\w+\+0x[0-9a-fA-F]+/,
+		'Scenario 1: function+offset format present');
+
+	like($log_content, qr/\[[\w:\\\/]+\.\w+:\d+\]/,
+		'Scenario 1: source files and line numbers present');
+
+	like($log_content, qr/\[0x[0-9a-fA-F]+\]/,
+		'Scenario 1: addresses present');
+
+	# Extract example paths to show in test output
+	my @example_paths = ($log_content =~ /\[([^\]]+\.\w+:\d+)\]/g);
+
+	if (@example_paths)
+	{
+		pass('Scenario 1: backtrace includes source file paths');
+		note("Example path: $example_paths[0]");
+	}
+	else
+	{
+		fail('Scenario 1: no source file paths found');
+	}
+}
+elsif ($scenario == 2)
+{
+	# Scenario 2: Export symbols only
+	like($log_content, qr/\w+\+0x[0-9a-fA-F]+/,
+		'Scenario 2: function+offset format present');
+
+	like($log_content, qr/\[0x[0-9a-fA-F]+\]/,
+		'Scenario 2: addresses present');
+
+	unlike($log_content, qr/\[[\w:\\\/]+\.\w+:\d+\]/,
+		'Scenario 2: no source files (expected without PDB)');
+}
+
+###############################################################################
+# Test that backtraces work for various types of errors.
+#
+# The backtrace mechanism should work regardless of what triggered the error.
+# Try a few different error types to make sure we're not somehow dependent on
+# the error path.  Also check that PL/pgSQL and triggers work, since those go
+# through different code paths.
+###############################################################################
+
+note('');
+note('=== PART 3: Error Scenario Coverage ===');
+
+### Test: Type conversion error
+($ret, $stdout, $stderr) = $node->psql('postgres', "SELECT 'invalid'::integer;");
+ok($ret != 0, 'type conversion error occurred');
+like($stderr, qr/invalid input syntax/i, 'type conversion error message');
+
+### Test: Constraint violation
+$node->safe_psql('postgres', "CREATE TABLE test_table (id integer PRIMARY KEY);");
+($ret, $stdout, $stderr) = $node->psql('postgres',
+	"INSERT INTO test_table VALUES (1), (1);");
+ok($ret != 0, 'constraint violation occurred');
+like($stderr, qr/(duplicate key|unique constraint)/i, 'constraint violation message');
+
+### Test: PL/pgSQL nested function
+$node->safe_psql('postgres', q{
+	CREATE FUNCTION nested_func() RETURNS void AS $$
+	BEGIN
+		PERFORM 1/0;
+	END;
+	$$ LANGUAGE plpgsql;
+});
+
+($ret, $stdout, $stderr) = $node->psql('postgres', "SELECT nested_func();");
+ok($ret != 0, 'nested function error occurred');
+
+sleep 1;
+$log_content = get_recent_log_content();
+my @addresses = ($log_content =~ /\[0x[0-9a-fA-F]+\]/g);
+my $frame_count = scalar @addresses;
+ok($frame_count >= 3,
+	"PL/pgSQL error has deeper stack (found $frame_count frames)");
+
+### Test: Trigger error
+$node->safe_psql('postgres', q{
+	CREATE TABLE trigger_test (val integer);
+
+	CREATE FUNCTION trigger_func() RETURNS trigger AS $$
+	BEGIN
+		PERFORM 1/0;
+		RETURN NEW;
+	END;
+	$$ LANGUAGE plpgsql;
+
+	CREATE TRIGGER test_trigger BEFORE INSERT ON trigger_test
+		FOR EACH ROW EXECUTE FUNCTION trigger_func();
+});
+
+($ret, $stdout, $stderr) = $node->psql('postgres',
+	"INSERT INTO trigger_test VALUES (1);");
+ok($ret != 0, 'trigger error occurred');
+like($stderr, qr/division by zero/i, 'trigger error message');
+
+###############################################################################
+# Verify that repeated backtrace generation doesn't cause problems.
+#
+# We don't have a good way to detect memory leaks in this test, but we can
+# at least check that the server doesn't crash or start spewing errors after
+# we've generated a bunch of backtraces.  Also verify that DbgHelp doesn't
+# log any initialization failures.
+###############################################################################
+
+note('');
+note('=== PART 4: Stability Tests ===');
+
+### Test: Multiple errors don't crash the server
+my $error_count = 0;
+for my $i (1..20)
+{
+	($ret, $stdout, $stderr) = $node->psql('postgres', "SELECT 1/0;");
+	$error_count++ if ($ret != 0);
+}
+
+is($error_count, 20, 'all 20 rapid errors occurred (DbgHelp stable)');
+
+sleep 2;
+$log_content = get_recent_log_content();
+@addresses = ($log_content =~ /\[0x[0-9a-fA-F]+\]/g);
+ok(scalar(@addresses) >= 20,
+	'multiple rapid errors produced backtraces (' . scalar(@addresses) . ' addresses found)');
+
+### Test: No SymInitialize failures
+unlike($log_content, qr/SymInitialize.*failed/i,
+	'no SymInitialize failures in log');
+
+### Test: Repeated errors in same session
+$node->safe_psql('postgres', q{
+	DO $$
+	BEGIN
+		FOR i IN 1..10 LOOP
+			BEGIN
+				EXECUTE 'SELECT 1/0';
+			EXCEPTION
+				WHEN division_by_zero THEN
+					NULL;  -- Swallow the error
+			END;
+		END LOOP;
+	END $$;
+});
+
+pass('repeated errors in same session did not crash');
+
+###############################################################################
+# Summary
+###############################################################################
+
+note('');
+note('=== TEST SUMMARY ===');
+note("Scenario: $scenario");
+note('');
+
+if ($scenario == 1) {
+	note('BUILD TYPE: Release/Debug/DebugOptimized WITH .pdb file');
+	note('');
+	note('Validated:');
+	note('  ✓ Function names with offsets');
+	note('  ✓ Source files and line numbers');
+	note('  ✓ Memory addresses');
+	note('  ✓ Stack depth (shallow and nested)');
+	note('  ✓ Multiple error scenarios');
+	note('  ✓ Stability (20 rapid errors, no crashes)');
+	note('  ✓ No DbgHelp initialization failures');
+}
+elsif ($scenario == 2) {
+	note('BUILD TYPE: Release WITHOUT .pdb file');
+	note('');
+	note('Validated:');
+	note('  ✓ Function names with offsets');
+	note('  ✓ Memory addresses');
+	note('  ✗ No source files (expected - no PDB)');
+	note('  ✓ Stack depth (shallow and nested)');
+	note('  ✓ Multiple error scenarios');
+	note('  ✓ Stability (20 rapid errors, no crashes)');
+	note('  ✓ No DbgHelp initialization failures');
+}
+note('====================');
+note('');
+
+$node->stop;
+
+done_testing();
diff --git a/src/test/modules/test_backtrace/test_backtrace--1.0.sql b/src/test/modules/test_backtrace/test_backtrace--1.0.sql
new file mode 100644
index 0000000000..f2e614a18d
--- /dev/null
+++ b/src/test/modules/test_backtrace/test_backtrace--1.0.sql
@@ -0,0 +1,66 @@
+/* src/test/modules/test_backtrace/test_backtrace--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_backtrace" to load this file. \quit
+
+--
+-- Test functions for Windows backtrace functionality
+--
+
+-- Function that triggers a division by zero error
+CREATE FUNCTION test_backtrace_div_by_zero()
+RETURNS void
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    PERFORM 1/0;
+END;
+$$;
+
+-- Function that triggers a type conversion error
+CREATE FUNCTION test_backtrace_type_error()
+RETURNS integer
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    RETURN 'not a number'::integer;
+END;
+$$;
+
+-- Nested function calls to test call stack depth
+CREATE FUNCTION test_backtrace_level3()
+RETURNS void
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    PERFORM 1/0;
+END;
+$$;
+
+CREATE FUNCTION test_backtrace_level2()
+RETURNS void
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    PERFORM test_backtrace_level3();
+END;
+$$;
+
+CREATE FUNCTION test_backtrace_level1()
+RETURNS void
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    PERFORM test_backtrace_level2();
+END;
+$$;
+
+-- Test array bounds error
+CREATE FUNCTION test_backtrace_array_bounds()
+RETURNS integer
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    RETURN ('{1,2,3}'::int[])[10];
+END;
+$$;
diff --git a/src/test/modules/test_backtrace/test_backtrace.control b/src/test/modules/test_backtrace/test_backtrace.control
new file mode 100644
index 0000000000..ac27b28287
--- /dev/null
+++ b/src/test/modules/test_backtrace/test_backtrace.control
@@ -0,0 +1,5 @@
+# test_backtrace extension
+comment = 'Test module for Windows backtrace functionality'
+default_version = '1.0'
+module_pathname = '$libdir/test_backtrace'
+relocatable = true
-- 
2.46.0.windows.1

#12Jakub Wartak
jakub.wartak@enterprisedb.com
In reply to: Bryan Green (#11)
2 attachment(s)
Re: [PATCH] Add Windows support for backtrace_functions (MSVC only)

On Thu, Oct 30, 2025 at 4:06 AM Bryan Green <dbryan.green@gmail.com> wrote:

[..]

Hi Bryan, cfbot is red. I'm was fan of having those tests for this
(bring complexity and we didn't have tests for Linux backtrace
anyway), but now MINGW win32 is failing on those tests where the
feature is not present:

[03:57:44.552] ------------------------------------- 8<
-------------------------------------
[03:57:44.552] stderr:
[03:57:44.552] # Failed test 'backtrace has valid format'
[03:57:44.552] # at
C:/cirrus/src/test/modules/test_backtrace/t/t_windows_backtrace.pl
line 106.
[03:57:44.552] # Failed test 'Unable to determine scenario -
PostgreSQL should always have export symbols'
[03:57:44.552] # at
C:/cirrus/src/test/modules/test_backtrace/t/t_windows_backtrace.pl
line 180.
[03:57:44.552] # Failed test 'Scenario mismatch: expected 2, got 0'
[03:57:44.552] # at
C:/cirrus/src/test/modules/test_backtrace/t/t_windows_backtrace.pl
line 224.
[03:57:44.552] # Failed test 'PL/pgSQL error has deeper stack (found
0 frames)'
[03:57:44.552] # at
C:/cirrus/src/test/modules/test_backtrace/t/t_windows_backtrace.pl
line 318.
[03:57:44.552] # Failed test 'multiple rapid errors produced
backtraces (0 addresses found)'
[03:57:44.552] # at
C:/cirrus/src/test/modules/test_backtrace/t/t_windows_backtrace.pl
line 366.
[03:57:44.552] # Looks like you failed 5 tests of 19.

Anyway, as expected this was thrown:
2025-10-30 03:57:37.973 GMT client backend[244] t_windows_backtrace.pl
BACKTRACE: backtrace generation is not supported by this installation

Please see attached files for convenience as I already had them on
disk. They are from https://commitfest.postgresql.org/patch/6116/ ->
https://cirrus-ci.com/task/5155398535086080 -> artifacts.

Instead of
    +# Skip if not Windows
    +if ($^O ne 'MSWin32')
    +{
    +    plan skip_all => 'Windows-specific backtrace tests';
    +}

Maybe we could also bypass MINGW too like below ? (but I have no
access, so i havent tried)
use Config;
# Skip if not Windows or MINGW/MSYS is detected
if ($^O ne 'MSWin32' || $Config{'ccname'} =~ /gcc|mingw/i ||
defined($ENV{'MSYSTEM'}))
{
plan skip_all => 'Windows-specific backtrace tests';
}

-J.

Attachments:

regress_log_t_windows_backtraceapplication/octet-stream; name=regress_log_t_windows_backtraceDownload
postgresql-2025-10-30_035737.log.gzapplication/gzip; name=postgresql-2025-10-30_035737.log.gzDownload
#13Bryan Green
dbryan.green@gmail.com
In reply to: Jakub Wartak (#12)
1 attachment(s)
Re: [PATCH] Add Windows support for backtrace_functions (MSVC only)

On 10/30/2025 2:01 AM, Jakub Wartak wrote:

On Thu, Oct 30, 2025 at 4:06 AM Bryan Green <dbryan.green@gmail.com> wrote:

[..]

Hi Bryan, cfbot is red. I'm was fan of having those tests for this
(bring complexity and we didn't have tests for Linux backtrace
anyway), but now MINGW win32 is failing on those tests where the
feature is not present:

[03:57:44.552] ------------------------------------- 8<
-------------------------------------
[03:57:44.552] stderr:
[03:57:44.552] # Failed test 'backtrace has valid format'
[03:57:44.552] # at
C:/cirrus/src/test/modules/test_backtrace/t/t_windows_backtrace.pl
line 106.
[03:57:44.552] # Failed test 'Unable to determine scenario -
PostgreSQL should always have export symbols'
[03:57:44.552] # at
C:/cirrus/src/test/modules/test_backtrace/t/t_windows_backtrace.pl
line 180.
[03:57:44.552] # Failed test 'Scenario mismatch: expected 2, got 0'
[03:57:44.552] # at
C:/cirrus/src/test/modules/test_backtrace/t/t_windows_backtrace.pl
line 224.
[03:57:44.552] # Failed test 'PL/pgSQL error has deeper stack (found
0 frames)'
[03:57:44.552] # at
C:/cirrus/src/test/modules/test_backtrace/t/t_windows_backtrace.pl
line 318.
[03:57:44.552] # Failed test 'multiple rapid errors produced
backtraces (0 addresses found)'
[03:57:44.552] # at
C:/cirrus/src/test/modules/test_backtrace/t/t_windows_backtrace.pl
line 366.
[03:57:44.552] # Looks like you failed 5 tests of 19.

Anyway, as expected this was thrown:
2025-10-30 03:57:37.973 GMT client backend[244] t_windows_backtrace.pl
BACKTRACE: backtrace generation is not supported by this installation

Please see attached files for convenience as I already had them on
disk. They are from https://commitfest.postgresql.org/patch/6116/ ->
https://cirrus-ci.com/task/5155398535086080 -> artifacts.

Instead of
+# Skip if not Windows
+if ($^O ne 'MSWin32')
+{
+    plan skip_all => 'Windows-specific backtrace tests';
+}

Maybe we could also bypass MINGW too like below ? (but I have no
access, so i havent tried)
use Config;
# Skip if not Windows or MINGW/MSYS is detected
if ($^O ne 'MSWin32' || $Config{'ccname'} =~ /gcc|mingw/i ||
defined($ENV{'MSYSTEM'}))
{
plan skip_all => 'Windows-specific backtrace tests';
}

-J.

Thanks for the feedback. Apologies for forgetting about filtering mingw
out of the tests. I have made changes to the test as well as
meson.build in the test_backtrace directory to keep it from running on
mingw. My cirrus build is all green across all the platforms. Find v3
attached.

Bryan Green

Attachments:

v3-0001-Add-Windows-support-for-backtrace_functions.patchtext/plain; charset=UTF-8; name=v3-0001-Add-Windows-support-for-backtrace_functions.patchDownload
From 091b8897305ad4d8a25efe560fd292b48b048533 Mon Sep 17 00:00:00 2001
From: Bryan Green <dbryan.green@gmail.com>
Date: Tue, 21 Oct 2025 19:31:29 -0500
Subject: [PATCH v3] Add Windows support for backtrace_functions.

backtrace_functions has been Unix-only up to now, because we relied on
glibc's backtrace() or similar platform facilities.  Windows doesn't have
anything equivalent in its standard C library, but it does have the DbgHelp
API, which can do the same thing if you ask it nicely.

The tricky bit is that DbgHelp needs to be initialized with SymInitialize()
before you can use it, and that's a fairly expensive operation.  We don't
want to do that every time we generate a backtrace.  Fortunately, it turns
out that we can initialize once per process and reuse the handle, which is
safe since we're holding an exclusive lock during error reporting anyway.
So the code just initializes lazily on first use.

If SymInitialize fails, we don't consider that an error; we just silently
decline to generate backtraces.  This seems reasonable since backtraces are
a debugging aid, not critical to operation.  It also matches the behavior
on platforms where backtrace() isn't available.

Symbol resolution quality depends on whether PDB files are present.  If they
are, DbgHelp can give us source file paths and line numbers, which is great.
If not, it can still give us function names by reading the export table, and
that turns out to be good enough because postgres.exe exports thousands of
functions.  (You get export symbols on Windows whether you like it or not,
unless you go out of your way to suppress them.  Might as well take advantage
of that.)  Fully stripped binaries would only show addresses, but that's not
a scenario that applies to Postgres, so we don't worry about it.

The TAP test verifies that symbol resolution works correctly in both the
with-PDB and without-PDB cases.  We have to use TAP because backtraces go
to the server log, not to psql.  The test figures out which case should
apply by checking whether postgres.pdb exists on disk, then parses the
backtrace output to see what we actually got.  If those don't match, that's
a bug.  This should catch the case where the PDB exists but DbgHelp fails
to load it, which seems like the most likely way this could break.

The test also verifies that we can generate a bunch of backtraces in quick
succession without crashing, which is really just a basic sanity check.  It
doesn't attempt to detect memory leaks, since that would require external
tools we don't want to depend on.

Author: Bryan Green
---
 src/backend/meson.build                       |   5 +
 src/backend/utils/error/elog.c                | 174 +++++++
 src/test/modules/meson.build                  |   1 +
 src/test/modules/test_backtrace/Makefile      |  33 ++
 src/test/modules/test_backtrace/README        | 224 +++++++++
 src/test/modules/test_backtrace/meson.build   |  13 +
 .../test_backtrace/t/t_windows_backtrace.pl   | 434 ++++++++++++++++++
 .../test_backtrace/test_backtrace--1.0.sql    |  66 +++
 .../test_backtrace/test_backtrace.control     |   5 +
 9 files changed, 955 insertions(+)
 create mode 100644 src/test/modules/test_backtrace/Makefile
 create mode 100644 src/test/modules/test_backtrace/README
 create mode 100644 src/test/modules/test_backtrace/meson.build
 create mode 100644 src/test/modules/test_backtrace/t/t_windows_backtrace.pl
 create mode 100644 src/test/modules/test_backtrace/test_backtrace--1.0.sql
 create mode 100644 src/test/modules/test_backtrace/test_backtrace.control

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..fc421ce444 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -140,6 +140,13 @@ static void write_syslog(int level, const char *line);
 static void write_eventlog(int level, const char *line, int len);
 #endif
 
+#ifdef _MSC_VER
+#include <windows.h>
+#include <dbghelp.h>
+static bool win32_backtrace_symbols_initialized = false;
+static HANDLE win32_backtrace_process = NULL;
+#endif
+
 /* We provide a small stack of ErrorData records for re-entrant cases */
 #define ERRORDATA_STACK_SIZE  5
 
@@ -1116,6 +1123,18 @@ 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
+win32_backtrace_cleanup(int code, Datum arg)
+{
+	SymCleanup(win32_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
@@ -1147,6 +1166,161 @@ set_backtrace(ErrorData *edata, int num_skip)
 			appendStringInfoString(&errtrace,
 								   "insufficient memory for backtrace generation");
 	}
+#elif defined(_MSC_VER)
+	{
+		void	   *stack[100];
+		DWORD		frames;
+		DWORD		i;
+		wchar_t		buffer[sizeof(SYMBOL_INFOW) + MAX_SYM_NAME * sizeof(wchar_t)];
+		PSYMBOL_INFOW symbol;
+		char	   *utf8_buffer;
+		int			utf8_len;
+
+		if (!win32_backtrace_symbols_initialized)
+		{
+			win32_backtrace_process = GetCurrentProcess();
+
+			SymSetOptions(SYMOPT_UNDNAME |
+						  SYMOPT_DEFERRED_LOADS |
+						  SYMOPT_LOAD_LINES |
+						  SYMOPT_FAIL_CRITICAL_ERRORS);
+
+			if (SymInitialize(win32_backtrace_process, NULL, TRUE))
+			{
+				win32_backtrace_symbols_initialized = true;
+				on_proc_exit(win32_backtrace_cleanup, 0);
+			}
+			else
+			{
+				DWORD		error = GetLastError();
+
+				elog(WARNING, "SymInitialize failed with error %lu", error);
+			}
+		}
+
+		frames = CaptureStackBackTrace(num_skip, lengthof(stack), stack, NULL);
+
+		if (frames == 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 (i = 0; i < frames; i++)
+		{
+			DWORD64		address = (DWORD64) (stack[i]);
+			DWORD64		displacement = 0;
+			BOOL		sym_result;
+
+			sym_result = SymFromAddrW(win32_backtrace_process,
+									  address,
+									  &displacement,
+									  symbol);
+
+			if (sym_result)
+			{
+				IMAGEHLP_LINEW64 line;
+				DWORD		line_displacement = 0;
+
+				line.SizeOfStruct = sizeof(IMAGEHLP_LINEW64);
+
+				if (SymGetLineFromAddrW64(win32_backtrace_process,
+										  address,
+										  &line_displacement,
+										  &line))
+				{
+					/* 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 file name to UTF-8 */
+						filename_len = WideCharToMultiByte(CP_UTF8, 0,
+														   line.FileName, -1,
+														   NULL, 0, NULL, NULL);
+						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);
+						}
+
+						pfree(utf8_buffer);
+					}
+					else
+					{
+						/* Conversion failed, use address only */
+						appendStringInfo(&errtrace,
+										 "\n[0x%llx]",
+										 (unsigned long long) address);
+					}
+				}
+				else
+				{
+					/* No line info, convert symbol name only */
+					utf8_len = WideCharToMultiByte(CP_UTF8, 0, symbol->Name, -1,
+												   NULL, 0, NULL, NULL);
+					if (utf8_len > 0)
+					{
+						utf8_buffer = palloc(utf8_len);
+						WideCharToMultiByte(CP_UTF8, 0, symbol->Name, -1,
+											utf8_buffer, utf8_len, NULL, NULL);
+
+						appendStringInfo(&errtrace,
+										 "\n%s+0x%llx [0x%llx]",
+										 utf8_buffer,
+										 (unsigned long long) displacement,
+										 (unsigned long long) address);
+
+						pfree(utf8_buffer);
+					}
+					else
+					{
+						/* Conversion failed, use address only */
+						appendStringInfo(&errtrace,
+										 "\n[0x%llx]",
+										 (unsigned long long) address);
+					}
+				}
+			}
+			else
+			{
+				appendStringInfo(&errtrace,
+								 "\n[0x%llx]",
+								 (unsigned long long) address);
+			}
+		}
+	}
 #else
 	appendStringInfoString(&errtrace,
 						   "backtrace generation is not supported by this installation");
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 14fc761c4c..ccb63f2b57 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -14,6 +14,7 @@ subdir('plsample')
 subdir('spgist_name_ops')
 subdir('ssl_passphrase_callback')
 subdir('test_aio')
+subdir('test_backtrace')
 subdir('test_binaryheap')
 subdir('test_bitmapset')
 subdir('test_bloomfilter')
diff --git a/src/test/modules/test_backtrace/Makefile b/src/test/modules/test_backtrace/Makefile
new file mode 100644
index 0000000000..3e3112cd74
--- /dev/null
+++ b/src/test/modules/test_backtrace/Makefile
@@ -0,0 +1,33 @@
+# src/test/modules/test_backtrace/Makefile
+#
+# Makefile for Windows backtrace testing module
+
+MODULE_big = test_backtrace
+OBJS = test_backtrace.o
+
+EXTENSION = test_backtrace
+DATA = test_backtrace--1.0.sql
+
+# Only TAP tests - no SQL regression tests since backtraces
+# go to server logs, not to client output
+TAP_TESTS = 1
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_backtrace
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
+
+# Platform-specific test handling
+ifeq ($(PORTNAME),win32)
+    # Run all tests on Windows
+    PROVE_FLAGS += -v
+else
+    # Skip tests on non-Windows platforms
+    TAP_TESTS = 0
+endif
diff --git a/src/test/modules/test_backtrace/README b/src/test/modules/test_backtrace/README
new file mode 100644
index 0000000000..7388320ad9
--- /dev/null
+++ b/src/test/modules/test_backtrace/README
@@ -0,0 +1,224 @@
+================================================================================
+                         Windows Backtrace Tests
+================================================================================
+
+TAP tests for the Windows backtrace implementation, which uses the DbgHelp API
+to capture and format stack traces.
+
+--------------------------------------------------------------------------------
+Why TAP Tests?
+--------------------------------------------------------------------------------
+
+Backtraces appear in the server log, not in psql output.  So we can't use SQL
+tests to validate them; we need TAP tests that can read the actual log files.
+
+--------------------------------------------------------------------------------
+Test File
+--------------------------------------------------------------------------------
+
+t_windows_backtrace.pl
+
+  Tests backtrace generation in two scenarios:
+
+  (1) WITH PDB FILE: postgres.pdb exists alongside postgres.exe
+      Expected: Function names, source files, line numbers, addresses
+      Validates: DbgHelp loads the PDB and resolves full symbols
+
+  (2) WITHOUT PDB FILE: postgres.pdb is absent
+      Expected: Function names and addresses (from export table)
+      Validates: We can still get useful backtraces without debug info
+
+  The test figures out which scenario applies by checking whether postgres.pdb
+  exists, then validates that the actual backtrace format matches expectations.
+  If the PDB is present but we don't get source files, that's a bug.
+
+  What gets tested:
+
+    - Basic functionality (backtraces appear in logs)
+    - Symbol resolution (correct format for scenario)
+    - Various error types (div-by-zero, constraints, etc)
+    - PL/pgSQL and trigger integration
+    - Stability (20 rapid errors don't crash anything)
+    - DbgHelp initialization doesn't fail
+
+  What doesn't get tested:
+
+    - Memory leaks (would need Dr. Memory or similar)
+    - Symbol servers (would need network setup)
+    - Stripped binaries (not relevant for Postgres)
+
+--------------------------------------------------------------------------------
+Running the Tests
+--------------------------------------------------------------------------------
+
+Prerequisites:
+
+  - Windows (test skips on other platforms)
+  - MSVC build (needs DbgHelp)
+  - Perl with PostgreSQL::Test modules
+
+From the PostgreSQL build directory:
+
+  meson test --suite test_backtrace --verbose
+
+Or with prove:
+
+  set PERL5LIB=C:\path\to\postgres\src\test\perl
+  prove src/test/modules/test_backtrace/t/t_windows_backtrace.pl
+
+To test both scenarios:
+
+  (1) WITH PDB: Just run the test normally
+      - PDB should be in the same directory as postgres.exe
+      - Test expects to find source file information
+
+  (2) WITHOUT PDB: Delete the PDB file and re-run
+      - del build_dir\tmp_install\...\bin\postgres.pdb
+      - Test expects export symbols only
+
+--------------------------------------------------------------------------------
+Expected Output
+--------------------------------------------------------------------------------
+
+With PDB:
+
+  PDB file found: C:\...\postgres.pdb
+  EXPECTED: Scenario 1 (full PDB symbols)
+  ACTUAL: Scenario 1 (found source files and symbols)
+  ok - Scenario matches expectation: Scenario 1
+
+  ERROR:  division by zero
+  BACKTRACE:
+  int4div+0x2a [C:\postgres\src\backend\utils\adt\int.c:841] [0x00007FF6...]
+  ExecInterpExpr+0x1b3 [C:\postgres\src\backend\executor\execExprInterp.c:2345]
+  ...
+
+Without PDB:
+
+  PDB file not found: C:\...\postgres.pdb
+  EXPECTED: Scenario 2 (export symbols only)
+  ACTUAL: Scenario 2 (found symbols but no source files)
+  ok - Scenario matches expectation: Scenario 2
+
+  ERROR:  division by zero
+  BACKTRACE:
+  int4div+0x2a [0x00007FF6...]
+  ExecInterpExpr+0x1b3 [0x00007FF6...]
+  ...
+
+Note: Postgres exports ~11,000 functions, so even without a PDB, you get
+function names.  Fully stripped binaries would only show addresses, but
+that's not a scenario we care about for Postgres.
+
+Failure (PDB exists but doesn't load):
+
+  PDB file found: C:\...\postgres.pdb
+  EXPECTED: Scenario 1 (full PDB symbols)
+  ACTUAL: Scenario 2 (found symbols but no source files)
+  not ok - PDB file exists but symbols not loading!
+
+  This means DbgHelp couldn't load the PDB.  Possible causes: corrupted PDB,
+  mismatched PDB (from different build), or DbgHelp initialization failed.
+
+--------------------------------------------------------------------------------
+How It Works
+--------------------------------------------------------------------------------
+
+The test validates expected vs actual:
+
+  1. Check if postgres.pdb exists on disk
+     -> If yes, expect full PDB symbols
+     -> If no, expect export symbols only
+
+  2. Parse the backtrace output
+     -> If source files present, got PDB symbols
+     -> If no source files, got exports only
+
+  3. Compare expected to actual
+     -> Pass if they match
+     -> Fail if they don't (indicates a problem)
+
+This catches the case where the PDB exists but DbgHelp fails to load it,
+which is the most likely failure mode.
+
+--------------------------------------------------------------------------------
+Configuration
+--------------------------------------------------------------------------------
+
+The test configures the server with:
+
+  backtrace_functions = 'int4div,int4in,ExecInterpExpr'
+  log_error_verbosity = verbose
+  logging_collector = on
+  log_destination = 'stderr'
+  log_min_messages = error
+
+Nothing fancy.  Just enough to generate backtraces and make them easy to find
+in the logs.
+
+--------------------------------------------------------------------------------
+Limitations
+--------------------------------------------------------------------------------
+
+This test verifies basic functionality.  It does not:
+
+  - Detect memory leaks (would need Dr. Memory, ASAN, or similar)
+  - Test symbol server scenarios (would need network setup and config)
+  - Validate symbol accuracy in detail (just checks format)
+  - Test performance or memory usage
+  - Validate path remapping (future work)
+
+The test ensures the feature works and doesn't crash.  That's about it.
+
+--------------------------------------------------------------------------------
+Troubleshooting
+--------------------------------------------------------------------------------
+
+"PDB file exists but symbols not loading!"
+
+  The PDB is there but DbgHelp couldn't use it.
+
+  Check:
+    - Is the PDB corrupted?
+    - Does the PDB match the executable? (same build)
+    - Are there SymInitialize errors in the log?
+
+"Scenario mismatch" (other than PDB not loading)
+
+  Something weird happened.  Look at the test output to see what was expected
+  vs what was found, and figure out what's going on.
+
+No backtraces at all
+
+  Check:
+    - Is backtrace_functions configured?
+    - Is logging_collector enabled?
+    - Are there SymInitialize failures in the log?
+
+Only addresses, no function names (even without PDB)
+
+  This would be very strange, since Postgres exports thousands of functions.
+  DbgHelp should be able to get them from the export table.  Check that
+  postgres.exe was linked normally.
+
+Test hangs
+
+  Probably a logging issue.  Check that logging_collector is working and
+  log files are appearing in the data directory.
+
+--------------------------------------------------------------------------------
+Symbol Servers
+--------------------------------------------------------------------------------
+
+This test doesn't try to exercise symbol server functionality.  It just checks
+whether a local PDB file gets used.  Symbol servers are a deployment concern,
+not a functionality test.
+
+For deployments, you'd typically either:
+  - Ship PDB files alongside executables (development/staging)
+  - Don't ship PDB files (production, smaller footprint)
+
+In the latter case, you still get useful backtraces from the export table.
+Whether that's sufficient depends on your debugging needs.
+
+================================================================================
diff --git a/src/test/modules/test_backtrace/meson.build b/src/test/modules/test_backtrace/meson.build
new file mode 100644
index 0000000000..e7587baf8a
--- /dev/null
+++ b/src/test/modules/test_backtrace/meson.build
@@ -0,0 +1,13 @@
+# TAP tests - only run on Windows
+if host_system == 'windows' and cc.get_id() == 'msvc'
+  tests += {
+    'name': 'test_backtrace',
+    'sd': meson.current_source_dir(),
+    'bd': meson.current_build_dir(),
+    'tap': {
+      'tests': [
+        't/t_windows_backtrace.pl',
+      ],
+    },
+  }
+endif
diff --git a/src/test/modules/test_backtrace/t/t_windows_backtrace.pl b/src/test/modules/test_backtrace/t/t_windows_backtrace.pl
new file mode 100644
index 0000000000..e93dd2e769
--- /dev/null
+++ b/src/test/modules/test_backtrace/t/t_windows_backtrace.pl
@@ -0,0 +1,434 @@
+#!/usr/bin/perl
+
+# Copyright (c) 2025, PostgreSQL Global Development Group
+
+# Test Windows backtrace generation using DbgHelp API.
+#
+# The main thing we need to verify is that the DbgHelp integration actually
+# works, and that we can resolve symbols both with and without PDB files
+# present.  We don't try to test symbol server scenarios or anything fancy;
+# just check that if you have a PDB, you get source file info, and if you
+# don't, you still get function names from the export table (which works
+# because postgres.exe exports thousands of functions).
+#
+# The test automatically detects which situation applies by checking whether
+# postgres.pdb exists alongside postgres.exe.  This avoids the need to know
+# how the executable was built or what build type we're testing.  If the PDB
+# is there but we don't get source info, that's a bug.  If it's not there
+# but we somehow get source info anyway, that's weird and worth investigating.
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Skip if not Windows
+if ($^O ne 'MSWin32')
+{
+	plan skip_all => 'Windows-specific backtrace tests';
+}
+
+my $cc = `pg_config --cc`;
+if ($cc =~ /gcc|mingw/i)
+{
+	plan skip_all => 'Test requires MSVC build (MinGW detected)';
+}
+
+# Initialize node
+my $node = PostgreSQL::Test::Cluster->new('backtrace_test');
+$node->init;
+
+# Configure for detailed logging with backtraces
+$node->append_conf('postgresql.conf', qq{
+backtrace_functions = 'int4div,int4in,ExecInterpExpr'
+log_error_verbosity = verbose
+logging_collector = on
+log_destination = 'stderr'
+log_min_messages = error
+});
+
+$node->start;
+
+# Helper to get recent log content.
+# Backtraces go to the server log, not to psql output, so we need to read
+# the actual log files to validate anything.
+sub get_recent_log_content
+{
+	my $log_dir = $node->data_dir . '/log';
+	my @log_files = glob("$log_dir/*.log $log_dir/*.csv");
+
+	# Get the most recent log file
+	my $latest_log = (sort { -M $a <=> -M $b } @log_files)[0];
+
+	return '' unless defined $latest_log && -f $latest_log;
+
+	my $content = '';
+	open(my $fh, '<', $latest_log) or return '';
+	{
+		local $/;
+		$content = <$fh>;
+	}
+	close($fh);
+
+	return $content;
+}
+
+###############################################################################
+# First, verify basic functionality and figure out what scenario we're in.
+#
+# We trigger an error and check that (a) it actually generates a backtrace,
+# and (b) we can tell from the backtrace format whether we have PDB symbols
+# or just exports.  Then we compare that to what we should have based on
+# whether postgres.pdb exists on disk.
+###############################################################################
+
+note('');
+note('=== PART 1: Basic Error Tests & Scenario Detection ===');
+
+### Test 1: Division by zero
+my ($ret, $stdout, $stderr) = $node->psql('postgres', "SELECT 1/0;");
+ok($ret != 0, 'division by zero error occurred');
+like($stderr, qr/division by zero/i, 'division by zero error message in psql');
+
+sleep 1;
+my $log_content = get_recent_log_content();
+like($log_content, qr/ERROR:.*division by zero/i, 'error logged to server log');
+like($log_content, qr/BACKTRACE:/i, 'BACKTRACE header in log');
+
+### Test 2: Detect scenario and validate it matches expectations
+#
+# The backtrace format tells us what DbgHelp actually gave us.  Source file
+# paths mean we got PDB symbols; lack thereof means export symbols only.
+#
+# We then check whether postgres.pdb exists on disk.  If it does, we should
+# have gotten PDB symbols; if not, we should have gotten exports only.
+# Mismatches indicate a problem with DbgHelp initialization or PDB loading.
+my $has_source_files = ($log_content =~ /\[[\w:\\\/]+\.\w+:\d+\]/);  # [file.c:123]
+my $has_symbols = ($log_content =~ /\w+\+0x[0-9a-fA-F]+/);           # function+0xABC
+my $has_addresses = ($log_content =~ /\[0x[0-9a-fA-F]+\]/);          # [0xABCDEF]
+
+ok($has_symbols || $has_addresses, 'backtrace has valid format');
+
+# Determine EXPECTED scenario based on PDB file existence.
+#
+# We need to find where postgres.exe actually lives.  The TAP test framework
+# creates a temporary install, and we can find the bin directory by looking
+# at the parent of the data directory.  If that doesn't work, search PATH.
+# (This is a bit ugly but there's no direct way to ask the test node for
+# its bin directory.)
+my $datadir = $node->data_dir;
+my $postgres_exe;
+
+# Try to find the bin directory relative to data directory
+# Typical structure: .../tmp_install/PORT_XXX/data and .../tmp_install/PORT_XXX/bin
+if ($datadir =~ /^(.+)[\\\/][^\\\/]+$/)
+{
+	my $base = $1;  # Get parent directory
+	$postgres_exe = "$base/bin/postgres.exe";
+}
+
+# Fallback: try to construct from test environment
+if (!defined($postgres_exe) || !-f $postgres_exe)
+{
+	# Try using PATH - the test sets up PATH to include the bin directory
+	my $path_dirs = $ENV{PATH};
+	foreach my $dir (split(/;/, $path_dirs))
+	{
+		my $candidate = "$dir/postgres.exe";
+		if (-f $candidate)
+		{
+			$postgres_exe = $candidate;
+			last;
+		}
+	}
+}
+
+# If still not found, just use the command name and note that we couldn't find it
+if (!defined($postgres_exe))
+{
+	$postgres_exe = 'postgres.exe';
+}
+
+my $postgres_pdb = $postgres_exe;
+$postgres_pdb =~ s/\.exe$/.pdb/i;
+
+my $expected_scenario;
+if (-f $postgres_pdb)
+{
+	$expected_scenario = 1;  # PDB exists, we SHOULD have full symbols
+	note("PDB file found: $postgres_pdb");
+	note("EXPECTED: Scenario 1 (full PDB symbols)");
+}
+else
+{
+	$expected_scenario = 2;  # No PDB, we SHOULD have export symbols only
+	note("PDB file not found: $postgres_pdb");
+	note("EXPECTED: Scenario 2 (export symbols only)");
+}
+
+# Determine ACTUAL scenario from log output
+my $actual_scenario;
+if ($has_source_files && $has_symbols)
+{
+	$actual_scenario = 1;  # Full PDB symbols
+	note('ACTUAL: Scenario 1 (found source files and symbols)');
+}
+elsif ($has_symbols && !$has_source_files)
+{
+	$actual_scenario = 2;  # Export symbols only
+	note('ACTUAL: Scenario 2 (found symbols but no source files)');
+}
+else
+{
+	$actual_scenario = 0;  # Unknown/invalid
+	fail('Unable to determine scenario - PostgreSQL should always have export symbols');
+	note('Expected either Scenario 1 (with PDB) or Scenario 2 (without PDB)');
+}
+
+# CRITICAL TEST: Validate actual matches expected.
+#
+# This is the main point of the test.  We need to verify that DbgHelp is
+# actually loading symbols correctly.  If the PDB exists but we don't get
+# source files, that's a bug.  If the PDB doesn't exist but we somehow get
+# source files anyway, that's bizarre and worth investigating.
+if ($actual_scenario == $expected_scenario)
+{
+	pass("Scenario matches expectation: Scenario $actual_scenario");
+	note('');
+	if ($actual_scenario == 1) {
+		note('*** SCENARIO 1: Full PDB symbols (build WITH .pdb file) ***');
+		note('Build type: Release/Debug/DebugOptimized WITH .pdb file');
+		note('Format: function+offset [file.c:line] [0xaddress]');
+	}
+	elsif ($actual_scenario == 2) {
+		note('*** SCENARIO 2: Export symbols only (build WITHOUT .pdb file) ***');
+		note('Build type: Release WITHOUT .pdb file');
+		note('Format: function+offset [0xaddress]');
+	}
+	note('');
+}
+elsif ($expected_scenario == 1 && $actual_scenario == 2)
+{
+	fail('PDB file exists but symbols not loading!');
+	note("PDB file found at: $postgres_pdb");
+	note('Expected: Full PDB symbols with source files');
+	note('Actual: Only export symbols (no source files)');
+	note('This indicates PDB is not being loaded by DbgHelp');
+}
+elsif ($expected_scenario == 2 && $actual_scenario == 1)
+{
+	fail('Found PDB symbols but PDB file does not exist!');
+	note("PDB file not found at: $postgres_pdb");
+	note('Expected: Export symbols only');
+	note('Actual: Full PDB symbols with source files');
+	note('This is unexpected - where are the symbols coming from?');
+}
+else
+{
+	fail("Scenario mismatch: expected $expected_scenario, got $actual_scenario");
+}
+
+my $scenario = $actual_scenario;
+
+###############################################################################
+# Now validate the backtrace format matches what we expect for this scenario.
+#
+# If we have PDB symbols, check for function names, source files, and addresses.
+# If we only have exports, check that source files are absent (as expected).
+# The point is to verify that the format is sane, not just that it exists.
+###############################################################################
+
+note('');
+note('=== PART 2: Format Validation ===');
+
+if ($scenario == 1)
+{
+	# Scenario 1: Full PDB symbols
+	like($log_content, qr/\w+\+0x[0-9a-fA-F]+/,
+		'Scenario 1: function+offset format present');
+
+	like($log_content, qr/\[[\w:\\\/]+\.\w+:\d+\]/,
+		'Scenario 1: source files and line numbers present');
+
+	like($log_content, qr/\[0x[0-9a-fA-F]+\]/,
+		'Scenario 1: addresses present');
+
+	# Extract example paths to show in test output
+	my @example_paths = ($log_content =~ /\[([^\]]+\.\w+:\d+)\]/g);
+
+	if (@example_paths)
+	{
+		pass('Scenario 1: backtrace includes source file paths');
+		note("Example path: $example_paths[0]");
+	}
+	else
+	{
+		fail('Scenario 1: no source file paths found');
+	}
+}
+elsif ($scenario == 2)
+{
+	# Scenario 2: Export symbols only
+	like($log_content, qr/\w+\+0x[0-9a-fA-F]+/,
+		'Scenario 2: function+offset format present');
+
+	like($log_content, qr/\[0x[0-9a-fA-F]+\]/,
+		'Scenario 2: addresses present');
+
+	unlike($log_content, qr/\[[\w:\\\/]+\.\w+:\d+\]/,
+		'Scenario 2: no source files (expected without PDB)');
+}
+
+###############################################################################
+# Test that backtraces work for various types of errors.
+#
+# The backtrace mechanism should work regardless of what triggered the error.
+# Try a few different error types to make sure we're not somehow dependent on
+# the error path.  Also check that PL/pgSQL and triggers work, since those go
+# through different code paths.
+###############################################################################
+
+note('');
+note('=== PART 3: Error Scenario Coverage ===');
+
+### Test: Type conversion error
+($ret, $stdout, $stderr) = $node->psql('postgres', "SELECT 'invalid'::integer;");
+ok($ret != 0, 'type conversion error occurred');
+like($stderr, qr/invalid input syntax/i, 'type conversion error message');
+
+### Test: Constraint violation
+$node->safe_psql('postgres', "CREATE TABLE test_table (id integer PRIMARY KEY);");
+($ret, $stdout, $stderr) = $node->psql('postgres',
+	"INSERT INTO test_table VALUES (1), (1);");
+ok($ret != 0, 'constraint violation occurred');
+like($stderr, qr/(duplicate key|unique constraint)/i, 'constraint violation message');
+
+### Test: PL/pgSQL nested function
+$node->safe_psql('postgres', q{
+	CREATE FUNCTION nested_func() RETURNS void AS $$
+	BEGIN
+		PERFORM 1/0;
+	END;
+	$$ LANGUAGE plpgsql;
+});
+
+($ret, $stdout, $stderr) = $node->psql('postgres', "SELECT nested_func();");
+ok($ret != 0, 'nested function error occurred');
+
+sleep 1;
+$log_content = get_recent_log_content();
+my @addresses = ($log_content =~ /\[0x[0-9a-fA-F]+\]/g);
+my $frame_count = scalar @addresses;
+ok($frame_count >= 3,
+	"PL/pgSQL error has deeper stack (found $frame_count frames)");
+
+### Test: Trigger error
+$node->safe_psql('postgres', q{
+	CREATE TABLE trigger_test (val integer);
+
+	CREATE FUNCTION trigger_func() RETURNS trigger AS $$
+	BEGIN
+		PERFORM 1/0;
+		RETURN NEW;
+	END;
+	$$ LANGUAGE plpgsql;
+
+	CREATE TRIGGER test_trigger BEFORE INSERT ON trigger_test
+		FOR EACH ROW EXECUTE FUNCTION trigger_func();
+});
+
+($ret, $stdout, $stderr) = $node->psql('postgres',
+	"INSERT INTO trigger_test VALUES (1);");
+ok($ret != 0, 'trigger error occurred');
+like($stderr, qr/division by zero/i, 'trigger error message');
+
+###############################################################################
+# Verify that repeated backtrace generation doesn't cause problems.
+#
+# We don't have a good way to detect memory leaks in this test, but we can
+# at least check that the server doesn't crash or start spewing errors after
+# we've generated a bunch of backtraces.  Also verify that DbgHelp doesn't
+# log any initialization failures.
+###############################################################################
+
+note('');
+note('=== PART 4: Stability Tests ===');
+
+### Test: Multiple errors don't crash the server
+my $error_count = 0;
+for my $i (1..20)
+{
+	($ret, $stdout, $stderr) = $node->psql('postgres', "SELECT 1/0;");
+	$error_count++ if ($ret != 0);
+}
+
+is($error_count, 20, 'all 20 rapid errors occurred (DbgHelp stable)');
+
+sleep 2;
+$log_content = get_recent_log_content();
+@addresses = ($log_content =~ /\[0x[0-9a-fA-F]+\]/g);
+ok(scalar(@addresses) >= 20,
+	'multiple rapid errors produced backtraces (' . scalar(@addresses) . ' addresses found)');
+
+### Test: No SymInitialize failures
+unlike($log_content, qr/SymInitialize.*failed/i,
+	'no SymInitialize failures in log');
+
+### Test: Repeated errors in same session
+$node->safe_psql('postgres', q{
+	DO $$
+	BEGIN
+		FOR i IN 1..10 LOOP
+			BEGIN
+				EXECUTE 'SELECT 1/0';
+			EXCEPTION
+				WHEN division_by_zero THEN
+					NULL;  -- Swallow the error
+			END;
+		END LOOP;
+	END $$;
+});
+
+pass('repeated errors in same session did not crash');
+
+###############################################################################
+# Summary
+###############################################################################
+
+note('');
+note('=== TEST SUMMARY ===');
+note("Scenario: $scenario");
+note('');
+
+if ($scenario == 1) {
+	note('BUILD TYPE: Release/Debug/DebugOptimized WITH .pdb file');
+	note('');
+	note('Validated:');
+	note('  ✓ Function names with offsets');
+	note('  ✓ Source files and line numbers');
+	note('  ✓ Memory addresses');
+	note('  ✓ Stack depth (shallow and nested)');
+	note('  ✓ Multiple error scenarios');
+	note('  ✓ Stability (20 rapid errors, no crashes)');
+	note('  ✓ No DbgHelp initialization failures');
+}
+elsif ($scenario == 2) {
+	note('BUILD TYPE: Release WITHOUT .pdb file');
+	note('');
+	note('Validated:');
+	note('  ✓ Function names with offsets');
+	note('  ✓ Memory addresses');
+	note('  ✗ No source files (expected - no PDB)');
+	note('  ✓ Stack depth (shallow and nested)');
+	note('  ✓ Multiple error scenarios');
+	note('  ✓ Stability (20 rapid errors, no crashes)');
+	note('  ✓ No DbgHelp initialization failures');
+}
+note('====================');
+note('');
+
+$node->stop;
+
+done_testing();
diff --git a/src/test/modules/test_backtrace/test_backtrace--1.0.sql b/src/test/modules/test_backtrace/test_backtrace--1.0.sql
new file mode 100644
index 0000000000..f2e614a18d
--- /dev/null
+++ b/src/test/modules/test_backtrace/test_backtrace--1.0.sql
@@ -0,0 +1,66 @@
+/* src/test/modules/test_backtrace/test_backtrace--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_backtrace" to load this file. \quit
+
+--
+-- Test functions for Windows backtrace functionality
+--
+
+-- Function that triggers a division by zero error
+CREATE FUNCTION test_backtrace_div_by_zero()
+RETURNS void
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    PERFORM 1/0;
+END;
+$$;
+
+-- Function that triggers a type conversion error
+CREATE FUNCTION test_backtrace_type_error()
+RETURNS integer
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    RETURN 'not a number'::integer;
+END;
+$$;
+
+-- Nested function calls to test call stack depth
+CREATE FUNCTION test_backtrace_level3()
+RETURNS void
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    PERFORM 1/0;
+END;
+$$;
+
+CREATE FUNCTION test_backtrace_level2()
+RETURNS void
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    PERFORM test_backtrace_level3();
+END;
+$$;
+
+CREATE FUNCTION test_backtrace_level1()
+RETURNS void
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    PERFORM test_backtrace_level2();
+END;
+$$;
+
+-- Test array bounds error
+CREATE FUNCTION test_backtrace_array_bounds()
+RETURNS integer
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    RETURN ('{1,2,3}'::int[])[10];
+END;
+$$;
diff --git a/src/test/modules/test_backtrace/test_backtrace.control b/src/test/modules/test_backtrace/test_backtrace.control
new file mode 100644
index 0000000000..ac27b28287
--- /dev/null
+++ b/src/test/modules/test_backtrace/test_backtrace.control
@@ -0,0 +1,5 @@
+# test_backtrace extension
+comment = 'Test module for Windows backtrace functionality'
+default_version = '1.0'
+module_pathname = '$libdir/test_backtrace'
+relocatable = true
--
2.46.0.windows.1

#14Álvaro Herrera
alvherre@kurilemu.de
In reply to: Jakub Wartak (#12)
Re: [PATCH] Add Windows support for backtrace_functions (MSVC only)

On 2025-Oct-30, Jakub Wartak wrote:

Hi Bryan, cfbot is red. I'm was fan of having those tests for this
(bring complexity and we didn't have tests for Linux backtrace
anyway), but now MINGW win32 is failing on those tests where the
feature is not present:

I hate to say this after the code is written, but I think we should not
put any tests in the first step. I predict that these are going to be
enormously brittle and that we'll waste a lot of time making them
stable. I think we should commit the Windows support for backtraces
first, then consider whether we actually want TAP tests for the overall
feature. We've gone several years with glibc backtrace support without
any tests -- why do we think the Windows implementation thereof _must_
necessarily have them?

--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
<Schwern> It does it in a really, really complicated way
<crab> why does it need to be complicated?
<Schwern> Because it's MakeMaker.

#15Bryan Green
dbryan.green@gmail.com
In reply to: Álvaro Herrera (#14)
Re: [PATCH] Add Windows support for backtrace_functions (MSVC only)

On 10/30/2025 3:37 AM, Álvaro Herrera wrote:

On 2025-Oct-30, Jakub Wartak wrote:

Hi Bryan, cfbot is red. I'm was fan of having those tests for this
(bring complexity and we didn't have tests for Linux backtrace
anyway), but now MINGW win32 is failing on those tests where the
feature is not present:

I hate to say this after the code is written, but I think we should not
put any tests in the first step. I predict that these are going to be
enormously brittle and that we'll waste a lot of time making them
stable. I think we should commit the Windows support for backtraces
first, then consider whether we actually want TAP tests for the overall
feature. We've gone several years with glibc backtrace support without
any tests -- why do we think the Windows implementation thereof _must_
necessarily have them?

It will not bother me to remove them. It was my first effort at writing
TAP tests, so it was a nice learning experience.

#16Jakub Wartak
jakub.wartak@enterprisedb.com
In reply to: Bryan Green (#15)
Re: [PATCH] Add Windows support for backtrace_functions (MSVC only)

On Thu, Oct 30, 2025 at 10:40 AM Bryan Green <dbryan.green@gmail.com> wrote:

On 10/30/2025 3:37 AM, Álvaro Herrera wrote:

On 2025-Oct-30, Jakub Wartak wrote:

Hi Bryan, cfbot is red. I'm was fan of having those tests for this
(bring complexity and we didn't have tests for Linux backtrace
anyway), but now MINGW win32 is failing on those tests where the
feature is not present:

I hate to say this after the code is written, but I think we should not
put any tests in the first step. I predict that these are going to be
enormously brittle and that we'll waste a lot of time making them
stable. I think we should commit the Windows support for backtraces
first, then consider whether we actually want TAP tests for the overall
feature. We've gone several years with glibc backtrace support without
any tests -- why do we think the Windows implementation thereof _must_
necessarily have them?

It will not bother me to remove them. It was my first effort at writing
TAP tests, so it was a nice learning experience.

Well, that was a typo on my part (stupid me), I wanted to write: I was
NOT a fan of having those tests for this (in first place) - sorry for
confusion!

Anyway we have test because I think Michael and Euler triggered this
but earlier i've tried to persuade NOT to do this (see: `Also is it
worth it to test that setting backtrace_funciton=FOO really emits
.*FOO.* in log message cross-platform way?`), anyway Bryan implemented
this and it looks like v3 has just turned [gG]reen ;)
(https://cirrus-ci.com/build/6001832838823936)

-J.

#17Bryan Green
dbryan.green@gmail.com
In reply to: Jakub Wartak (#16)
Re: [PATCH] Add Windows support for backtrace_functions (MSVC only)

On 10/30/2025 3:52 AM, Jakub Wartak wrote:

On Thu, Oct 30, 2025 at 10:40 AM Bryan Green <dbryan.green@gmail.com> wrote:

On 10/30/2025 3:37 AM, Álvaro Herrera wrote:

On 2025-Oct-30, Jakub Wartak wrote:

Hi Bryan, cfbot is red. I'm was fan of having those tests for this
(bring complexity and we didn't have tests for Linux backtrace
anyway), but now MINGW win32 is failing on those tests where the
feature is not present:

I hate to say this after the code is written, but I think we should not
put any tests in the first step. I predict that these are going to be
enormously brittle and that we'll waste a lot of time making them
stable. I think we should commit the Windows support for backtraces
first, then consider whether we actually want TAP tests for the overall
feature. We've gone several years with glibc backtrace support without
any tests -- why do we think the Windows implementation thereof _must_
necessarily have them?

It will not bother me to remove them. It was my first effort at writing
TAP tests, so it was a nice learning experience.

Well, that was a typo on my part (stupid me), I wanted to write: I was
NOT a fan of having those tests for this (in first place) - sorry for
confusion!

Anyway we have test because I think Michael and Euler triggered this
but earlier i've tried to persuade NOT to do this (see: `Also is it
worth it to test that setting backtrace_funciton=FOO really emits
.*FOO.* in log message cross-platform way?`), anyway Bryan implemented
this and it looks like v3 has just turned [gG]reen ;)
(https://cirrus-ci.com/build/6001832838823936)

-J.

The tests are easy enough to get rid of. I think Alvaro has a good idea
of committing the windows support for backtraces and then consider
whether we want TAP tests or not. I will make a v4 patch without the
TAP tests unless someone strongly objects.

The tests don't really test whether this code would be the cause of
cause of a problem, they mainly test whether you are getting the correct
output in your backtrace. If you have a pdb file, you should get
filenames and linenumbers in addition to addresses and symbols. If you
don't have a pdb file you will only get export function symbols and
addresses. So, even if you should have a pdb and don't...you still get
something useful.

Bryan Green

#18Bryan Green
dbryan.green@gmail.com
In reply to: Jakub Wartak (#16)
1 attachment(s)
Re: [PATCH] Add Windows support for backtrace_functions (MSVC only)

On 10/30/2025 3:52 AM, Jakub Wartak wrote:

On Thu, Oct 30, 2025 at 10:40 AM Bryan Green <dbryan.green@gmail.com> wrote:

On 10/30/2025 3:37 AM, Álvaro Herrera wrote:

On 2025-Oct-30, Jakub Wartak wrote:

Hi Bryan, cfbot is red. I'm was fan of having those tests for this
(bring complexity and we didn't have tests for Linux backtrace
anyway), but now MINGW win32 is failing on those tests where the
feature is not present:

I hate to say this after the code is written, but I think we should not
put any tests in the first step. I predict that these are going to be
enormously brittle and that we'll waste a lot of time making them
stable. I think we should commit the Windows support for backtraces
first, then consider whether we actually want TAP tests for the overall
feature. We've gone several years with glibc backtrace support without
any tests -- why do we think the Windows implementation thereof _must_
necessarily have them?

It will not bother me to remove them. It was my first effort at writing
TAP tests, so it was a nice learning experience.

Well, that was a typo on my part (stupid me), I wanted to write: I was
NOT a fan of having those tests for this (in first place) - sorry for
confusion!

Anyway we have test because I think Michael and Euler triggered this
but earlier i've tried to persuade NOT to do this (see: `Also is it
worth it to test that setting backtrace_funciton=FOO really emits
.*FOO.* in log message cross-platform way?`), anyway Bryan implemented
this and it looks like v3 has just turned [gG]reen ;)
(https://cirrus-ci.com/build/6001832838823936)

-J.

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.

Thanks,
Bryan Green

Attachments:

v4-0001-Add-Windows-support-for-backtrace_functions.patchtext/plain; charset=UTF-8; name=v4-0001-Add-Windows-support-for-backtrace_functions.patchDownload
From 81ee688f490fa37b5eb30d1d88123e3d0a8423f2 Mon Sep 17 00:00:00 2001
From: Bryan Green <dbryan.green@gmail.com>
Date: Tue, 21 Oct 2025 19:31:29 -0500
Subject: [PATCH] Add Windows support for backtrace_functions

Implement backtrace generation on Windows using the DbgHelp API, providing
similar functionality to the existing Unix/Linux support. When PDB files
are available, backtraces include function names and source locations.
Without PDB files, raw addresses are shown.

DbgHelp is initialized once per backend and cleaned up via on_proc_exit().
This adds a dependency on dbghelp.lib for MSVC builds.
---
 src/backend/meson.build        |   5 +
 src/backend/utils/error/elog.c | 174 +++++++++++++++++++++++++++++++++
 2 files changed, 179 insertions(+)

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..fc421ce444 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -140,6 +140,13 @@ static void write_syslog(int level, const char *line);
 static void write_eventlog(int level, const char *line, int len);
 #endif
 
+#ifdef _MSC_VER
+#include <windows.h>
+#include <dbghelp.h>
+static bool win32_backtrace_symbols_initialized = false;
+static HANDLE win32_backtrace_process = NULL;
+#endif
+
 /* We provide a small stack of ErrorData records for re-entrant cases */
 #define ERRORDATA_STACK_SIZE  5
 
@@ -1116,6 +1123,18 @@ 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
+win32_backtrace_cleanup(int code, Datum arg)
+{
+	SymCleanup(win32_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
@@ -1147,6 +1166,161 @@ set_backtrace(ErrorData *edata, int num_skip)
 			appendStringInfoString(&errtrace,
 								   "insufficient memory for backtrace generation");
 	}
+#elif defined(_MSC_VER)
+	{
+		void	   *stack[100];
+		DWORD		frames;
+		DWORD		i;
+		wchar_t		buffer[sizeof(SYMBOL_INFOW) + MAX_SYM_NAME * sizeof(wchar_t)];
+		PSYMBOL_INFOW symbol;
+		char	   *utf8_buffer;
+		int			utf8_len;
+
+		if (!win32_backtrace_symbols_initialized)
+		{
+			win32_backtrace_process = GetCurrentProcess();
+
+			SymSetOptions(SYMOPT_UNDNAME |
+						  SYMOPT_DEFERRED_LOADS |
+						  SYMOPT_LOAD_LINES |
+						  SYMOPT_FAIL_CRITICAL_ERRORS);
+
+			if (SymInitialize(win32_backtrace_process, NULL, TRUE))
+			{
+				win32_backtrace_symbols_initialized = true;
+				on_proc_exit(win32_backtrace_cleanup, 0);
+			}
+			else
+			{
+				DWORD		error = GetLastError();
+
+				elog(WARNING, "SymInitialize failed with error %lu", error);
+			}
+		}
+
+		frames = CaptureStackBackTrace(num_skip, lengthof(stack), stack, NULL);
+
+		if (frames == 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 (i = 0; i < frames; i++)
+		{
+			DWORD64		address = (DWORD64) (stack[i]);
+			DWORD64		displacement = 0;
+			BOOL		sym_result;
+
+			sym_result = SymFromAddrW(win32_backtrace_process,
+									  address,
+									  &displacement,
+									  symbol);
+
+			if (sym_result)
+			{
+				IMAGEHLP_LINEW64 line;
+				DWORD		line_displacement = 0;
+
+				line.SizeOfStruct = sizeof(IMAGEHLP_LINEW64);
+
+				if (SymGetLineFromAddrW64(win32_backtrace_process,
+										  address,
+										  &line_displacement,
+										  &line))
+				{
+					/* 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 file name to UTF-8 */
+						filename_len = WideCharToMultiByte(CP_UTF8, 0,
+														   line.FileName, -1,
+														   NULL, 0, NULL, NULL);
+						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);
+						}
+
+						pfree(utf8_buffer);
+					}
+					else
+					{
+						/* Conversion failed, use address only */
+						appendStringInfo(&errtrace,
+										 "\n[0x%llx]",
+										 (unsigned long long) address);
+					}
+				}
+				else
+				{
+					/* No line info, convert symbol name only */
+					utf8_len = WideCharToMultiByte(CP_UTF8, 0, symbol->Name, -1,
+												   NULL, 0, NULL, NULL);
+					if (utf8_len > 0)
+					{
+						utf8_buffer = palloc(utf8_len);
+						WideCharToMultiByte(CP_UTF8, 0, symbol->Name, -1,
+											utf8_buffer, utf8_len, NULL, NULL);
+
+						appendStringInfo(&errtrace,
+										 "\n%s+0x%llx [0x%llx]",
+										 utf8_buffer,
+										 (unsigned long long) displacement,
+										 (unsigned long long) address);
+
+						pfree(utf8_buffer);
+					}
+					else
+					{
+						/* Conversion failed, use address only */
+						appendStringInfo(&errtrace,
+										 "\n[0x%llx]",
+										 (unsigned long long) address);
+					}
+				}
+			}
+			else
+			{
+				appendStringInfo(&errtrace,
+								 "\n[0x%llx]",
+								 (unsigned long long) address);
+			}
+		}
+	}
 #else
 	appendStringInfoString(&errtrace,
 						   "backtrace generation is not supported by this installation");
-- 
2.46.0.windows.1

#19Euler Taveira
euler@eulerto.com
In reply to: Bryan Green (#18)
Re: [PATCH] Add Windows support for backtrace_functions (MSVC only)

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.

+#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
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]https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/ns-dbghelp-symbol_infow, SYMBOL_INFO is an alias that automatically selects ANSI vs
UNICODE. Shouldn't we use it instead of SYMBOL_INFOW?

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

Is there a reason to continue if SymInitialize failed? It should return after
printing the message. Per the error message style guide [2]https://www.postgresql.org/docs/current/error-style-guide.html, 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.

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

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

+		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?

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

You should use SymFromAddr, no? [3]https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/nf-dbghelp-symfromaddrw I saw that you used the Unicode functions
instead of the generic functions [4]https://learn.microsoft.com/en-us/windows/win32/intl/conventions-for-function-prototypes.

+					/* 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.

+						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?

[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

--
Euler Taveira
EDB https://www.enterprisedb.com/

#20Bryan Green
dbryan.green@gmail.com
In reply to: Euler Taveira (#19)
Re: [PATCH] Add Windows support for backtrace_functions (MSVC only)

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

#21Bryan Green
dbryan.green@gmail.com
In reply to: Bryan Green (#20)
1 attachment(s)
Re: [PATCH] Add Windows support for backtrace_functions (MSVC only)

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

Attachments:

v5-0001-Add-Windows-support-for-backtrace_functions.patchtext/plain; charset=UTF-8; name=v5-0001-Add-Windows-support-for-backtrace_functions.patchDownload
From ba3bf14d523e4c656be1cf3d8bdb80afaa3355ed Mon Sep 17 00:00:00 2001
From: Bryan Green <dbryan.green@gmail.com>
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 <dbryan.green@gmail.com>
Reviewed-by: Euler Taveira <euler@eulerto.com>
Reviewed-by: Jakub Wartak <jakub.wartak@enterprisedb.com>
---
 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

#22Euler Taveira
euler@eulerto.com
In reply to: Bryan Green (#21)
Re: [PATCH] Add Windows support for backtrace_functions (MSVC only)

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.

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.)

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.

+					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).

--
Euler Taveira
EDB https://www.enterprisedb.com/

#23Bryan Green
dbryan.green@gmail.com
In reply to: Euler Taveira (#22)
1 attachment(s)
Re: [PATCH] Add Windows support for backtrace_functions (MSVC only)

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

Attachments:

v6-0001-Add-backtrace-support-for-Windows-using-DbgHelp-A.patchtext/plain; charset=UTF-8; name=v6-0001-Add-backtrace-support-for-Windows-using-DbgHelp-A.patchDownload
From 10cdd598f15d5bcc73cd65cf08138134acdfeda6 Mon Sep 17 00:00:00 2001
From: Bryan Green <dbryan.green@gmail.com>
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

#24Jakub Wartak
jakub.wartak@enterprisedb.com
In reply to: Bryan Green (#23)
Re: [PATCH] Add Windows support for backtrace_functions (MSVC only)

On Sun, Nov 2, 2025 at 6:24 AM Bryan Green <dbryan.green@gmail.com> wrote:
[..snip]

Thanks for working on this Bryan! This looks good to me, perhaps we
should set it to Ready for Committer?. Just to provide some
transparency, one can see it working properly even without access to
win32 via public CI [1]https://api.cirrus-ci.com/v1/artifact/task/4843974281658368/testrun/build/testrun/regress/regress/log/postmaster.log - that is generating proper win32 backtrace
dump - when asked to do this via [2]https://api.cirrus-ci.com/v1/artifact/task/4843974281658368/testrun/build/testrun/regress/regress/regression.diffs (just have public
postgres-public-CI connected to Cirrus, and put something like below
into the test, so it dumps the info to the log):
SET backtrace_functions = 'typenameType';
CREATE TABLE tab (id invalidtype);
RESET backtrace_functions

Pasting log in case it will disappear in future:

2026-01-06 07:47:22.102 GMT client backend[5924] pg_regress/backtrace
ERROR: type "invalidtype" does not exist at character 22
2026-01-06 07:47:22.102 GMT client backend[5924] pg_regress/backtrace
BACKTRACE:
typenameType+0x97 [0x7ff6376f7987]
[c:\cirrus\src\backend\parser\parse_type.c:270]
transformColumnType+0x27 [0x7ff637704267]
[c:\cirrus\src\backend\parser\parse_utilcmd.c:4364]
transformColumnDefinition+0x284 [0x7ff6376fe4a4]
[c:\cirrus\src\backend\parser\parse_utilcmd.c:655]
transformCreateStmt+0x50f [0x7ff6376f952f]
[c:\cirrus\src\backend\parser\parse_utilcmd.c:283]
ProcessUtilitySlow+0x26c [0x7ff6378503ac]
[c:\cirrus\src\backend\tcop\utility.c:1141]
standard_ProcessUtility+0xfef [0x7ff63784d52f]
[c:\cirrus\src\backend\tcop\utility.c:1074]
ProcessUtility+0x169 [0x7ff63784c529]
[c:\cirrus\src\backend\tcop\utility.c:528]
PortalRunUtility+0x13d [0x7ff63784b5fd]
[c:\cirrus\src\backend\tcop\pquery.c:1158]
PortalRunMulti+0x2a2 [0x7ff63784b922]
[c:\cirrus\src\backend\tcop\pquery.c:1308]
PortalRun+0x302 [0x7ff63784a5b2] [c:\cirrus\src\backend\tcop\pquery.c:787]
exec_simple_query+0x59d [0x7ff6378469cd]
[c:\cirrus\src\backend\tcop\postgres.c:1286]
PostgresMain+0xaa8 [0x7ff6378443b8]
[c:\cirrus\src\backend\tcop\postgres.c:4779]
BackendMain+0x115 [0x7ff63783dec5]
[c:\cirrus\src\backend\tcop\backend_startup.c:125]
SubPostmasterMain+0x2d6 [0x7ff63772b016]
[c:\cirrus\src\backend\postmaster\launch_backend.c:692]
main+0x329 [0x7ff6375d8d29] [c:\cirrus\src\backend\main\main.c:222]
invoke_main+0x39 [0x7ff637c06b39]
[D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:79]
__scrt_common_main_seh+0x12e [0x7ff637c06a8e]
[D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288]
__scrt_common_main+0xe [0x7ff637c0694e]
[D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:331]
mainCRTStartup+0xe [0x7ff637c06bae]
[D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_main.cpp:17]
BaseThreadInitThunk+0x10 [0x7fff63734cb0]
RtlUserThreadStart+0x2b [0x7fff63c7edcb]

-J.

[1]: https://api.cirrus-ci.com/v1/artifact/task/4843974281658368/testrun/build/testrun/regress/regress/log/postmaster.log
[2]: https://api.cirrus-ci.com/v1/artifact/task/4843974281658368/testrun/build/testrun/regress/regress/regression.diffs