Skip to main content

How the API_AVAILABLE Macro Works on macOS: C Preprocessor Deep Dive

· 6 min read
Pranav Ram Joshi
Software Engineer — Systems & Networks

There are various ways one could use C preprocessor macros. Learning how to properly use them — or failing to — can end up producing some bizarre codebases. If you have ever encountered the <os/availability.h> (see Apple os/availability.h source (GitHub — apple-oss-distributions)) header file on macOS, you'll find some macros that are confusing to interpret.

The API_AVAILABLE Macro Definition

The macro is defined as follows:

/*
* API Introductions
*
* Use to specify the release that a particular API became available.
*
* Platform names:
* macos, ios, tvos, watchos
*
* Examples:
* API_AVAILABLE(macos(10.10))
* API_AVAILABLE(macos(10.9), ios(10.0))
* API_AVAILABLE(macos(10.4), ios(8.0), watchos(2.0), tvos(10.0))
*/
#define API_AVAILABLE(...) \
__API_AVAILABLE_GET_MACRO(__VA_ARGS__,__API_AVAILABLE8, __API_AVAILABLE7, __API_AVAILABLE6, __API_AVAILABLE5, __API_AVAILABLE4, __API_AVAILABLE3, __API_AVAILABLE2, __API_AVAILABLE1, 0)(__VA_ARGS__)

At first glance, this seems terrifying. The documentation helps provide a bit of information as to how the macro should be invoked, but the implementation detail that I'm interested in is for one to study on their own. This macro ultimately expands into __attribute__((availability(...))) (see Clang Attributes Reference — availability), a compiler attribute that marks the platform and version where a given API was introduced. __API_AVAILABLE_GET_MACRO is another parameterized macro that is not defined in this file but in <AvailabilityInternal.h> header file. If we browse that file, we get the definition as:

#define __API_AVAILABLE_PLATFORM_macos(x) macos,introduced=x
#define __API_AVAILABLE_PLATFORM_macosx(x) macosx,introduced=x
#define __API_AVAILABLE_PLATFORM_ios(x) ios,introduced=x
#define __API_AVAILABLE_PLATFORM_watchos(x) watchos,introduced=x
#define __API_AVAILABLE_PLATFORM_tvos(x) tvos,introduced=x

#if defined(__has_attribute)
#if __has_attribute(availability)
#define __API_A(x) __attribute__((availability(__API_AVAILABLE_PLATFORM_##x)))
#else
#define __API_A(x)
#endif
#else
#define __API_A(x)
#endif

#define __API_AVAILABLE1(x) __API_A(x)
#define __API_AVAILABLE2(x,y) __API_A(x) __API_A(y)
#define __API_AVAILABLE3(x,y,z) __API_A(x) __API_A(y) __API_A(z)
#define __API_AVAILABLE4(x,y,z,t) __API_A(x) __API_A(y) __API_A(z) __API_A(t)
#define __API_AVAILABLE5(x,y,z,t,b) __API_A(x) __API_A(y) __API_A(z) __API_A(t) __API_A(b)
#define __API_AVAILABLE6(x,y,z,t,b,m) __API_A(x) __API_A(y) __API_A(z) __API_A(t) __API_A(b) __API_A(m)
#define __API_AVAILABLE7(x,y,z,t,b,m,d) __API_A(x) __API_A(y) __API_A(z) __API_A(t) __API_A(b) __API_A(m) __API_A(d)
#define __API_AVAILABLE8(x,y,z,t,b,m,d,l) __API_A(x) __API_A(y) __API_A(z) __API_A(t) __API_A(b) __API_A(m) __API_A(d) __API_A(l)
#define __API_AVAILABLE_GET_MACRO(_1,_2,_3,_4,_5,_6,_7,_8,NAME,...) NAME

Note the use of the token pasting operator (##) (see GNU Token Pasting) in the definition of __API_A. This operator concatenates __API_AVAILABLE_PLATFORM_ with the argument x at preprocessing time, selecting the correct platform macro — for instance, __API_AVAILABLE_PLATFORM_macos when x is macos(10.10).

Macro Expansion: Step by Step

The focal point of this blog is that how the expansion of API_AVAILABLE() takes place. Let's try to study one of the example. Say that this macro is invoked as: API_AVAILABLE(macos(10.10), ios(9.9)). The following expansion would take place:

/* invoking the macro */
API_AVAILABLE(macos(10.10), ios(9.9))

/* would expand to */
__API_AVAILABLE_GET_MACRO(macos(10.10), ios(9.9),__API_AVAILABLE8, __API_AVAILABLE7, __API_AVAILABLE6, __API_AVAILABLE5, __API_AVAILABLE4, __API_AVAILABLE3, __API_AVAILABLE2, __API_AVAILABLE1, 0)(macos(10.10), ios(9.9))

/*
* realize that the ninth argument to `__API_AVAILABLE_GET_MACRO` is the one that this macro expands to.
* after the ninth argument, this macro is defined to be variadic, and the expansion doesn't use them either.
* So the expansion would look like
*/
__API_AVAILABLE2(macos(10.10), ios(9.9))

/* which would further expand to */
__API_A(macos(10.10)) __API_A(ios(9.9))

/* which would then be expanded to (assuming that `__has_attribute` is defined and `availability` is available) */
__attribute__((availability(__API_AVAILABLE_PLATFORM_macos(10.10)))) __attribute__((availability(__API_AVAILABLE_PLATFORM_ios(9.9))))

/* finally, the expansion of `__API_AVAILABLE_PLATFORM_XXX()` macro will expand as */
__attribute__((availability(macos,introduced=10.10))) __attribute__((availability(ios,introduced=9.9)))

How VA_ARGS Enables Argument Counting

Here, the magic happens during the expansion of __API_AVAILABLE_GET_MACRO. Notice that it expanded to __API_AVAILABLE2() given that we called API_AVAILABLE with two arguments. This is possible because of how __VA_ARGS__ works. GNU Variadic Macros states the following:

The variable argument is completely macro-expanded before it is inserted into the macro expansion, just like an ordinary argument.

Consider that we have the following macro definition:

#define args(...) \
printf("%s\n", #__VA_ARGS__)

int
main (void)
{
args(Hello, World!);

return (0);
}

After compilation, this program would output the following:

$ ./prog
Hello, World!

Inside the definition of args, the variadic arguments was converted to string using GNU Stringification. Notice that the token comma (,) was also part of the variadic argument. GNU's documentation states the following:

[This] kind of macro is called variadic. When the macro is invoked, all the tokens in its argument list after the last named argument (this macro has none), including any commas, become the variable argument.

Because of this property, after the invocation and expansion of API_AVAILABLE, the variable arguments are replaced in this macro, thereby allowing the expansion of the subsequent macro to have the comma included and shifting the arguments as needed. Consider another example:

API_AVAILABLE(macos(5.5), ios(6.6), macosx(7.7), watchos(8.8))

The expansion would be as follows:

__API_AVAILABLE_GET_MACRO(macos(5.5), ios(6.6), macosx(7.7), watchos(8.8),__API_AVAILABLE8, __API_AVAILABLE7, __API_AVAILABLE6, __API_AVAILABLE5, __API_AVAILABLE4, __API_AVAILABLE3, __API_AVAILABLE2, __API_AVAILABLE1, 0)(macos(5.5), ios(6.6), macosx(7.7), watchos(8.8))

As mentioned previously, the variable argument is completely macro-expanded before the macro expansion. This includes the comma tokens present in the variable argument. The result is a well-known C preprocessor technique: macro argument counting. The expansion to __API_AVAILABLE_GET_MACRO contains multiple arguments, but only the first 9 positional slots matter. The ninth positional argument is the one the macro expands to — and because the variadic arguments shift the later parameters to the right, the correct __API_AVAILABLEN variant lands in that ninth slot. In this case, it would be __API_AVAILABLE4, which is the one we need as we supplied API_AVAILABLE with four arguments.

Defensive Macro Design with __has_attribute

Also, it uses one of the good practice when using macros. Notice the following fragment:

#if defined(__has_attribute)
#if __has_attribute(availability)
#define __API_A(x) __attribute__((availability(__API_AVAILABLE_PLATFORM_##x)))
#else
#define __API_A(x)
#endif
#else
#define __API_A(x)
#endif

Using macros can be helpful but they come at a cost. Since they are evaluated by the preprocessor (cpp(1)), they sometimes lead to bugs that can be hard to trace. Here, if we are working on a system that does not have the __has_attribute operator or the availability attribute, we must ensure that the macro expands to nothing. Without such a check, the code would fail to compile on platforms where these compiler extensions are unavailable. Apple uses this pattern not only for API_AVAILABLE, but also for related macros like API_DEPRECATED and API_UNAVAILABLE, all of which follow the same defensive design.

This kind of portability-conscious C programming is especially important in systems-level code. For a practical example, see my BPF packet filter implementation that targets multiple BSD variants and macOS.