From f0d4b25e67555cd7f43cb17ab7bcff870bfea669 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 v5 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 when compiling for older C++
standards, even though they only became an official part of C++20.
---
 .cirrus.tasks.yml                             |  7 ++-
 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, 141 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 2a821593ce5..c4b4d213b68 100644
--- a/.cirrus.tasks.yml
+++ b/.cirrus.tasks.yml
@@ -450,7 +450,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++
@@ -802,9 +803,11 @@ task:
     echo 127.0.0.3 pg-loadbalancetest >> c:\Windows\System32\Drivers\etc\hosts
     type c:\Windows\System32\Drivers\etc\hosts
 
+  # Uses C++20 standard, because that's needed for MSVC to accept designated
+  # initializers.
   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 55df1dd3797..63d725dfc9d 100644
--- a/meson.build
+++ b/meson.build
@@ -2192,6 +2192,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 65173b9d9fd..d2f354e7e15 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
@@ -956,13 +966,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

