From d58a560966d53640af8f7f24882ba004221f35ef Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Fri, 2 Jan 2026 22:31:05 +0100
Subject: [PATCH v4 4/4] tests: Add a test C++ extension module

While we already test that our headers are valid C++ using headerscheck,
it turns out that the macros we define might still expand to invalid
C++ code. This adds a small test extension that is compiled using C++
which uses a few macros that have caused problems in the passed or we
expect might cause problems in the future. This is definitely not meant
to be exhaustive. It's main purpose is to serve as a regression test for
previous failures, and as a place where future failures can easily be
added.

To get CI green, this also fixes a few issues when compiling C++
extensions on MSVC. Notably, our use of designated initializers in
common macros means that on MSVC we essentially need C++20. Given that
no-one has complained about that yet, it seems unlikely that anyone is
currently compiling C++ extensions on MSVC with a lower standard. GCC
and clang allow such initializers in earlier standards in also, even
though they only became an official part of C++20.
---
 .cirrus.tasks.yml                             |  6 ++-
 meson.build                                   |  4 ++
 src/include/c.h                               | 18 ++++++-
 src/include/nodes/pg_list.h                   |  8 +--
 src/test/modules/Makefile                     |  1 +
 src/test/modules/meson.build                  |  1 +
 src/test/modules/test_cplusplusext/Makefile   | 24 +++++++++
 .../modules/test_cplusplusext/meson.build     | 35 +++++++++++++
 .../test_cplusplusext/test_cplusplusext.cpp   | 51 +++++++++++++++++++
 9 files changed, 140 insertions(+), 8 deletions(-)
 create mode 100644 src/test/modules/test_cplusplusext/Makefile
 create mode 100644 src/test/modules/test_cplusplusext/meson.build
 create mode 100644 src/test/modules/test_cplusplusext/test_cplusplusext.cpp

diff --git a/.cirrus.tasks.yml b/.cirrus.tasks.yml
index 038d043d00e..94abf63265c 100644
--- a/.cirrus.tasks.yml
+++ b/.cirrus.tasks.yml
@@ -453,7 +453,8 @@ task:
 
     # SANITIZER_FLAGS is set in the tasks below
     CFLAGS: -Og -ggdb -fno-sanitize-recover=all $SANITIZER_FLAGS
-    CXXFLAGS: $CFLAGS
+    # Use -std=c++11 (not gnu++11) to catch C++ portability issues
+    CXXFLAGS: $CFLAGS -std=c++11
     LDFLAGS: $SANITIZER_FLAGS
     CC: ccache gcc
     CXX: ccache g++
@@ -573,6 +574,7 @@ task:
         su postgres <<-EOF
           set -e
           export CC='ccache gcc -m32'
+          export CXX='ccache g++ -m32'
           meson setup \
             ${MESON_COMMON_PG_CONFIG_ARGS} \
             --buildtype=debug \
@@ -806,7 +808,7 @@ task:
 
   configure_script: |
     vcvarsall x64
-    meson setup --backend ninja %MESON_COMMON_PG_CONFIG_ARGS% --buildtype debug -Db_pch=true -Dextra_lib_dirs=c:\openssl\1.1\lib -Dextra_include_dirs=c:\openssl\1.1\include -DTAR=%TAR% %MESON_FEATURES% build
+    meson setup --backend ninja %MESON_COMMON_PG_CONFIG_ARGS% --buildtype debug -Db_pch=true -Dextra_lib_dirs=c:\openssl\1.1\lib -Dextra_include_dirs=c:\openssl\1.1\include -DTAR=%TAR% -Dcpp_args=/std:c++20 %MESON_FEATURES% build
 
   build_script: |
     vcvarsall x64
diff --git a/meson.build b/meson.build
index cc3ded782e4..4a63904870e 100644
--- a/meson.build
+++ b/meson.build
@@ -2185,6 +2185,10 @@ if cc.get_id() == 'msvc'
     '/w24777', # 'function' : format string 'string' requires an argument of type 'type1', but variadic argument number has type 'type2' [like -Wformat]
   ]
 
+  cxxflags_warn += [
+    '/wd4200', # nonstandard extension used: zero-sized array in struct/union
+  ]
+
   cppflags += [
     '/DWIN32',
     '/DWINDOWS',
diff --git a/src/include/c.h b/src/include/c.h
index df6c753d908..cc35530424e 100644
--- a/src/include/c.h
+++ b/src/include/c.h
@@ -341,6 +341,16 @@
 #define pg_unreachable() abort()
 #endif
 
+/*
+ * C++ has sligthly different syntax for inline compound literals than C. GCC
+ * and Clang support he C-style syntax too, but MSVC does not.
+ */
+#ifdef __cplusplus
+#define pg_compound_literal(type, ...) (type { __VA_ARGS__ })
+#else
+#define pg_compound_literal(type, ...) ((type) { __VA_ARGS__ })
+#endif
+
 /*
  * Define a compiler-independent macro for determining if an expression is a
  * compile-time integer const.  We don't define this macro to return 0 when
@@ -948,13 +958,17 @@ pg_noreturn extern void ExceptionalCondition(const char *conditionName,
 	static_assert(condition, errmessage)
 #define StaticAssertStmt(condition, errmessage) \
 	do { static_assert(condition, errmessage); } while(0)
-#ifdef HAVE_STATEMENT_EXPRESSIONS
+#ifdef __cplusplus
+/* C++11 lambdas provide a convenient way to use static_assert as an expression */
+#define StaticAssertExpr(condition, errmessage) \
+	((void) ([](){ static_assert(condition, errmessage); }(), 0))
+#elif defined(HAVE_STATEMENT_EXPRESSIONS)
 #define StaticAssertExpr(condition, errmessage) \
 	((void) ({ static_assert(condition, errmessage); true; }))
 #else
 #define StaticAssertExpr(condition, errmessage) \
 	((void) sizeof(struct { int static_assert_failure : (condition) ? 1 : -1; }))
-#endif							/* HAVE_STATEMENT_EXPRESSIONS */
+#endif
 
 
 /*
diff --git a/src/include/nodes/pg_list.h b/src/include/nodes/pg_list.h
index ae80975548f..5498d09bcba 100644
--- a/src/include/nodes/pg_list.h
+++ b/src/include/nodes/pg_list.h
@@ -204,10 +204,10 @@ list_length(const List *l)
 /*
  * Convenience macros for building fixed-length lists
  */
-#define list_make_ptr_cell(v)	((ListCell) {.ptr_value = (v)})
-#define list_make_int_cell(v)	((ListCell) {.int_value = (v)})
-#define list_make_oid_cell(v)	((ListCell) {.oid_value = (v)})
-#define list_make_xid_cell(v)	((ListCell) {.xid_value = (v)})
+#define list_make_ptr_cell(v)	pg_compound_literal(ListCell, .ptr_value = (v))
+#define list_make_int_cell(v)	pg_compound_literal(ListCell, .int_value = (v))
+#define list_make_oid_cell(v)	pg_compound_literal(ListCell, .oid_value = (v))
+#define list_make_xid_cell(v)	pg_compound_literal(ListCell, .xid_value = (v))
 
 #define list_make1(x1) \
 	list_make1_impl(T_List, list_make_ptr_cell(x1))
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 4c6d56d97d8..92ac0a342b5 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -20,6 +20,7 @@ SUBDIRS = \
 		  test_bitmapset \
 		  test_bloomfilter \
 		  test_cloexec \
+		  test_cplusplusext \
 		  test_copy_callbacks \
 		  test_custom_rmgrs \
 		  test_custom_stats \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 1b31c5b98d6..0c7e8ad4856 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -19,6 +19,7 @@ subdir('test_aio')
 subdir('test_binaryheap')
 subdir('test_bitmapset')
 subdir('test_bloomfilter')
+subdir('test_cplusplusext')
 subdir('test_cloexec')
 subdir('test_copy_callbacks')
 subdir('test_custom_rmgrs')
diff --git a/src/test/modules/test_cplusplusext/Makefile b/src/test/modules/test_cplusplusext/Makefile
new file mode 100644
index 00000000000..19abd0f98c6
--- /dev/null
+++ b/src/test/modules/test_cplusplusext/Makefile
@@ -0,0 +1,24 @@
+# src/test/modules/test_cplusplusext/Makefile
+#
+# Test that PostgreSQL headers compile with a C++ compiler.
+# If this module compiles, the test passes.
+
+MODULE_big = test_cplusplusext
+OBJS = \
+	$(WIN32RES) \
+	test_cplusplusext.o
+PGFILEDESC = "test_cplusplusext - test C++ compatibility of PostgreSQL headers"
+
+# Use C++ compiler for linking because this module includes C++ files
+override COMPILER = $(CXX) $(CXXFLAGS)
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_cplusplusext
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_cplusplusext/meson.build b/src/test/modules/test_cplusplusext/meson.build
new file mode 100644
index 00000000000..d47fbfa7f51
--- /dev/null
+++ b/src/test/modules/test_cplusplusext/meson.build
@@ -0,0 +1,35 @@
+# Copyright (c) 2025-2026, PostgreSQL Global Development Group
+
+# This module tests that PostgreSQL headers compile with a C++ compiler.
+# It has no runtime tests - if it compiles, the test passes.
+
+if not add_languages('cpp', required: false, native: false)
+  subdir_done()
+endif
+
+cpp = meson.get_compiler('cpp')
+
+# MSVC requires C++20 for designated initializers used in PG_MODULE_MAGIC and
+# other macros. Skip if the compiler doesn't support them.
+if not cpp.compiles('''
+    struct Foo { int a; int b; };
+    Foo f = {.a = 1, .b = 2};
+    ''',
+    name: 'C++ designated initializers')
+  subdir_done()
+endif
+
+test_cplusplusext_sources = files(
+  'test_cplusplusext.cpp',
+)
+
+if host_system == 'windows'
+  test_cplusplusext_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_cplusplusext',
+    '--FILEDESC', 'test_cplusplusext - test C++ compatibility of PostgreSQL headers',])
+endif
+
+test_cplusplusext = shared_module('test_cplusplusext',
+  test_cplusplusext_sources,
+  kwargs: pg_test_mod_args,
+)
diff --git a/src/test/modules/test_cplusplusext/test_cplusplusext.cpp b/src/test/modules/test_cplusplusext/test_cplusplusext.cpp
new file mode 100644
index 00000000000..012db8b7959
--- /dev/null
+++ b/src/test/modules/test_cplusplusext/test_cplusplusext.cpp
@@ -0,0 +1,51 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_cplusplusext.cpp
+ *		Test that PostgreSQL headers compile with a C++ compiler.
+ *
+ * This file is compiled with a C++ compiler to verify that PostgreSQL
+ * headers remain compatible with C++ extensions.
+ *
+ * Copyright (c) 2025-2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/test/modules/test_cplusplusext/test_cplusplusext.cpp
+ *
+ * -------------------------------------------------------------------------
+ */
+
+extern "C" {
+#include "postgres.h"
+#include "fmgr.h"
+#include "nodes/pg_list.h"
+#include "nodes/parsenodes.h"
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(test_cplusplus_compat);
+}
+
+StaticAssertDecl(sizeof(int32) == 4, "int32 should be 4 bytes");
+
+extern "C" Datum
+test_cplusplus_compat(PG_FUNCTION_ARGS)
+{
+	List	   *node_list = list_make1(makeNode(RangeTblRef));
+	RangeTblRef *copy = copyObject(linitial_node(RangeTblRef, node_list));
+
+	foreach_ptr(RangeTblRef, rtr, node_list) {
+		rtr->rtindex++;
+	}
+
+	foreach_node(RangeTblRef, rtr, node_list) {
+		rtr->rtindex++;
+	}
+
+	StaticAssertStmt(sizeof(int32) == 4, "int32 should be 4 bytes");
+	(void) StaticAssertExpr(sizeof(int64) == 8, "int64 should be 8 bytes");
+
+	pfree(copy);
+	list_free_deep(node_list);
+
+	PG_RETURN_VOID();
+}
-- 
2.52.0

