Skip to main content

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

· 10 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> 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 for 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

The macro __API_AVAILABLE_GET_MACRO is used here for counting the arguments that was passed to API_AVAILABLE macro. This technique was discussed in 2006 where Laurent Deniau proposed a clever trick to count arguments of __VA_ARGS__. Bit of a drift from the original topic, but the C macro overview section of [Recursive macros with C++20] by David Mazières provides the cpptorture.c file, which apparently takes 100s of years and tremendous amount of memory to compile.

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:

#include <stdio.h>

#define A a
#define B b

#define CONCAT2(a, b) #a #b

#define PREFIX "expand_"

#define PREFIX_STR(...) PREFIX CONCAT2(__VA_ARGS__)

/*
* If you want one without the PREFIX form, see the macro below.
* But the concept remains same...
*/
#define STR(...) CONCAT2(__VA_ARGS__)

int
main (void)
{
char *result = PREFIX_STR(A, B);
char *unexpected = CONCAT2(A, B);

printf("The string in result is: %s\n", result);
printf("The string in unexpected is: %s\n", unexpected);

return (0);
}

After compilation, this program would output the following:

$ ./prog
The string in result is: expand_ab
The string in unexpected is: AB

Notice that macro A and B was expanded before it was placed as an argument for 'CONCAT2'. One could argue that it could also have been passed as:

CONCAT2(A, B)

and only later would it expand to a and b respectively. But this isn't the case, as can be seen from the second line of output. Refer to [Argument Prescan] section of Chapter 3 Macros from GNU's documentation. It explains that if the replacement list contained either stringification (#) or concatenation (##) operator, the argument is not macro-expanded.

tip

Because of this property, you could see that the concatenation operator is used only at the __API_A(x) macro above. Such macro wrapper is a common practice.

When the replacement list does not contain stringification (#) or concatenation operator (##), the behavior of variadic macro can be inferred easily. Consider the following example:

#include <stdio.h>

#define my_printf(...) \
printf(__VA_ARGS__)

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

int
main (void)
{

int num = 0x45;

my_printf("The value stored in num is: %x\n", num);
args(Hello, World!);

return (0);
}

whose output would be:

$ ./prog
The value stored in num is: 45
Hello, World!

On one hand, notice the behavior of my_printf macro. Since the replacement list for this macro does not contain # or ## operator, the comma , punctuator is treated normally, i.e., it is passed to printf(3) such that to allow specifying the format argument. But 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.

Finally, let's discuss the token pasting macro used in __API_A() macro. It uses the token pasting (##) operator but the pasted token itself is used as a macro. One could wonder that how the expansion is possible after the ## operator has been used. The following is stated in Argument Prescan section (which is linked above):

Macro arguments are completely macro-expanded before they are substituted into a macro body, unless they are stringized or pasted with other tokens. After substitution, the entire macro body, including the substituted arguments, is scanned again for macros to be expanded. The result is that the arguments are scanned twice to expand macro calls in them.

It states that once the macro-expansion of argument takes place, the macro body is again macro expanded. This is why we are able to create a macro body (replacement list) which uses the token pasting operator such that the pasted token result in a valid macro. Conside the following example:

#include <stdio.h>

/* Redundant macro */
#define RETRIEVE FOO

#define RETRIEVE_VAL(value) value
#define PASTE(prefix, value) prefix ## _VAL(value)

int
main (void)
{
int num1 = PASTE(RETRIEVE, 100);
printf("The value of num1 is: %d\n", num1);
int num2 = PASTE(RETRIEVE, 200);
printf("The value of num2 is: %d\n", num2);

return (0);
}

where, the output would be:

$ ./prog
The value of num1 is: 100
The value of num2 is: 200

Notice the first argument to PASTE macro must be RETRIEVE. A redundant RETRIEVE macro is #defined to show that when used in PASTE, it is not macro-expanded, i.e., the macro body would not become FOO_VAL(100) and FOO_VAL(200) and cause compilation error since there's no FOO_VAL macro. This is because the token pasting (##) operator is used. Inside PASTE macro, the macro body would become: RETRIEVE_VAL(100) and RETRIEVE_VAL(200). Since the macro body is scanned again, the preprocessor will notice that it is a valid macro in itself and the replacement macro for it is substituted, which would become 100 and 200 respectively.

Because of these properties, 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. In fact, extended Berekely Packet Filter (eBPF)--specifically Compile-Once Run-Everywhere (CO-RE) concept--heavily uses such macro magics to ensure consistency across various architectures. If you want to explore more of it, look into [libbpf/src/bpf_helpers.h] source file of libbpf.