Initial sketch.
authorMark Wooding <mdw@distorted.org.uk>
Thu, 27 Apr 2023 13:50:40 +0000 (14:50 +0100)
committerMark Wooding <mdw@distorted.org.uk>
Sun, 30 Apr 2023 18:59:12 +0000 (19:59 +0100)
13 files changed:
.gitignore [new file with mode: 0644]
.skelrc [new file with mode: 0644]
FINALLY.3 [new file with mode: 0644]
Makefile.am [new file with mode: 0644]
configure.ac [new file with mode: 0644]
examine-binary [new file with mode: 0755]
finally-cxx-test.cc [new file with mode: 0644]
finally-test.c [new file with mode: 0644]
finally-test.h [new file with mode: 0644]
finally.h [new file with mode: 0644]
m4/finally.m4 [new file with mode: 0644]
test-guts.c [new file with mode: 0644]
try-catch.cc [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..9e92c26
--- /dev/null
@@ -0,0 +1,19 @@
+## Pervasive build machinery.
+Makefile.in
+
+## Top-level generated files.
+/aclocal.m4
+/autom4te.cache/
+/configure
+/precomp/
+
+## GNU build system machinery installed by `autoreconf'.
+/config/compile
+/config/config.guess
+/config/config.sub
+/config/depcomp
+/config/install-sh
+/config/ltmain.sh
+/config/missing
+/config/tap-driver.sh
+/config/test-driver
diff --git a/.skelrc b/.skelrc
new file mode 100644 (file)
index 0000000..2d46352
--- /dev/null
+++ b/.skelrc
@@ -0,0 +1,9 @@
+;;; -*-emacs-lisp-*-
+
+(setq skel-alist
+      (append
+       '((author . "Mark Wooding")
+        (full-title . "the `Finally' package")
+        (library . "Finally")
+        (licence-text . "[[lgpl-2]]"))
+       skel-alist))
diff --git a/FINALLY.3 b/FINALLY.3
new file mode 100644 (file)
index 0000000..a7de5fc
--- /dev/null
+++ b/FINALLY.3
@@ -0,0 +1,162 @@
+.\" -*-nroff-*-
+.TH control 3 "28 April 2023" "Straylight/Edgeware" "FINALLY macro package"
+.SH NAME
+FINALLY \- defer execution until scope exit
+.SH SYNOPSIS
+.nf
+.B "#include <finally.h>"
+
+.BI FINALLY( stmt );
+.BI FINALLY_TAGGED( tag ", " stmt );
+.fi
+.
+.SH DESCRIPTION
+.
+.SS "General usage of FINALLY"
+The
+.B FINALLY
+macro arranges that the statement
+.I stmt
+should be executed when control leaves the immediately enclosing scope,
+whether this is by just running off the end, or by explicit control
+flow, e.g.,
+.BR return ,
+.BR goto ,
+.BR break,
+or
+.BR continue .
+If a scope contains multiple
+.B FINALLY
+calls, then the statements are executed in reverse order.
+.PP
+In particular, if the macro call is placed at top-level within a
+function body, then
+.I stmt
+is executed when the function returns, either as a result of a
+.B return
+statement, or by running off the end of the function body.
+.PP
+With GNU-like compilers, you can arrange for the statement to be
+executed when control escapes the scope as the result of a C++ exception
+by setting the
+.B \-fexceptions
+compiler flag.
+.PP
+The statement will
+.I not
+be executed if control leaves the scope as a result of
+.BR longjmp (3)
+or similar.  It also won't be executed if control `leaves' as a result
+of a call to
+.BR exit (3)
+or similar; but that's almost certainly what you want.
+.PP
+The
+.B FINALLY
+macro's expansion is syntactically one or more declarations.  In C89
+code, therefore, it must appear at the head of a block, before any
+statements.  In C99 or later, it may appear anywhere in a block.
+However, because it may expand to more than one declaration, it is not
+suitable for use in a
+.B for
+statement.
+.
+.SS "Macros and FINALLY_TAGGED"
+At most one use of
+.B FINALLY
+can occur on any given line of source code.  In particular, this
+rule forbids macros which expand into more than one use of
+.BR FINALLY ,
+since the macro expansions will cause them all to be credited to the
+line holding the original expansion site.  To avoid trouble, macro
+authors should use
+.B FINALLY_TAGGED
+instead.  Each use of
+.B FINALLY_TAGGED
+on the same source line must have a distinct
+.I tag
+argument, which may be any identifier.
+.
+.SS "Nonlocal transfers, and thread and process exits"
+The statement will
+.I not
+be executed if control leaves the scope as a result of
+.BR longjmp (3)
+or similar.  It also won't be executed if control `leaves' as a result
+of a call to
+.BR exit (3),
+.BR pthread_exit (3),
+.BR thrd_exit (3),
+or similar.
+.PP
+You didn't want
+.B FINALLY
+blocks to run at
+.BR exit (3)
+time.  Almost all uses of
+.B FINALLY
+are to release process-local resources such as memory, file descriptors
+or locks, or something else similar.  The kernel will release these by
+itself when the process exits, because leaking resources when processes
+crash would just be too awful to consider.  All you'd achieve by
+carefully freeing memory prior to
+.BR exit (3)
+would be to waste time.
+.
+.SS "Setup and configuration"
+The
+.B FINALLY
+macro uses compiler-specific techniques.  Not only does it need
+different implementations on different compilers, but it may require
+unusual compiler options and runtime support libraries to work.
+.PP
+Firstly, the
+.B <finally.h>
+header can work portably under C++11 or later, using RAII and lambdas:
+idiomatic C++ doesn't benefit much from this, but the facility is there
+anyway.  If the header detects that such a compiler is in use (the macro
+.B __cplusplus
+is defined to a value greater than or equal to 201103), then it will use
+the C++-specific implementation.
+.PP
+Otherwise, the header checks the value of
+.BR FINALLY_CONFIG_FLAVOUR :
+it should be one of the following options.
+.TP
+.B NIL
+No support.  An error is reported (using
+.BR #error ).
+.TP
+.B GCC_NESTED_FUNCTIONS
+Use the GNU C Compiler's support for nested functions and the
+.B cleanup
+function attribute.  Note that this use of nested functions does not
+involve the use of trampolines, and does not require an executable
+stack.  (This is verified as part of the package tests.)
+.PP
+If
+.B FINALLY_CONFIG_FLAVOUR
+is not set, then
+.B <finally.h>
+guesses one of these flavours based on the compiler version and other
+predefined macros.  This guess may be wrong, and things may not work
+anyway because necessary compiler options or libraries aren't available.
+.PP
+The best way to set everything up correctly is to use the provided
+.B FINALLY_CHECK
+Autoconf macro.  It will define
+.B FINALLY_CONFIG_FLAVOUR
+to a suitable value, and also report necessary compiler flags and
+libraries in the
+.B FINALLY_CFLAGS
+and
+.B FINALLY_LIBS
+makefile variables.  See the
+.B finally.m4
+file's internal documentation for the details.
+.
+.SH BUGS
+The selection of supported compilers is extremely limited.
+.
+.SH AUTHOR
+Mark Wooding, <mdw@distorted.org.uk>
diff --git a/Makefile.am b/Makefile.am
new file mode 100644 (file)
index 0000000..d46043e
--- /dev/null
@@ -0,0 +1,78 @@
+### -*-automake-*-
+###
+### Build script for `finally'
+###
+### (c) 2023 Mark Wooding
+###
+
+###----- Licensing notice ---------------------------------------------------
+###
+### This file is part of the `Finally' package.
+###
+### Finally is free software: you can redistribute it and/or modify it
+### under the terms of the GNU Library General Public License as published
+### by the Free Software Foundation; either version 2 of the License, or
+### (at your option) any later version.
+###
+### Finally is distributed in the hope that it will be useful, but WITHOUT
+### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+### FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+### License for more details.
+###
+### You should have received a copy of the GNU Library General Public
+### License along with Finally.  If not, write to the Free Software
+### Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
+### USA.
+
+include_HEADERS                 =
+dist_man_MANS           =
+EXTRA_DIST              =
+
+###--------------------------------------------------------------------------
+### The main build.
+
+include_HEADERS                += finally.h
+dist_man_MANS          += FINALLY.3
+
+###--------------------------------------------------------------------------
+### Test configuration.
+
+check_PROGRAMS          =
+check_LIBRARIES                 =
+TESTS                   =
+
+AM_TESTS_ENVIRONMENT    = env TEST_OUTFORM=tap
+LOG_DRIVER              = env AM_TAP_AWK=$(AWK) $(SHELL) \
+                               $(top_srcdir)/config/tap-driver.sh
+
+check_LIBRARIES                += libfintest.a
+libfintest_a_SOURCES    =
+libfintest_a_SOURCES   += finally-test.h
+libfintest_a_SOURCES   += test-guts.c
+if FEXCEPTIONS
+libfintest_a_SOURCES   += try-catch.cc
+endif
+
+if C
+## The C test program.
+check_PROGRAMS         += finally-test
+finally_test_SOURCES    = finally-test.c
+if FEXCEPTIONS
+nodist_EXTRA_finally_test_SOURCES = bodge.cc
+endif
+finally_test_LDADD      = libfintest.a $(FINALLY_LIBS)
+TESTS                  += finally-test
+TESTS                  += examine-binary
+EXTRA_DIST             += examine-binary
+endif
+
+if CXX14
+## The C++ test program.  Which is actually exactly the same program, only
+## (partially) compiled with a C++ compiler.
+check_PROGRAMS         += finally-cxx-test
+finally_cxx_test_SOURCES = finally-cxx-test.cc
+finally_cxx_test_LDADD  = libfintest.a
+TESTS                  += finally-cxx-test
+endif
+
+###----- That's all, folks --------------------------------------------------
diff --git a/configure.ac b/configure.ac
new file mode 100644 (file)
index 0000000..f1c0dc1
--- /dev/null
@@ -0,0 +1,152 @@
+dnl -*-autoconf-*-
+dnl
+dnl Configuration script for `finally'
+dnl
+dnl (c) 2023 Mark Wooding
+dnl
+
+dnl----- Licensing notice ---------------------------------------------------
+dnl
+dnl This file is part of the `Finally' package.
+dnl
+dnl Finally is free software: you can redistribute it and/or modify it
+dnl under the terms of the GNU Library General Public License as published
+dnl by the Free Software Foundation; either version 2 of the License, or
+dnl (at your option) any later version.
+dnl
+dnl Finally is distributed in the hope that it will be useful, but WITHOUT
+dnl ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+dnl FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+dnl License for more details.
+dnl
+dnl You should have received a copy of the GNU Library General Public
+dnl License along with Finally.  If not, write to the Free Software
+dnl Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
+dnl USA.
+
+dnl--------------------------------------------------------------------------
+dnl Initialization.
+
+AC_INIT([finally], [0.9], [mdw@distorted.org.uk])
+AC_CONFIG_SRCDIR([finally.h])
+AC_CONFIG_AUX_DIR([config])
+AC_CONFIG_MACRO_DIR([m4])
+AM_INIT_AUTOMAKE([foreign])
+AC_REQUIRE_AUX_FILE([tap-driver.sh])
+AM_SILENT_RULES([yes])
+
+dnl--------------------------------------------------------------------------
+dnl C programming environment.
+
+AC_PROG_CC
+AM_PROG_CC_C_O
+AC_SUBST(AM_CFLAGS)
+
+case $GCC in
+  yes)
+    AM_CFLAGS="$AM_CFLAGS -std=c89 -pedantic -Wall -Wextra -Werror"
+    ;;
+esac
+
+dnl--------------------------------------------------------------------------
+dnl C++ programming environment.
+
+AC_PROG_CXX
+AC_SUBST(AM_CXXFLAGS)
+
+case $GXX in
+  yes)
+    AM_CXXFLAGS="$AM_CXXFLAGS -std=c++11 -pedantic -Wall -Wextra -Werror"
+    ;;
+esac
+
+AC_DEFUN([FINALLY_CXX14_PROGRAM], [AC_LANG_PROGRAM([], [
+#if !defined(__cplusplus) || __cplusplus < 201103
+  choke me
+#endif
+])])
+
+AC_CACHE_CHECK([whether $CXX works at all], [finally_cv_cxx_works_p], [
+  AC_LANG_PUSH([C++])
+  AC_COMPILE_IFELSE([AC_LANG_PROGRAM([], [])],
+                   [finally_cv_cxx_works_p=yes],
+                   [finally_cv_cxx_works_p=no])
+  AC_LANG_POP([C++])])
+
+case $finally_cv_cxx_works_p in
+  yes)
+    AC_CACHE_CHECK([whether $CC accepts \`-fexceptions'],
+                  [finally_cv_gcc_fexceptions_p], [
+      AC_LANG_PUSH([C])
+      finally_test_original_CFLAGS=$CFLAGS
+      CFLAGS="$CFLAGS -fexceptions"
+      AC_COMPILE_IFELSE([AC_LANG_PROGRAM([], [])],
+                       [finally_cv_gcc_fexceptions_p=yes],
+                       [finally_cv_gcc_fexceptions_p=no])
+      CFLAGS=$finally_test_original_CFLAGS
+      AC_LANG_POP([C])])
+    ;;
+  no)
+    finally_cv_gcc_fexceptions_p=no
+    ;;
+esac
+
+case $finally_cv_cxx_works_p in
+  yes)
+    AC_CACHE_CHECK([whether $CXX supports C++11], [finally_cv_cxx14_p], [
+      AC_LANG_PUSH([C++])
+      AC_COMPILE_IFELSE([FINALLY_CXX14_PROGRAM],
+                       [finally_cv_cxx14_p=yes],
+                       [finally_cv_cxx14_p=no])
+      AC_LANG_POP([C++])])
+    ;;
+  *)
+    finally_cv_cxx14_p=no
+    CXX=\${CC}
+    ;;
+esac
+
+dnl--------------------------------------------------------------------------
+dnl Other tools.
+
+AC_CHECK_PROGS([autom4te])
+AC_PROG_RANLIB
+
+dnl--------------------------------------------------------------------------
+dnl Test configuration.
+
+finally_test_original_CFLAGS=$CFLAGS
+CFLAGS="$CFLAGS $AM_CFLAGS"
+case $finally_cv_gcc_fexceptions_p in
+  yes) CFLAGS="$CFLAGS -fexceptions" ;;
+esac
+FINALLY_CHECK
+CFLAGS=$finally_test_original_CFLAGS
+AM_CFLAGS="$AM_CFLAGS $FINALLY_CFLAGS"
+AM_CONDITIONAL([C], [test $finally_flavour != NIL])
+AM_CONDITIONAL([CXX], [test $finally_cv_cxx_works_p = yes])
+AM_CONDITIONAL([CXX14], [test $finally_cv_cxx14_p = yes])
+
+case $finally_flavour,$finally_cv_cxx14_p in
+  NIL,no)
+    AC_MSG_ERROR([no suitable C or C++ compiler found: not going to space today])
+    ;;
+esac
+
+case $finally_flavour,$finally_cv_gcc_fexceptions_p,$finally_cv_cxx_works_p in
+  NIL,*,no) finally_fexceptions_p=no ;;
+  *,yes,*) AM_CFLAGS="$AM_CFLAGS -fexceptions"; finally_fexceptions_p=yes ;;
+  *,yes) finally_fexceptions_p=yes ;;
+esac
+case $finally_fexceptions_p in
+  yes) AC_DEFINE([HAVE_FEXCEPTIONS], [1]) ;;
+esac
+AM_CONDITIONAL([FEXCEPTIONS], [test $finally_fexceptions_p = yes])
+
+dnl--------------------------------------------------------------------------
+dnl Output.
+
+AC_CONFIG_FILES([Makefile])
+AC_OUTPUT
+
+dnl----- That's all, folks --------------------------------------------------
diff --git a/examine-binary b/examine-binary
new file mode 100755 (executable)
index 0000000..acae4e6
--- /dev/null
@@ -0,0 +1,37 @@
+#! /bin/sh -e
+
+echo "1..1"
+env | sort | sed 's/^/# /'
+
+check_exe () {
+  tid=$1 exe=$2
+  if ! [ -f $exe ]; then
+    echo "ok $tid # skip executable not found"; return; fi
+  objty=$(objdump -f $exe | sed -n '/^.*: *file format \(.*\)/s//\1/p')
+  echo "# $exe object type $objty"
+  case $objty in
+    elf32-* | elf64-*)
+      if f=$(objdump -p $exe |
+              sed -n '/^ *STACK / { n; s/^.*flags *\([-rwx]*\).*/\1/p; }')
+      then
+       echo "# STACK segment flags $f"
+       case $f in
+         rw-) echo "ok $tid stack not executable" ;;
+         rwx) echo "not ok $tid executable stack" ;;
+         *) echo "not ok $tid unexpected stack-segment flags" ;;
+       esac
+      else
+       echo "not ok $tid failed to find stack-segment flags"
+      fi
+      ;;
+    pei-x86-64)
+      if nm $exe | grep __enable_execute_stack; then
+       echo "not ok $tid # found call to __enable_execute_stack"
+      else
+       echo "ok $tid # stack maybe not executable"
+      fi
+      ;;
+  esac
+}
+
+check_exe 1 finally-test
diff --git a/finally-cxx-test.cc b/finally-cxx-test.cc
new file mode 100644 (file)
index 0000000..b01ce89
--- /dev/null
@@ -0,0 +1,29 @@
+/* -*-c++-*-
+ *
+ * Test program for using `finally.h' from C++
+ *
+ * (c) 2023 Mark Wooding
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of the `Finally' package.
+ *
+ * Finally is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Library General Public License as published
+ * by the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Finally is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with Finally.  If not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
+ * USA.
+ */
+
+/* Let's just use the usual C code. */
+#include "finally-test.c"
diff --git a/finally-test.c b/finally-test.c
new file mode 100644 (file)
index 0000000..e95f16b
--- /dev/null
@@ -0,0 +1,163 @@
+/* -*-c-*-
+ *
+ * Test program for using `finally.h'
+ *
+ * (c) 2023 Mark Wooding
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of the `Finally' package.
+ *
+ * Finally is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Library General Public License as published
+ * by the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Finally is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with Finally.  If not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
+ * USA.
+ */
+
+/* Attention!
+ *
+ * This file is also compiled as C++, so we must be careful to keep it in the
+ * common subset of C and C++.  Anything language-specific needs to be split
+ * off into its own separate source file.
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
+#include <stdio.h>
+
+#include "finally.h"
+#include "finally-test.h"
+
+/*----- Main code ---------------------------------------------------------*/
+
+static int test_softball(void)
+{
+  FINALLY({ STEP(1); });
+
+  STEP(0);
+  return (2);
+}
+
+static int test_ordering(void)
+{
+  FINALLY({ STEP(2); });
+  FINALLY({ STEP(1); });
+
+  STEP(0);
+
+  return (3);
+}
+
+static int test_capture(void)
+{
+  int n = -1; FINALLY({ STEP(n); });
+
+  STEP(0);
+  n = 1; return (2);
+}
+
+static int test_internal_block(void)
+{
+  int i;
+  FINALLY({ STEP(10); });
+
+  for (i = 0; i < 5; i++) {
+    FINALLY({ STEP(2*i + 1); });
+    STEP(2*i);
+  }
+  return (11);
+}
+
+static int test_local_xfer(void)
+{
+  int i, j;
+
+  STEP(0);
+
+  { FINALLY({ STEP(2); }); STEP(1); }
+
+  do {
+    FINALLY({ STEP(4); });
+    STEP(3);
+    if (secretly_true) break;
+    MISSTEP;
+  } while (0);
+
+  for (i = 0; i < 5; i++) {
+    FINALLY({
+      if (i == 3) STEP(46);
+      else STEP(12*i + 16);
+    });
+    STEP(12*i + 5);
+    for (j = 0; j < 5; j++) {
+      FINALLY({ STEP(12*i + 2*j + 7); });
+      if (i == 3 && j == 1) { STEP(44); goto escape; }
+      else if (j != 3) STEP(12*i + 2*j + 6);
+      else { FINALLY({ STEP(12*i + 2*j + 6); }); continue; }
+    }
+  }
+escape:
+  STEP(47);
+
+  return (48);
+}
+
+#if defined(HAVE_FEXCEPTIONS)
+void try_catch_filling(unsigned f)
+{
+  int outstep = f&TCF_THROW ? 12 : 5; FINALLY({ STEP(outstep); });
+
+  if (f&TCF_THROW) STEP(10);
+  else STEP(2);
+
+  try_catch_inner(f);
+  STEP(4);
+}
+
+static int test_try_catch(void)
+{
+  STEP(0);
+  try_catch_outer(0);
+  STEP(8);
+  try_catch_outer(TCF_THROW);
+  return (15);
+}
+#endif
+
+int main(void)
+{
+  init_test();
+
+#define RUNTEST(name)                                                  \
+       do { begin_test(#name); STEP(test_##name()); end_test(); } while (0)
+#define SKIPTEST(name, excuse) skip_test(#name, excuse)
+
+  RUNTEST(softball);
+  RUNTEST(ordering);
+  RUNTEST(local_xfer);
+  RUNTEST(capture);
+  RUNTEST(internal_block);
+#if defined(HAVE_FEXCEPTIONS)
+  RUNTEST(try_catch);
+#else
+  SKIPTEST(try_catch, "no C++ compiler or no exception-handling support");
+#endif
+
+#undef RUNTEST
+#undef SKIPTEST
+
+  return (test_report());
+}
+
+/*----- That's all, folks -------------------------------------------------*/
diff --git a/finally-test.h b/finally-test.h
new file mode 100644 (file)
index 0000000..c2c4693
--- /dev/null
@@ -0,0 +1,70 @@
+/* -*-c-*-
+ *
+ * Definitions for testing
+ *
+ * (c) 2023 Mark Wooding
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of the `Finally' package.
+ *
+ * Finally is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Library General Public License as published
+ * by the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Finally is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with Finally.  If not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
+ * USA.
+ */
+
+#ifndef FINALLY_TEST_H
+#define FINALLY_TEST_H
+
+#ifdef __cplusplus
+  extern "C" {
+#endif
+
+/*----- Utility macros ----------------------------------------------------*/
+
+#define STR(x) STR1(x)
+#define STR1(x) #x
+#define WHERE __FILE__ ":" STR(__LINE__)
+
+/*----- Strange hacks -----------------------------------------------------*/
+
+extern const int secretly_true;
+
+/*----- Functions provided ------------------------------------------------*/
+
+#define STEP(s) check_step(s, WHERE)
+#define MISSTEP STEP(-1)
+
+extern void init_test(void);
+extern void begin_test(const char *name);
+extern void check_step(int s, const char *where);
+extern void end_test(void);
+extern void skip_test(const char *name, const char *excuse);
+extern int test_report(void);
+
+#if defined(HAVE_FEXCEPTIONS)
+# define TCF_THROW 1u
+  extern void try_catch_outer(unsigned f);
+  extern void try_catch_filling(unsigned f);
+  extern void try_catch_inner(unsigned f);
+#endif
+
+/*----- That's all, folks -------------------------------------------------*/
+
+#ifdef __cplusplus
+  }
+#endif
+
+#endif
diff --git a/finally.h b/finally.h
new file mode 100644 (file)
index 0000000..8488161
--- /dev/null
+++ b/finally.h
@@ -0,0 +1,186 @@
+/* -*-c-*-
+ *
+ * Arrange to have code executed when a function ends
+ *
+ * (c) 2023 Straylight/Edgeware
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of the `Finally' package.
+ *
+ * Finally is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Library General Public License as published
+ * by the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Finally is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with Finally.  If not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
+ * USA.
+ */
+
+#ifndef FINALLY_H
+#define FINALLY_H
+
+/*----- Compatibility machinery -------------------------------------------*/
+
+/* Some preliminary hacks for detecting compiler versions. */
+#ifdef __GNUC__
+#  define FINALLY__GCC_P(maj, min)                                     \
+       (__GNUC__ > (maj) || (__GNUC__ == (maj) && __GNUC_MINOR__ >= (min)))
+#else
+#  define FINALLY__GCC_P(maj, min) 0
+#endif
+
+#ifdef __clang__
+#  define FINALLY__CLANG_P(maj, min)                                   \
+       (__clang_major__ > (maj) || (__clang_major__ == (maj) &&        \
+                                    __clang_minor__ >= (min)))
+#else
+#  define FINALLY__CLANG_P(maj, min) 0
+#endif
+
+/* If configuration machinery hasn't determined a flavour, then we'll take a
+ * rough guess based on compiler versions.
+ */
+#if !defined(FINALLY_CONFIG_FLAVOUR) && \
+       !defined(__clang__) && FINALLY__GCC_P(3, 3)
+#  define FINALLY_CONFIG_FLAVOUR GCC_NESTED_FUNCTIONS
+#endif
+
+#if !defined(FINALLY_CONFIG_FLAVOUR)
+#  define FINALLY_CONFIG_FLAVOUR NIL
+#endif
+
+/*----- Macros provided ---------------------------------------------------*/
+
+/* Before we start, a note about compatibility.  We're using pretty esoteric
+ * compiler features here, and not all compilers support them.  I'm most
+ * interested in GCC, which will work fine.  This isn't going to work for
+ * other compilers, but lots of them try to impersonate GCC, and it's just
+ * not worth the effort to try to see through their lies.
+ *
+ * So the rules are: if you include this header file, you've either already
+ * made an effort to check that it's likely to work (e.g., by using the
+ * provided Autoconf macro), or you're willing to put up with whatever
+ * wreckage you end up with if the compiler doesn't actually like what we're
+ * saying here.
+ */
+
+/* And a utility for pasting tokens after macro expansion. */
+#define FINALLY__GLUE(x, y) FINALLY__DOGLUE(x, y)
+#define FINALLY__DOGLUE(x, y) x##y
+
+/* Now, we need a way to make a temporary name which isn't likely to conflict
+ * with anything else.
+ */
+#define FINALLY__TMP(name) FINALLY__GLUE(_finally__##name##__, __LINE__)
+
+/* And some other tricks which we may or may not need. */
+#if defined(__cplusplus) && __cplusplus >= 201703
+#  define FINALLY__IGNORABLE fucksocks
+#elif FINALLY__GCC_P(2, 5) || FINALLY__CLANG_P(3, 3)
+#  define FINALLY__IGNORABLE __attribute__((__unused__))
+#endif
+#ifndef FINALLY__IGNORABLE
+#  define FINALLY__IGNORABLE
+#endif
+
+/* Flavour selection machinery. */
+#define FINALLY__FLAVOUR_NIL -1
+#define FINALLY__FLAVOUR_GCC_NESTED_FUNCTIONS 1
+
+#define FINALLY__SELECTED_FLAVOUR                                      \
+       FINALLY__GLUE(FINALLY__FLAVOUR_, FINALLY_CONFIG_FLAVOUR)
+
+/* @FINALLY(code)@
+ * @FINALLY_TAGGED(tag, code)@
+ *
+ * This macro may be placed anywhere within a function where a declaration is
+ * acceptable -- so at the head of a block in C89, or anywhere between
+ * statements in C99 or later.  It causes @code@ to be executed when control
+ * leaves the enclosing scope.  If it's placed at top-level within a
+ * function, for example, then the @code@ is executed when the function
+ * returns.
+ *
+ * There may be multiple @FINALLY@ invocations within a scope; they are
+ * executed in reverse order when control leaves the scope.
+ *
+ * Due to technical limitations, it's forbidden to have two @FINALLY@
+ * invocations on the same source line.  This would seem to stymie writing a
+ * macro which expands to two invocations of @FINALLY@, which would be useful
+ * if those invocations were to end up in different scopes.  Such a macro can
+ * still be written using @FINALLY_TAGGED@ instead, which takes an additional
+ * @tag@ argument, which may be any identifier: the rule then becomes that a
+ * source line may not contain two invocations of @FINALLY_TAGGED@ bearing
+ * the same @tag@.  (It would be possible to overcome this limitation using
+ * the @__COUNTER__@ GCC extension, but I'd prefer not to limit the potential
+ * portability of this feature even further by insisting that something
+ * analogous exist in every supported compiler, and it would do users a
+ * disservice to create portability problems by having different rules on
+ * different compilers.)
+ */
+#if defined(__cplusplus) && __cplusplus >= 201103
+  /* Oooh, we're being compiled by a C++ compiler.  There's no need to deploy
+   * nonportable tricks, because we have an insane language that can already
+   * do what we need.
+   */
+
+   namespace finally {
+     template<typename F> class Finally {
+       F fn;
+     public:
+       Finally(F f) : fn{f} { ; }
+       ~Finally() { fn(); }
+     };
+     template<typename F> Finally<F> finally(F &&fn)
+       { return Finally<F>(fn); }
+   }
+
+#  define FINALLY_TAGGED(tag, body)                                    \
+       FINALLY__IGNORABLE auto FINALLY__TMP(tag##__var) =              \
+         finally::finally([&]{ body })
+
+#elif FINALLY__GLUE(FINALLY__FLAVOUR_, FINALLY_CONFIG_FLAVOUR) == \
+       FINALLY__FLAVOUR_GCC_NESTED_FUNCTIONS
+   /* We're being compiled by GCC, or by something which wants to be mistaken
+    * for GCC at any rate.  And GCC has nested functions.  So, for each
+    * block, we'll define a dummy variable that we don't care about, and
+    * attach a nested function as its cleanup handler which will execute our
+    * cleanup code.
+    */
+
+#  define FINALLY_TAGGED(tag, body)                                    \
+       __extension__ __inline__                                        \
+         void FINALLY__TMP(tag##__fn)                                  \
+           (const int __attribute__((__unused__)) *_finally__hunoz)    \
+           { body }                                                    \
+       __attribute__((__unused__, __cleanup__(FINALLY__TMP(tag##__fn)))) \
+         int FINALLY__TMP(tag##__var)
+
+#elif FINALLY__GLUE(FINALLY__FLAVOUR_, FINALLY_CONFIG_FLAVOUR) == \
+       FINALLY__GLUE(FINALLY__FLAVOUR_, NIL)
+   /* We don't have a flavour to support this environment. */
+
+#  error "Compiler not supported.  This isn't going to work."
+#else
+   /* This case analysis should be exhaustive. */
+
+#  error "Internal error: `FINALLY_CONFIG_FLAVOUR' bungled."
+#endif
+
+/* We now have `FINALLY_TAGGED'; defining `FINALLY' is easy.  The TAG here is
+ * guaranteed not conflict with any call on `FINALLY_TAGGED', since TAGs are
+ * required to be identifiers.
+ */
+#define FINALLY(code) FINALLY_TAGGED(0, code)
+
+/*----- That's all, folks -------------------------------------------------*/
+
+#endif
diff --git a/m4/finally.m4 b/m4/finally.m4
new file mode 100644 (file)
index 0000000..70c2d83
--- /dev/null
@@ -0,0 +1,135 @@
+dnl -*-autoconf-*-
+
+### SYNOPSIS
+###
+###   FINALLY([IF-SUCCEEDED], [IF-FAILED])
+###
+### DESCRIPTION
+###
+###   Probe at the C compiler to determine how, if at all, to implement the
+###   `FINALLY' macro, which arranges to run some code when control leaves a
+###   given scope.  This isn't at all a standard C feature, so we need to use
+###   compiler-specific hacks, and this is the main machinery for deciding
+###   which hacks to deploy.
+###
+###   On exit, the shell variable `finally_flavour' is set to an uppercase
+###   word naming the chosen implementation strategy: it will be `NIL' if the
+###   macro failed and no strategy could be found.  The preprocessor define
+###   `FINALLY_CONFIG_FLAVOUR' is set to `FINALLY_CONFIG_FLAVOUR_...'
+###   followed by the same word: this is the main input to the selection
+###   machinery in `finally.h'.
+###
+###   The substitution variables `FINALLY_CFLAGS' and `FINALLY_LIBS' are set
+###   to any additional compiler flags or libraries needed to support the
+###   `FINALLY' macro.  They can be set per-target in the `Makefile', or
+###   stuffed into the global variables by the `configure' script.
+###
+###    If the macro managed to find a workable strategy, then the shell
+###    fragment IF-SUCCEEDED is run; otherwise, (if `finally_flavour' is
+###    `NIL'), the shell fragment IF-FAILED is run.
+###
+### LICENSE
+###
+###   Copyright (c) 2023 Mark Wooding <mdw@distorted.org.uk>
+###
+###   This program is free software: you can redistribute it and/or modify it
+###   under the terms of the GNU General Public License as published by the
+###   Free Software Foundation, either version 2 of the License, or (at your
+###   option) any later version.
+###
+###   This program is distributed in the hope that it will be useful, but
+###   WITHOUT ANY WARRANTY; without even the implied warranty of
+###   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+###   General Public License for more details.
+###
+###   You should have received a copy of the GNU General Public License along
+###   with this program. If not, see <http://www.gnu.org/licenses/>.
+###
+###   In particular, no exception to the GPL is granted regarding generated
+###   `configure' scripts which are the output of Autoconf.
+
+AC_DEFUN([FINALLY_GCC_NESTED_FUNCTIONS_TEST_PROGRAM], [AC_LANG_PROGRAM([], [
+  __extension__ __inline__ void nested(void) { ; }
+  nested();
+])])
+AC_DEFUN([FINALLY_GCC_ATTRIBUTE_CLEANUP_TEST_PROGRAM], [AC_LANG_PROGRAM([
+  extern void cleanup_fn(const int *x);
+  extern void bamboozle(int *x_inout);
+], [
+  __attribute__((cleanup(cleanup_fn))) int x = 0;
+  bamboozle(&x);
+])])
+
+dnl Decide whether we can define a plausible `FINALLY' macro.
+AC_DEFUN([FINALLY_CHECK],
+[finally_flavour=undecided finally_result="not supported"
+
+dnl We're going to want to test C code.
+AC_LANG_PUSH([C])
+
+case $finally_flavour,$GCC in
+  undecided,yes)
+    dnl Our GCC-ish strategies have a common factor: they depend on
+    dnl `__attribute__((cleanup(...)))' working.  So let's check for that.
+
+    AC_CACHE_CHECK([whether the alleged GNU C compiler supports \`__attribute__((cleanup(...)))'],
+                  [finally_cv_gcc_attribute_cleanup_p], [
+      AC_COMPILE_IFELSE([FINALLY_GCC_ATTRIBUTE_CLEANUP_TEST_PROGRAM],
+                       [finally_cv_gcc_attribute_cleanup_p=yes],
+                       [finally_cv_gcc_attribute_cleanup_p=no])])
+    case $finally_cv_gcc_attribute_cleanup_p in
+      no) finally_flavour=NIL ;;
+    esac
+    ;;
+esac
+
+case $finally_flavour,$GCC in
+  undecided,yes)
+    dnl Autoconf has decided that the compiler smells a bit like GCC, and it
+    dnl certainly seems to support a GCC extension.  But many compilers
+    dnl impersonate GCC, in more or less convincing ways.  Our GCC-flavoured
+    dnl `FINALLY' code depends on nested functions, which GCC has supported
+    dnl pretty much forever, but other compilers don't even though they lie
+    dnl about being compatible.
+
+    AC_CACHE_CHECK([whether the alleged GNU C compiler supports nested functions],
+                  [finally_cv_gcc_nested_functions_p], [
+      AC_COMPILE_IFELSE([FINALLY_GCC_NESTED_FUNCTIONS_TEST_PROGRAM],
+                       [finally_cv_gcc_nested_functions_p=yes],
+                       [finally_cv_gcc_nested_functions_p=no])])
+    case $finally_cv_gcc_nested_functions_p in
+      yes)
+       finally_flavour=GCC_NESTED_FUNCTIONS
+       finally_result="GCC nested functions"
+       ;;
+    esac
+    ;;
+esac
+
+case $finally_flavour in
+  undecided)
+    dnl We've got this far and we've drawn a blank.  Give up.
+    finally_flavour=NIL
+    ;;
+esac
+
+AC_LANG_POP([C])
+
+dnl Pass the results on to the implementation machinery.
+AC_MSG_CHECKING([how to implement deferred cleanup code])
+AC_DEFINE_UNQUOTED([FINALLY_CONFIG_FLAVOUR],
+                  [$finally_flavour],
+  [Select one of the implementation strategies for the `FINALLY' macro.])
+AC_SUBST(FINALLY_CFLAGS) AC_SUBST(FINALLY_LIBS)
+AC_MSG_RESULT([$finally_result])
+
+dnl Invoke the caller's shell fragments according to our findings.
+case $finally_flavour in
+  nil)
+    $2
+    ;;
+  *)
+    $1
+    ;;
+esac
+])
diff --git a/test-guts.c b/test-guts.c
new file mode 100644 (file)
index 0000000..b2926e9
--- /dev/null
@@ -0,0 +1,135 @@
+/* -*-c-*-
+ *
+ * Main test machinery
+ *
+ * (c) 2023 Mark Wooding
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of the `Finally' package.
+ *
+ * Finally is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Library General Public License as published
+ * by the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Finally is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with Finally.  If not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
+ * USA.
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "finally-test.h"
+
+/*----- Global variables --------------------------------------------------*/
+
+const int secretly_true = 1;
+
+/*----- Static variables --------------------------------------------------*/
+
+static const char *curr;
+static unsigned nrun = 0, nskip = 0, nlose = 0;
+static unsigned flags = 0;
+enum { HUMAN, TAP }; static unsigned outform = HUMAN;
+#define f_failthis 1u
+
+static int step;
+
+/*----- Main code ---------------------------------------------------------*/
+
+void init_test(void)
+{
+  const char *p;
+
+  p = getenv("TEST_OUTFORM");
+  if (!p || strcmp(p, "human") == 0) outform = HUMAN;
+  else if (strcmp(p, "tap") == 0) outform = TAP;
+  else { fprintf(stderr, "unknown output format `%s'\n", p); exit(2); }
+}
+
+void begin_test(const char *name)
+  { step = 0; flags &= ~f_failthis; curr = name; }
+
+void check_step(int s, const char *where)
+{
+  FILE *fp;
+
+  if (step != s) {
+    switch (outform) {
+      case HUMAN: fp = stderr; break;
+      case TAP: fp = stdout; fputs("# ", stdout); break;
+      default: abort();
+    }
+    fprintf(fp, "%s (%s): misstep: expected %d but found %d\n",
+           where, curr, step, s);
+    flags |= f_failthis;
+  }
+  step++;
+}
+
+void end_test(void)
+{
+  nrun++; if (flags&f_failthis) nlose++;
+
+  switch (outform) {
+    case HUMAN:
+      printf("%s: %s\n", curr, flags&f_failthis ? "FAILED" : "ok");
+      break;
+    case TAP:
+      printf("%s %u %s\n", flags&f_failthis ? "not ok" : "ok", nrun, curr);
+      break;
+    default:
+      abort();
+  }
+}
+
+void skip_test(const char *name, const char *excuse)
+{
+  nrun++; nskip++;
+
+  switch (outform) {
+    case HUMAN: printf("%s: (skipped: %s)\n", name, excuse); break;
+    case TAP: printf("ok %u %s # skip (%s)\n", nrun, name, excuse); break;
+    default: abort();
+  }
+}
+
+int test_report(void)
+{
+#define TESTS(n) ((n) == 1 ? "test" : "tests")
+
+  switch (outform) {
+    case HUMAN:
+      if (nlose)
+       printf("FAILED %u out of %u %s", nlose, nrun, TESTS(nrun));
+      else if (nskip)
+       printf("passed %u out of %u %s",
+              nrun - nlose - nskip, nrun, TESTS(nrun));
+      else
+       printf("passed all %u %s", nrun, TESTS(nrun));
+      if (nskip) printf(" (skipped %u %s)", nskip, TESTS(nskip));
+      putchar('\n');
+      return (nlose ? 2 : 0);
+    case TAP:
+      printf("1..%u\n", nrun);
+      return (0);
+    default:
+      abort();
+  }
+
+#undef TESTS
+}
+
+/*----- That's all, folks -------------------------------------------------*/
diff --git a/try-catch.cc b/try-catch.cc
new file mode 100644 (file)
index 0000000..488db12
--- /dev/null
@@ -0,0 +1,53 @@
+/* -*-c++-*-
+ *
+ * Check handling of exceptions
+ *
+ * (c) 2023 Straylight/Edgeware
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of the `Finally' package.
+ *
+ * Finally is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Library General Public License as published
+ * by the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Finally is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with Finally.  If not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
+ * USA.
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
+#include "finally-test.h"
+
+/*----- Main code ---------------------------------------------------------*/
+
+class ball { };
+
+void try_catch_inner(unsigned f)
+{
+  if (f&TCF_THROW) STEP(11);
+  else STEP(3);
+  if (f&TCF_THROW) throw ball();
+}
+
+void try_catch_outer(unsigned f)
+{
+  if (f&TCF_THROW) STEP(9);
+  else STEP(1);
+  try { try_catch_filling(f); STEP(6); }
+  catch (ball) { STEP(13); }
+  if (f&TCF_THROW) STEP(14);
+  else STEP(7);
+}
+
+/*----- That's all, folks -------------------------------------------------*/