DEV Community

Paul J. Lucas
Paul J. Lucas

Posted on

Probing the Compiler in Autotools

Introduction

The previous article in this series on Autotools showed how you can use Autoconf macros to probe the platform to see if it supports certain library functions, e.g., fnmatch or getline. If not, Autotools will compile its own versions downloaded from the Gnulib portability library.

As C has evolved over the years with newer standards, e.g., C11 and C23, both new language features have been added to the language and new APIs have been added to the standard library. For every new standard, the value of the __STDC_VERSION__ preprocessor macro is updated, e.g., 201112L for C11 and 202311L for C23. (C++ uses __cplusplus for this.) Hence, one way to know whether you can use a certain language feature or library function is simply to check __STDC_VERSION__. For example, to check whether “no return” can be [[noreturn]] or _Noreturn:

#if __STC_VERSION__ >= 202311L
#  define _Noreturn  [[noreturn]]
#else
#  /* Assume _Noreturn is fine as-is. */
#endif
Enter fullscreen mode Exit fullscreen mode

While this often works, it has a few problems:

  1. Using __STDC_VERSION__ is a blunt instrument. In many cases, compiler vendors support language features as extensions long before the features are standardized. For example, gcc supported typeof long before it made it into C23.
  2. In some cases, the semantics of extensions may not match those in an eventual standard. Ideally, you want to probe the compiler in advance to see whether it supports a language feature in exactly the way you need.
  3. Using #if __STDC_VERSION__ works only in C source code; it doesn’t work in makefiles, so you can’t conditionally compile .c files based on the compiler’s standard version.

Much better than relying on __STDC_VERSION__ is to probe the compiler directly to get definitive answers as to whether it supports the features you need. Continuing in this series, this article is about using Autotools to probe the compiler and platform directly.

Probing the Compiler

Autotools has a built-in macro AC_COMPILE_IFELSE that can be used to probe the compiler. For example, to check whether typeof is supported:

AC_COMPILE_IFELSE(
  [AC_LANG_PROGRAM([], [typeof(0) x;])]
  [pjl_cv_typeof=yes],
  [pjl_cv_typeof=no]
)
if test "x$pjl_cv_$1" = xyes; then
  AC_DEFINE(HAVE_TYPEOF, [1], [Define to 1 if you have 'typeof'.])
else
  AC_DEFINE(HAVE_TYPEOF, [0], [Define to 1 if you have 'typeof'.])
fi
Enter fullscreen mode Exit fullscreen mode

AC_LANG_PROGRAM is used to specify a program fragment (where you don’t need to explicitly declare main yourself) to see if it compiles. The idea is to put the smallest possible fragment, often a single statement, that definitively determines whether a language feature is supported. For typeof, the declaration:

typeof(0) x;  // If supported, same as: int x;
Enter fullscreen mode Exit fullscreen mode

will compile only if typeof is supported.

Reminder: in C, the type of an unadorned integer literal is int.

If the snippet compiles, then the variable pjl_cv_typeof is set to yes, otherwise no. Then, we can use a standard shell if statement to define HAVE_TYPEOF to either 0 or 1 that will be added to config.h.

Names of variables like pjl_cv_typeof conventionally contain cv for “configure value.”

Writing m4 Macros

While the above works, it’s fairly verbose, especially if you want to probe the compiler for several language features. Better would be to write your own Autoconf m4 macro:

AC_DEFUN([TRY_COMPILE], [
  AC_CACHE_CHECK([for $1], [pjl_cv_$1],
    [AC_COMPILE_IFELSE(
      [AC_LANG_PROGRAM([$2], [$3])],
      [pjl_cv_$1=yes],
      [pjl_cv_$1=no]
    )]
  )
  if test "$pjl_cv_$1" = "yes"; then
    AC_DEFINE(HAVE_[]m4_toupper($1), [1], [Define to 1 if you have '$1'.])
  else
    AC_DEFINE(HAVE_[]m4_toupper($1), [0], [Define to 1 if you have '$1'.])
  fi
])
Enter fullscreen mode Exit fullscreen mode

This macro takes three arguments:

  1. Its label.
  2. Preamble, if any, such as necessary #includes.
  3. Code snippet.

Arguments are referred to via $x where x is its number.

AC_DEFUN is used to define a new Autoconf macro taking its name and body as arguments.

AC_CACHE_CHECK does two things:

  1. When configure is running, prints a line of the form checking whatever ... followed by either yes or no where whatever is the first argument to AC_CACHE_CHECK. In this case, it would print checking for typeof... which is nice since it lets the user know what’s going on and the result of the probe.
  2. Caches the result in the variable pjl_cv_$1 where $1 is first expanded.

M4 also has its own set of built-in macros, such as m4_toupper that converts its argument to uppercase, hence:

HAVE_[]m4_toupper($1)
Enter fullscreen mode Exit fullscreen mode

will define HAVE_TYPEOF when typeof is $1. (The [] is needed to separate HAVE_ from m4_toupper otherwise it would have attempted to define HAVE_m4_toupper which isn’t what you want.)

If you put this macro into m4/try_compile.m4, you can use it in configure.ac:

TRY_COMPILE([typeof], [], [typeof(0) x;])
Enter fullscreen mode Exit fullscreen mode

That’s a lot less verbose.

For another example, if you want to use C23’s nullptr in your program if the compiler supports it, you can put the following into configure.ac:

TRY_COMPILE([nullptr], [], [void *p = nullptr;])
if test "x$pjl_cv_nullptr" = xno; then
  AC_DEFINE([nullptr], [NULL], [Define to NULL.])
fi
Enter fullscreen mode Exit fullscreen mode

If the compiler doesn’t support it, configure will put the following into config.h so it’ll fall back to using NULL:

#define nullptr NULL
Enter fullscreen mode Exit fullscreen mode

Of course nullptr and NULL aren’t 100% equivalent, but NULL can be substituted for nullptr in the majority of cases.

If you want to require a particular language feature, you can also write a MUST_COMPILE variation:

AC_DEFUN([MUST_COMPILE], [
  AC_CACHE_CHECK([for $1], [pjl_cv_$1],
    [AC_COMPILE_IFELSE(
      [AC_LANG_PROGRAM([$2], [$3])],
      [pjl_cv_$1=yes],
      [pjl_cv_$1=no]
    )]
  )
  if test "x$pjl_cv_$1" = xno; then
    AC_MSG_ERROR(["$1" not supported by C compiler])
  fi
])
Enter fullscreen mode Exit fullscreen mode

where AC_MSG_ERROR prints an error message and terminates configure.

Preprocessor Macros in Makefiles

As mentioned, preprocessor macros can’t be used in makefiles. For example, if you wanted to compile my_threads.c only if __STDC_NO_THREADS__ is defined by the compiler, you could put the following into configure.ac:

TRY_COMPILE([__STDC_NO_THREADS__], [], [int x = __STDC_NO_THREADS__;])

AM_CONDITIONAL([HAVE___STDC_NO_THREADS__],
               [test "x$pjl_cv___STDC_NO_THREADS__" = xyes])
Enter fullscreen mode Exit fullscreen mode

Then in src/Makefile.am you can do:

my_prog_SOURCES =  my_prog.c

if HAVE___STDC_NO_THREADS__
my_prog_SOURCES += my_threads.c
endif
Enter fullscreen mode Exit fullscreen mode

Conclusion

Although __STDC_VERSION__ exists as a mechanism for checking what version of C the compiler supports, it’s a blunt instrument. Far better is to probe the compiler directly for features that you want.

There are more parts to come!

Top comments (0)