Skip to content

Commit

Permalink
Feature-test macros + config header!
Browse files Browse the repository at this point in the history
We're getting bug reports (e.g. #732) for situations where people have a
libpqxx compiled as C++17 but an application compiled as C++20, or _vice
versa._

Generally it's probably not wise to link C++17-compiled code to code
compiled as C++20.  But two things I did exacerbate the problem:
1. I moved a bunch of C++ feature checks to compile time, using C++20's
   feature test macros.  It looked like a much cleaner, easier way
   forward with reduced maintenance overhead.
2. Usage of C++20's `std::source_location` _when present_ affects the
   ABI.  So effectively there's one ABI with, and one without.  I see
   that mostly as the price of doing libraries in C++ — it's generally
   dangerous to package library binaries, unless they've been designed
   to be compatible, or they come as a range of binaries for various
   compilers, versions, and configurations.

And the real problem is that _these two changes interacted._  The
detection of support for `std::source_location` happened at compile
time.  And so if you compile libpqxx in C++17 (without this feature)
and the application in C++20 (with this feature), the two will not be
link-compatible!

In this first commit I'm prototyping a new approach that I hope will
combine the ease of maintenance of feature test macros with the ABI
stability of a configuration header.  Configuration speed should lie
somewhere inbetween: no more compiling separate little checks for
every individual C++ feature.

But it's not easy.  There are 2 orthogonal binary choices, leaving me
with 4 scenarios to support:
* Autoconf vs. CMake
* Cross-compiled vs. native.

How does cross compilation factor into it?  It works like this: I need
to check C++ feature test macros.  I check them in C++.  But I don't
want to keep editing that C++ code for every feature — there's a
repetitive preprocessor dance that I don't think I could simplify to one
simple line of code, because I'd need to pass macro names as parameters
to macros.  So, I write a minimal config file and run it through a
Python script that generates the C++ code for me.  Then I have the build
configuration machinery compile _and run_ that C++ code, and generate
the configuration header.

Yes, alright, but how does cross compilation factor into it?  First, if
you're cross-compiling, it's not a given that you can _run_ the binary
you produce on the same system!  The whole point is that the two systems
are different.  And two, you'll have a native compilation environment
but there's no guarantee that it will resemble the cross compilation
environment at all.  So if you compile a binary to run locally, you may
get very different results.

So for cross-compilation, the Python script just generates a minimal
configuration header that just has all features disabled.  And in this
first commit I've got that working for autoconf.  But I'm still
struggling with CMake (thanks @KayEss for helping with this).

If it gets too difficult, I may go a different route: generate C++ code
from Python, but only run it through the preprocessor.  (I believe
autoconf has a standard, portable way of running the preprocessor; let's
hope CMake has one as well.)  The output will still C++ code, but now
it's been preprocessed, so hopefully it'll be possible to tell portably
which features are present.  And ironically, I think I'd then have to
have another Python script to _postprocess_ the preprocessed code and
turn it into a ready-to-use config header.
  • Loading branch information
jtv committed Oct 28, 2023
1 parent 7586846 commit fbb44b2
Show file tree
Hide file tree
Showing 5 changed files with 406 additions and 4 deletions.
59 changes: 59 additions & 0 deletions aclocal.m4
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,65 @@ You have another version of autoconf. It may work, but is not guaranteed to.
If you have problems, you may need to regenerate the build system entirely.
To do so, use the procedure documented by the package, typically 'autoreconf'.])])

# ===========================================================================
# https://www.gnu.org/software/autoconf-archive/ax_file_escapes.html
# ===========================================================================
#
# SYNOPSIS
#
# AX_FILE_ESCAPES
#
# DESCRIPTION
#
# Writes the specified data to the specified file.
#
# LICENSE
#
# Copyright (c) 2008 Tom Howard <[email protected]>
#
# Copying and distribution of this file, with or without modification, are
# permitted in any medium without royalty provided the copyright notice
# and this notice are preserved. This file is offered as-is, without any
# warranty.

#serial 8

AC_DEFUN([AX_FILE_ESCAPES],[
AX_DOLLAR="\$"
AX_SRB="\\135"
AX_SLB="\\133"
AX_BS="\\\\"
AX_DQ="\""
])

# ===========================================================================
# https://www.gnu.org/software/autoconf-archive/ax_print_to_file.html
# ===========================================================================
#
# SYNOPSIS
#
# AX_PRINT_TO_FILE([FILE],[DATA])
#
# DESCRIPTION
#
# Writes the specified data to the specified file.
#
# LICENSE
#
# Copyright (c) 2008 Tom Howard <[email protected]>
#
# Copying and distribution of this file, with or without modification, are
# permitted in any medium without royalty provided the copyright notice
# and this notice are preserved. This file is offered as-is, without any
# warranty.

#serial 8

AC_DEFUN([AX_PRINT_TO_FILE],[
AC_REQUIRE([AX_FILE_ESCAPES])
printf "$2" > "$1"
])

# Copyright (C) 2002-2021 Free Software Foundation, Inc.
#
# This file is free software; the Free Software Foundation
Expand Down
159 changes: 157 additions & 2 deletions configure
Original file line number Diff line number Diff line change
Expand Up @@ -1933,6 +1933,49 @@ printf "%s\n" "$ac_res" >&6; }
eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno

} # ac_fn_cxx_check_header_compile

# ac_fn_cxx_try_run LINENO
# ------------------------
# Try to run conftest.$ac_ext, and return whether this succeeded. Assumes that
# executables *can* be run.
ac_fn_cxx_try_run ()
{
as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack
if { { ac_try="$ac_link"
case "(($ac_try" in
*\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
*) ac_try_echo=$ac_try;;
esac
eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\""
printf "%s\n" "$ac_try_echo"; } >&5
(eval "$ac_link") 2>&5
ac_status=$?
printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
test $ac_status = 0; } && { ac_try='./conftest$ac_exeext'
{ { case "(($ac_try" in
*\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
*) ac_try_echo=$ac_try;;
esac
eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\""
printf "%s\n" "$ac_try_echo"; } >&5
(eval "$ac_try") 2>&5
ac_status=$?
printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
test $ac_status = 0; }; }
then :
ac_retval=0
else $as_nop
printf "%s\n" "$as_me: program exited with status $ac_status" >&5
printf "%s\n" "$as_me: failed program was:" >&5
sed 's/^/| /' conftest.$ac_ext >&5

ac_retval=$ac_status
fi
rm -rf conftest.dSYM conftest_ipa8_conftest.oo
eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno
as_fn_set_status $ac_retval

} # ac_fn_cxx_try_run
ac_configure_args_raw=
for ac_arg
do
Expand Down Expand Up @@ -3571,7 +3614,6 @@ ac_config_headers="$ac_config_headers include/pqxx/config.h"
# Read test programme from config-test.



# Checks for programs.


Expand Down Expand Up @@ -17600,6 +17642,7 @@ printf "%s\n" "$cxa_demangle" >&6; }


# C++20: Assume support.
# XXX: Replace with generated pqxx_have_concepts.
# Check for sufficient Concepts support, introduced with C++20.
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking concepts" >&5
printf %s "checking concepts... " >&6; }
Expand Down Expand Up @@ -17664,6 +17707,7 @@ printf "%s\n" "$concepts" >&6; }


# C++20: Assume support.
# XXX: Replace with generated pqxx_have_span.
# Check for C++20 std::span.
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking std::span" >&5
printf %s "checking std::span... " >&6; }
Expand Down Expand Up @@ -18807,6 +18851,118 @@ rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
printf "%s\n" "yes" >&6; }

# Configuration based on C++ feature test macros. We want a resuable header
# describing these, so users can build a libpqxx with a fixed, stable config
# even when actual compile options may differ.
#
# Unfortunately this will not work when cross-compiling. There is just no
# guarantee that compilation for the machine where we're building will be
# at all similar to compilation for the target architecture. In that case,
# we will generate a dummy config header with all features disabled.
mkdir -p include/pqxx

AX_DOLLAR="\$"
AX_SRB="\\135"
AX_SLB="\\133"
AX_BS="\\\\"
AX_DQ="\""

if test "$cross_compiling" = yes
then :
# We're cross-compiling. Generate a dummy.


printf "// Dummy settings for cross-compile. Configure manually.
#define pqxx_have_concepts false
#define pqxx_have_multidim false
#define pqxx_have_source_location false
#define pqxx_have_span false
#define pqxx_have_ssize false
#define pqxx_have_unreachable false
" > "include/pqxx/cxx_features.hxx"


else $as_nop
cat confdefs.h - <<_ACEOF >conftest.$ac_ext
/* end confdefs.h. */
#include <fstream>

int main()
{
std::ofstream("include/pqxx/cxx_features.hxx") <<

// Feature test macros for libpqxx.
// Generated by generate_checks.py.

"#define pqxx_have_concepts "
#if defined(__cpp_concepts) && __cpp_concepts
"true"
#else
"false"
#endif
"\n"

"#define pqxx_have_multidim "
#if defined(__cpp_multidimensional_subscript) && __cpp_multidimensional_subscript
"true"
#else
"false"
#endif
"\n"

"#define pqxx_have_source_location "
#if defined(__cpp_lib_source_location) && __cpp_lib_source_location
"true"
#else
"false"
#endif
"\n"

"#define pqxx_have_span "
#if defined(__cpp_lib_span) && __cpp_lib_span
"true"
#else
"false"
#endif
"\n"

"#define pqxx_have_ssize "
#if defined(__cpp_lib_ssize) && __cpp_lib_ssize
"true"
#else
"false"
#endif
"\n"

"#define pqxx_have_unreachable "
#if defined(__cpp_lib_unreachable) && __cpp_lib_unreachable
"true"
#else
"false"
#endif
"\n"

<< std::endl;
}



_ACEOF
if ac_fn_cxx_try_run "$LINENO"
then :
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: Generated cxx_features.hxx." >&5
printf "%s\n" "Generated cxx_features.hxx." >&6; }
else $as_nop
{ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;}
as_fn_error $? "\"C++ features check failed.\"
See \`config.log' for more details" "$LINENO" 5; }
fi
rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \
conftest.$ac_objext conftest.beam conftest.$ac_ext
fi



{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether ${MAKE-make} sets \$(MAKE)" >&5
printf %s "checking whether ${MAKE-make} sets \$(MAKE)... " >&6; }
Expand Down Expand Up @@ -18844,7 +19000,6 @@ fi
ac_config_files="$ac_config_files Makefile config/Makefile doc/Makefile doc/Doxyfile src/Makefile test/Makefile tools/Makefile include/Makefile include/pqxx/Makefile libpqxx.pc"



ac_config_commands="$ac_config_commands configitems"


Expand Down
32 changes: 30 additions & 2 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ AC_PREFIX_DEFAULT(/usr/local)
AC_DEFUN([read_test], [AC_LANG_SOURCE(
esyscmd(tools/m4esc.py --input=config-tests/$1))])


# Checks for programs.
AC_PROG_CXX
AC_PROG_INSTALL
Expand Down Expand Up @@ -300,6 +299,7 @@ AC_MSG_RESULT($cxa_demangle)


# C++20: Assume support.
# XXX: Replace with generated pqxx_have_concepts.
# Check for sufficient Concepts support, introduced with C++20.
AC_MSG_CHECKING([concepts])
concepts=yes
Expand All @@ -314,6 +314,7 @@ AC_MSG_RESULT($concepts)


# C++20: Assume support.
# XXX: Replace with generated pqxx_have_span.
# Check for C++20 std::span.
AC_MSG_CHECKING([std::span])
span=yes
Expand Down Expand Up @@ -697,6 +698,34 @@ change!
])])
AC_MSG_RESULT(yes)

# Configuration based on C++ feature test macros. We want a resuable header
# describing these, so users can build a libpqxx with a fixed, stable config
# even when actual compile options may differ.
#
# Unfortunately this will not work when cross-compiling. There is just no
# guarantee that compilation for the machine where we're building will be
# at all similar to compilation for the target architecture. In that case,
# we will generate a dummy config header with all features disabled.
mkdir -p include/pqxx
AC_RUN_IFELSE(
[AC_LANG_SOURCE(
esyscmd(
"tools/generate_checks.py" \
"cxx_features.txt" \
"-H" "include/pqxx/cxx_features.hxx"
)
)],
[AC_MSG_RESULT([Generated cxx_features.hxx.])],
[AC_MSG_FAILURE(["C++ features check failed."])],
# We're cross-compiling. Generate a dummy.
[AX_PRINT_TO_FILE(
include/pqxx/cxx_features.hxx,
esyscmd(
"tools/generate_checks.py" \
"cxx_features.txt" \
"--dummy"))]
)


AC_PROG_MAKE_SET

Expand All @@ -705,7 +734,6 @@ AC_CONFIG_FILES([
test/Makefile tools/Makefile include/Makefile include/pqxx/Makefile
libpqxx.pc])


AC_CONFIG_COMMANDS([configitems], ["${srcdir}/tools/splitconfig" "${srcdir}"])

AC_CONFIG_FILES([compile_flags])
Expand Down
6 changes: 6 additions & 0 deletions cxx_features.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
__cpp_concepts pqxx_have_concepts
__cpp_multidimensional_subscript pqxx_have_multidim
__cpp_lib_source_location pqxx_have_source_location
__cpp_lib_span pqxx_have_span
__cpp_lib_ssize pqxx_have_ssize
__cpp_lib_unreachable pqxx_have_unreachable
Loading

0 comments on commit fbb44b2

Please sign in to comment.