If all you have is a hammer, everything looks like a nail. If your hammer is C++, everything looks like your thumb.
I don't know who said it first, but this humorous perversion of the law of the instrument has, in several variations, been floating around the internet for ages. While it is probably more true of C++, the same can certainly be said of C.
Both C and C++ give the programmer a huge amount of control
over what the computer actually does. While this does make it
relatively easy to write efficient code, it also makes it
relatively easy to write code which crashes. Higher level
lanuages can often provide descriptive error messages at
run-time, but with C/C++ there is often a single run-time
error message: Segmentation fault (core dumped)
, and
that's if you get lucky enough to actually see an error
message! Often, the program can keep running but generate data
which is complete garbage or, even worse, data which is just
slightly incorrect (which is much harder to spot).
To make up for the lack of run-time errors, compilers and static analyzers instead try to generate compile-time errors. With a few simple annotations, you can give compilers and static analyzers more information about how your program is supposed to work, which gives them have a better chance of informing you when you, or someone using your API, make a mistake.
Of course, it's not all about the errors… giving the compiler
more information about your program also means it can do a
better job of optimizing for you. Unfortunately, different
compilers can handle different information, often presented in
different ways, which can lead to an #ifdef
maze
or, more often, simply omitting the extra information.
Hedley helps by providing easy-to-use macros which hide
that #ifdef
maze away in a single C/C++ header
which works everywhere; just drop it into your project
and #include
it—no build system magic is
required.
For example, here is what a function to print a fatal error might look like using Hedley:
HEDLEY_NO_RETURN HEDLEY_NON_NULL(1,2) HEDLEY_PRINTF_FORMAT(2,3) void print_fatal_error(FILE* fp, const char* fmt, ...);
Without Hedley, the same declaration would look like this:
#if defined(__IAR_SYSTEMS_ICC__) && (__VER__ >= 8000000) __noreturn #elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L _Noreturn #elif defined(__cplusplus) && (__cplusplus >= 201103L) [[noreturn]] #elif defined(__has_attribute) # if __has_attribute(noreturn) __attribute__((__noreturn__)) # endif #elif \ (defined(__GNUC__) && ((__GNUC__ >= 4) || ((__GNUC__ == 3) && (__GNUC_MINOR__ >= 2)))) || \ (defined(__INTEL_COMPILER) && (__INTEL_COMPILER >= 1600)) || \ (defined(__SUNPRO_C) && (__SUNPRO_C >= 0x5110)) || \ (defined(__SUNPRO_CC) && (__SUNPRO_CC >= 0x5110)) || \ ((defined(__CC_ARM) && defined(__ARMCC_VERSION)) && (__ARMCC_VERSION >= 4010000)) || \ (defined(__xlC__) && (__xlC__ >= 0x0a01)) || \ (defined(__TI_COMPILER_VERSION__) && (__TI_COMPILER_VERSION__ >= 8000000)) || \ (defined(__TI_COMPILER_VERSION__) && (__TI_COMPILER_VERSION__ >= 7003000) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) __attribute__((__noreturn__)) #elif defined(_MSC_VER) && (_MSC_VER >= 1310) __declspec(noreturn) #elif defined(__TI_COMPILER_VERSION__) && (__TI_COMPILER_VERSION__ >= 6000000) && defined(__cplusplus) # pragma FUNC_NEVER_RETURNS; #endif #if defined(__has_attribute) # if __has_attribute(nonnull) __attribute__((__nonnull__(1,2))) # endif #elif \ (defined(__GNUC__) && ((__GNUC__ >= 4) || ((__GNUC__ == 3) && (__GNUC_MINOR__ >= 3)))) || \ (defined(__INTEL_COMPILER) && (__INTEL_COMPILER >= 1600)) || \ ((defined(__CC_ARM) && defined(__ARMCC_VERSION)) && (__ARMCC_VERSION >= 4010000)) || \ __attribute__((__nonnull__(1,2))) #endif #if defined(__has_attribute) # if __has_attribute(format) # if defined(__MINGW32__) # if !defined(__USE_MINGW_ANSI_STDIO) __attribute__((__format__(ms_printf, 2, 3))) # else __attribute__((__format__(gnu_printf, 2, 3))) # endif # else __attribute__((__format__(__printf__, 2, 3))) # endif # endif #elif \ (defined(__GNUC__) && ((__GNUC__ >= 4) || ((__GNUC__ == 3) && (__GNUC_MINOR__ >= 1)))) || \ (defined(__INTEL_COMPILER) && (__INTEL_COMPILER >= 1600)) || \ ((defined(__CC_ARM) && defined(__ARMCC_VERSION)) && (__ARMCC_VERSION >= 5060000)) || \ (defined(__xlC__) && (__xlC__ >= 0x0a01)) || \ (defined(__TI_COMPILER_VERSION__) && (__TI_COMPILER_VERSION__ >= 8000000)) || \ (defined(__TI_COMPILER_VERSION__) && (__TI_COMPILER_VERSION__ >= 7003000) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) __attribute__((__format__(__printf__, 2, 3))) #endif void print_fatal_error(FILE* fp, const char* fmt, ...);
Not only is the Hedley version vastly easier to write, but it's easy enough to read that it helps document your code, meaning it's useful even if your compiler doesn't support the annotations in question.
Of course, you're unlikely to encounter that monstrosity above in the real world. You're much more likely to see something like:
#if defined(__GNUC__) __attribute__(noreturn) __attribute__((__format__(printf, 2, 3))) __attribute__((__nonnull__(1,2))) #elif defined(_MSC_VER) __declspec(noreturn) #endif void print_fatal_error(FILE* fp, const char* fmt, ...);
While easier on the eyes than the first version (though not the Hedley version), there are a number of problems with this:
With Hedley, other people have already put in a lot of effort to make sure the compiler-specific annotations are written wherever they are accepted, but are not written when they would cause an error.
Hedley is a single header file; to use it, just include it:
#include "hedley.h"
And you're ready to go!
You can copy the header to your project if you want (it's CC0-licensed), or use a git submodule. You don't need to change the prefix ("HEDLEY_") or name ("hedley.h"), but if you want to feel free. I would appreciate it if you keep the link back to the original project, though, to help people keep any improvements synced (in both directions).
HEDLEY_NON_NULL
First, let's look at a pretty straightforward function:
#include "hedley.h" #include <stdio.h> struct Context { char* name; }; void print_message(struct Context* ctx, const char* message) { printf("%s: %s\n", ctx->name, message); }
Here we just print out a message, but this is a very common pattern for lots of things: we take a pointer to a context, plus some additional argument(s), and do something with the fields of the context and the arguments. You see this everywhere.
Now, what happens if we call it with NULL for the context?
$ gcc -g -Wall -Werror test.c $ ./a.out Segmentation fault (core dumped)
Now, let's add
a HEDLEY_NON_NULL
attribute. HEDLEY_NON_NULL
lists parameters which
must never be NULL. It accepts a variadic list of
parameters identified by their position (so in our
example ctx is 1 and message is 2).
While many C libraries can handle a NULL value for
a string coversion specifier (%s), the standard doesn't
mandate it, so we should make sure both parameters
are non-NULL:
#include "hedley.h" #include <stdio.h> struct Context { char* name; }; HEDLEY_NON_NULL(1,2) void print_message(struct Context* ctx, const char* message) { printf("%s: %s\n", ctx->name, message); }
Let's see what happens when we try to compile and run our call now:
$ gcc -g -Wall -Werror test.c test.c: In function ‘main’: test.c:16:3: error: null argument where non-null required (argument 1) [-Werror=nonnull] print_message(NULL, "Hello, world!"); ^~~~~~~~~~~~~ cc1: all warnings being treated as errors
Most compilers aren't too smart about this; you can “trick” them by assigning NULL to a variable then calling the function, but it's definitely better than nothing. Static analyzers, on the other hand, can find extremely convoluted cases where you may be passing NULL.
Of course, HEDLEY_NON_NULL
is far from the only
macro Hedley supports. See
the API Reference for the
full list and lots of examples. There are macros for:
Describe your API to the compiler and/or static analyzers, giving your users helpful warnings when they misuse your API, without false positives. Includes macros for:
Tell the compiler what you're doing (or what you want it to do) so compiled code is faster.
Macros which help convey versioning information, when symbols are are added and deprecated, and symbol visibility.
Macros Hedley uses to detect compiler, compiler version, and features availability.
Assorted macros which are useful, but don't really fit anywhere else.
Hedley includes some useful helpers for public APIs, but they aren't quite as simple to use as the rest of the macros. There are two groups of macros in this category: symbol visibility and versioning.
With standard C, you can basically choose between “static” functions, which are visible only within the current compilation unit (i.e. the current file). This means that if you want to use a symbol in another compilation unit within your project, you have to expose it publicly. Even if you don't include a prototype in the public header, this still has implications for performance and code size. Furthermore, leaking internal sybmols which are not properly namespaced can result in collisions. For a good overview, see GCC's “Visibility” wiki page.
Hedley's visibility support is pretty easy to use, but it does require you to put some code in your header:
#if defined(FOO_COMPILATION) #define FOO_API HEDLEY_PUBLIC #else #define FOO_API HEDLEY_IMPORT #endif
Obviously, you should replace FOO_
with the
appropriate prefix for your library.
Once you've included this code in one of your headers, simply add -DFOO_COMPILATION (or whatever your compiler wants) to the C flags you use to build your library, but not in the C flags people use to build with your library.
Then, simply annotate your public prototypes
with FOO_API
:
FOO_API void foo_bar(int baz);
If you compile with -fvisibility=hidden, only
symbols annotated with FOO_API
will be visible.
If you'd like to avoid adding the visibility flag but still
want the same effect, just annotate your internal (but
non-static) functions with
HEDLEY_PRIVATE
; for static functions there is
no need to add anything.
If you're using CMake ≥ 2.8.12, you can use the C_VISIBILITY_PRESET to set the proper visibility flag (if your compiler supports it).
Hedley's versioning macros are a bit more complicated to use, but the effect can be pretty amazing. IMHO they are well worth the effort.
First off, credit where credit is due: the idea for these macros came from the GLib, where something very similar was implemented by Emmanuale Bassi.
First, you'll want to define some macros to identify the
current version of the library. Usually this will be in a
file which is automatically generated by the build system
(see configure_file
for CMake,
or AC_CONFIG_FILES
for autoconf), but you can maintain the file manually if you
feel more comfortable with that. Here is an example for a
library called “Foo”, version 1.2.3:
#include "hedley/hedley.h" /* Define these to whatever your version number is. */ #define FOO_VERSION_MAJOR 1 #define FOO_VERSION_MINOR 2 #define FOO_VERSION_REVISION 3 #define FOO_VERSION HEDLEY_ENCODE_VERSION(FOO_VERSION_MAJOR, FOO_VERSION_MINOR, FOO_VERSION_REVISION)
Pretty standard stuff. Now, things start to get interesting; we want to let consumers target a specific version of your library, warning them if they use a symbol which is only available in a newer version, or is deprecated in the version they chose. We'll make the default target the current version:
#if !defined(FOO_TARGET_VERSION) #define FOO_TARGET_VERSION FOO_VERSION #endif
Now, we'll define some macros for each version of your API:
#define FOO_VERSION_1_0 HEDLEY_ENCODE_VERSION(1,0,0) #define FOO_VERSION_1_1 HEDLEY_ENCODE_VERSION(1,1,0) #define FOO_VERSION_1_2 HEDLEY_ENCODE_VERSION(1,2,0) #if FOO_TARGET_VERSION < HEDLEY_ENCODE_VERSION(1,1,0) #define FOO_AVAILABLE_SINCE_1_1 HEDLEY_UNAVAILABLE(1.1) #define FOO_DEPRECATED_SINCE_1_1 #define FOO_DEPRECATED_SINCE_1_1_FOR(replacement) #else #define FOO_AVAILABLE_SINCE_1_1 #define FOO_DEPRECATED_SINCE_1_1 HEDLEY_DEPRECATED(1.1) #define FOO_DEPRECATED_SINCE_1_1_FOR(replacement) HEDLEY_DEPRECATED_FOR(1.1, replacement) #endif #if FOO_TARGET_VERSION < HEDLEY_ENCODE_VERSION(1,2,0) #define FOO_AVAILABLE_SINCE_1_2 HEDLEY_UNAVAILABLE(1.2) #define FOO_DEPRECATED_SINCE_1_2 #define FOO_DEPRECATED_SINCE_1_2_FOR(replacement) #else #define FOO_AVAILABLE_SINCE_1_2 #define FOO_DEPRECATED_SINCE_1_2 HEDLEY_DEPRECATED(1.2) #define FOO_DEPRECATED_SINCE_1_2_FOR(replacement) HEDLEY_DEPRECATED_FOR(1.2, replacement) #endif
Technically, the first three defines are unnecessary, but they can be convenient to allow people to define the target version.
Once we have these macros defined, we can annotate our API:
void foo_bar(void); FOO_DEPRECATED_SINCE_1_1_FOR(foo_qux) void foo_baz(void); FOO_AVAILABLE_SINCE_1_2 void foo_qux(void);
Now, let's see what happens when we try to call each function, while targeting different versions of the API:
$ cc -DFOO_TARGET_VERSION=FOO_VERSION_1_0 test.c test.c: In function ‘main’: test.c:7:3: warning: call to ‘foo_qux’ declared with attribute warning: Not available until 1.2 foo_qux(); ^~~~~~~~~~~~~~~~~~~~~~~~~
$ cc -DFOO_TARGET_VERSION=FOO_VERSION_1_1 test.c test.c: In function ‘main’: test.c:6:3: warning: ‘foo_baz’ is deprecated: Since 1.1; use foo_qux [-Wdeprecated-declarations] foo_baz(); ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In file included from test.c:1:0: test.h:45:1: note: declared here foo_baz(void) { ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test.c:7:3: warning: call to ‘foo_qux’ declared with attribute warning: Not available until 1.2 foo_qux(); ^~~~~~~~~~~~~~~~~~~~~~~~~
$ cc -DFOO_TARGET_VERSION=FOO_VERSION_1_2 test.c test.c: In function ‘main’: test.c:6:3: warning: ‘foo_baz’ is deprecated: Since 1.1; use foo_qux [-Wdeprecated-declarations] foo_baz(); ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In file included from test.c:1:0: test.h:45:1: note: declared here foo_baz(void) { ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$ cc test.c test.c: In function ‘main’: test.c:6:3: warning: ‘foo_baz’ is deprecated: Since 1.1; use foo_qux [-Wdeprecated-declarations] foo_baz(); ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In file included from test.c:1:0: test.h:45:1: note: declared here foo_baz(void) { ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This allows people depending on your API to avoid unintentionally bumping the version of your library they depend on when developing on a system with a newer version.
If you need to call the deprecated functions in your code (e.g., because one deprecated function is implemented by calling another, you can reuse the FOO_COMPILATION macro we defined earlier:
#define FOO_VERSION_1_0 HEDLEY_ENCODE_VERSION(1,0,0) #define FOO_VERSION_1_1 HEDLEY_ENCODE_VERSION(1,1,0) #define FOO_VERSION_1_2 HEDLEY_ENCODE_VERSION(1,2,0) #if defined(FOO_COMPILATION) #define FOO_AVAILABLE_SINCE_1_1 #define FOO_DEPRECATED_SINCE_1_1 #define FOO_DEPRECATED_SINCE_1_1_FOR(replacement) #else #if FOO_TARGET_VERSION < HEDLEY_ENCODE_VERSION(1,1,0) #define FOO_AVAILABLE_SINCE_1_1 HEDLEY_UNAVAILABLE(1.1) #define FOO_DEPRECATED_SINCE_1_1 #define FOO_DEPRECATED_SINCE_1_1_FOR(replacement) #else #define FOO_AVAILABLE_SINCE_1_1 #define FOO_DEPRECATED_SINCE_1_1 HEDLEY_DEPRECATED(1.1) #define FOO_DEPRECATED_SINCE_1_1_FOR(replacement) HEDLEY_DEPRECATED_FOR(1.1, replacement) #endif #endif
If you would like to take it a step further, you can create
macros for the minimum required and max allowed versions.
This allows people to target a range of different versions of
your library (using #ifdef
s to switch between
different functionality based on the value
of FOO_VERSION). For example:
#define FOO_VERSION_1_0 HEDLEY_ENCODE_VERSION(1,0,0) #define FOO_VERSION_1_1 HEDLEY_ENCODE_VERSION(1,1,0) #define FOO_VERSION_1_2 HEDLEY_ENCODE_VERSION(1,2,0) #if !defined(FOO_TARGET_VERSION) #define FOO_TARGET_VERSION FOO_VERSION #endif #if !defined(FOO_VERSION_MIN_REQUIRED) #define FOO_VERSION_MIN_REQUIRED FOO_TARGET_VERSION #endif #if !defined(FOO_VERSION_MAX_ALLOWED) #define FOO_VERSION_MAX_ALLOWED FOO_TARGET_VERSION #endif #if FOO_VERSION_MIN_REQUIRED > FOO_VERSION_1_1 #define FOO_DEPRECATED_SINCE_1_1 HEDLEY_DEPRECATED(1.1) #define FOO_DEPRECATED_SINCE_1_1_FOR(replacement) HEDLEY_DEPRECATED_FOR(1.1, replacement) #else #define FOO_DEPRECATED_SINCE_1_1 #define FOO_DEPRECATED_SINCE_1_1_FOR(replacement) #endif #if FOO_VERSION_MAX_ALLOWED < FOO_VERSION_1_1 #define FOO_AVAILABLE_SINCE_1_1 HEDLEY_UNAVAILABLE(1.1) #else #define FOO_AVAILABLE_SINCE_1_1 #endif #if FOO_VERSION_MIN_REQUIRED > FOO_VERSION_1_2 #define FOO_DEPRECATED_SINCE_1_2 HEDLEY_DEPRECATED(1.2) #define FOO_DEPRECATED_SINCE_1_2_FOR(replacement) HEDLEY_DEPRECATED_FOR(1.2, replacement) #else #define FOO_DEPRECATED_SINCE_1_2 #define FOO_DEPRECATED_SINCE_1_2_FOR(replacement) #endif #if FOO_VERSION_MAX_ALLOWED < FOO_VERSION_1_2 #define FOO_AVAILABLE_SINCE_1_2 HEDLEY_UNAVAILABLE(1.2) #else #define FOO_AVAILABLE_SINCE_1_2 #endif
Notice that we still allow the consumer to simply define FOO_TARGET_VERSION; many consumers will not need the flexibility of setting the minimum required and maximum allowed versions separately.
One advantage of this method is that, if you need to call deprecated functions in your code, you can simply set FOO_VERSION_MIN_REQUIRED and FOO_VERSION_MAX_ALLOWED to 0 and FOO_VERSION in your code, and you don't have to have a special case for FOO_COMPILATION.