[Development] Upcoming Clang compiler changes for macOS and iOS: RFC for solutions

Jake Petroules Jake.Petroules at qt.io
Wed Sep 13 11:00:55 CEST 2017


Hello all,

I wanted to talk about some new features in Apple Clang that will affect us once Xcode 9 is released in 1 to 2 weeks.

Basically, there are new language extensions for C/C++ and Objective-C (__builtin_available() and @available()) that allow you to perform OS version runtime checks when calling APIs that may not be available on your deployment target. The key point is, the compiler warns you if the checks aren't present where they need to be.

The LLVM documentation can explain these best: http://clang.llvm.org/docs/LanguageExtensions.html#objective-c-available

So basically, we should no longer use QSysInfo or QOperatingSystemVersion to perform these checks, but instead use the compiler-provided functions. There are two new warnings in conjunction with this, -Wunguarded-availability (not enabled by default) and -Wunguarded-availability-new (enabled by default for APIs introduced in 2017 releases of Apple's OSes).

The problem is that these new builtins are only available in Xcode 9 and above, and we must still be able to build Qt with the version of Xcode we use on the macOS 10.10 CI (Xcode 7?). We have to use them when they're available, because the compiler can't recognize QSysInfo/QOperatingSystemVersion as a valid version check and therefore will still emit warnings. We also don't want to disable the warnings entirely for obvious reasons.

So my question is, for those of you who know the C++ preprocessor well enough, is there a way to transform this:

__builtin_available(macOS 10.13, iOS 11, *)

into some other form, using a C++ preprocessor macro and/or template magic? Note that the order and number of arguments is variable, the number of components in each dot-separated version number is variable, and the last argument must always be '*'. Maybe one of you can come up with some Alexei Alexandrescu-level crazy solution with macros and templates, but I'm thinking this is likely infeasible.

If it's not possible, we'll have to go with a solution like this:

#if __has_builtin(__builtin_available)
#define QT_AVAILABLE_MACOS(macos_major, macos_minor, macos_patch) \
    __builtin_available(macOS macos_major ## . ## macos_minor ## . ## macos_patch, *)
#else
#define QT_AVAILABLE_MACOS(macos_major, macos_minor, macos_patch) \
    QOperatingSystemVersion::current() >= QOperatingSystemVersion(QOperatingSystemVersion::MacOS, macos_major, macos_minor, macos_patch)
#endif

Because the semantics of using multiple @available / __builtin_available calls in a single if statement is not defined (you'll get a warning like this):

warning: @available does not guard availability here; use if (@available) instead [-Wunsupported-availability-guard]
    if (@available(macOS 10.12, *) || @available(iOS 10.11, *)) {

we must have a version of that QT_AVAILABLE_ macro for every combination of OSes we want to check at once. So, at least 6 - each of the four OSes by themselves, macOS and iOS together, and all four Apple platforms together. There's technically 8 platforms if you include app extensions but I've ignored that for now. For example:

#if __has_builtin(__builtin_available)
#define QT_AVAILABLE_MACOS(macos_major, macos_minor, macos_patch) \
    __builtin_available(macOS macos_major ## . ## macos_minor ## . ## macos_patch, *)
#define QT_AVAILABLE_IOS(ios_major, ios_minor, ios_patch) \
    __builtin_available(iOS ios_major ## . ## ios_minor ## . ## ios_patch, *)
#define QT_AVAILABLE_TVOS(tvos_major, tvos_minor, tvos_patch) \
    __builtin_available(tvOS tvos_major ## . ## tvos_minor ## . ## tvos_patch, *)
#define QT_AVAILABLE_WATCHOS(watchos_major, watchos_minor, watchos_patch) \
    __builtin_available(watchOS watchos_major ## . ## watchos_minor ## . ## watchos_patch, *)
#define QT_AVAILABLE_MACOS_IOS(macos_major, macos_minor, macos_patch, ios_major, ios_minor, ios_patch) \
    __builtin_available(macOS macos_major ## . ## macos_minor ## . ## macos_patch, iOS ios_major ## . ## ios_minor ## . ## ios_patch, *)
#define QT_AVAILABLE_DARWIN(macos_major, macos_minor, macos_patch, ios_major, ios_minor, ios_patch, tvos_major, tvos_minor, tvos_patch, watchos_major, watchos_minor, watchos_patch) \
    __builtin_available(macOS macos_major ## . ## macos_minor ## . ## macos_patch, iOS ios_major ## . ## ios_minor ## . ## ios_patch, tvOS tvos_major ## . ## tvos_minor ## . ## tvos_patch, watchOS watchos_major ## . ## watchos_minor ## . ## watchos_patch, *)
#else
#define QT_AVAILABLE_MACOS(macos_major, macos_minor, macos_patch) \
    QOperatingSystemVersion::current() >= QOperatingSystemVersion(QOperatingSystemVersion::MacOS, macos_major, macos_minor, macos_patch)
#define QT_AVAILABLE_IOS(ios_major, ios_minor, ios_patch) \
    QOperatingSystemVersion::current() >= QOperatingSystemVersion(QOperatingSystemVersion::IOS, ios_major, ios_minor, ios_patch)
#define QT_AVAILABLE_TVOS(tvos_major, tvos_minor, tvos_patch) \
    QOperatingSystemVersion::current() >= QOperatingSystemVersion(QOperatingSystemVersion::TvOS, tvos_major, tvos_minor, tvos_patch)
#define QT_AVAILABLE_WATCHOS(watchos_major, watchos_minor, watchos_patch) \
    QOperatingSystemVersion::current() >= QOperatingSystemVersion(QOperatingSystemVersion::WatchOS, watchos_major, watchos_minor, watchos_patch)
#define QT_AVAILABLE_MACOS_IOS(macos_major, macos_minor, macos_patch, ios_major, ios_minor, ios_patch) \
    QOperatingSystemVersion::current() >= QOperatingSystemVersion(QOperatingSystemVersion::MacOS, macos_major, macos_minor, macos_patch) || \
    QOperatingSystemVersion::current() >= QOperatingSystemVersion(QOperatingSystemVersion::IOS, ios_major, ios_minor, ios_patch)
#define QT_AVAILABLE_DARWIN(macos_major, macos_minor, macos_patch, ios_major, ios_minor, ios_patch, tvos_major, tvos_minor, tvos_patch, watchos_major, watchos_minor, watchos_patch) \
    QOperatingSystemVersion::current() >= QOperatingSystemVersion(QOperatingSystemVersion::MacOS, macos_major, macos_minor, macos_patch) || \
    QOperatingSystemVersion::current() >= QOperatingSystemVersion(QOperatingSystemVersion::IOS, ios_major, ios_minor, ios_patch) || \
    QOperatingSystemVersion::current() >= QOperatingSystemVersion(QOperatingSystemVersion::TvOS, tvos_major, tvos_minor, tvos_patch) || \
    QOperatingSystemVersion::current() >= QOperatingSystemVersion(QOperatingSystemVersion::WatchOS, watchos_major, watchos_minor, watchos_patch)
#endif

and a typical usage of the last one would look like:

    if (QT_AVAILABLE_DARWIN(/*macOS*/ 10,13,0, /*iOS*/ 11,0,0, /*tvOS*/ 11,0,0, /*watchOS*/ 4,0,0)) {
        // use new APIs...
    }

My other attempt at a macro which instead "backports" the new language feature looks like this:

#define QT_AVAILABLE_LPAREN (
#define QT_AVAILABLE_RPAREN )
#define QT_AVAILABLE_COMMA ,
#define QT_AVAILABLE_CAT(L, R) QT_AVAILABLE_CAT_(L, R)
#define QT_AVAILABLE_CAT_(L, R) L ## R
#define QT_AVAILABLE_EXPAND(...) __VA_ARGS__
#define QT_AVAILABLE_SPLIT(OP, D) QT_AVAILABLE_EXPAND(OP QT_AVAILABLE_CAT(QT_AVAILABLE_SPLIT_, D) QT_AVAILABLE_RPAREN)
#define QT_AVAILABLE_SPLIT_macOS QT_AVAILABLE_LPAREN QOperatingSystemVersion(QOperatingSystemVersion::MacOS QT_AVAILABLE_COMMA
#define QT_AVAILABLE_SPLIT_iOS QT_AVAILABLE_LPAREN QOperatingSystemVersion(QOperatingSystemVersion::IOS QT_AVAILABLE_COMMA
#define QT_AVAILABLE_SPLIT_tvOS QT_AVAILABLE_LPAREN QOperatingSystemVersion(QOperatingSystemVersion::TvOS QT_AVAILABLE_COMMA
#define QT_AVAILABLE_SPLIT_watchOS QT_AVAILABLE_LPAREN QOperatingSystemVersion(QOperatingSystemVersion::WatchOS QT_AVAILABLE_COMMA

static bool qtAvailable(const std::vector<QOperatingSystemVersion> &versions)
{
    std::vector<QOperatingSystemVersion::Type> types;
    const auto current = QOperatingSystemVersion::current();
    for (const auto &v : versions) {
        types.push_back(v.type());
        if (current >= v)
            return true;
    }
    return !types.contains(current.type());
}

#if __has_builtin(__builtin_available) && 0
#define __builtin_available2 __builtin_available
#define __builtin_available3 __builtin_available
#define __builtin_available4 __builtin_available
#else
#define __builtin_available(a, e) \
    qtAvailable({QT_AVAILABLE_SPLIT(, a)})
#define __builtin_available2(a, b, e) \
    qtAvailable({QT_AVAILABLE_SPLIT(, a), QT_AVAILABLE_SPLIT(, b)})
#define __builtin_available3(a, b, c, e) \
    qtAvailable({QT_AVAILABLE_SPLIT(, a), QT_AVAILABLE_SPLIT(, b), QT_AVAILABLE_SPLIT(, c)})
#define __builtin_available4(a, b, c, d, e) \
    qtAvailable({QT_AVAILABLE_SPLIT(, a), QT_AVAILABLE_SPLIT(, b), QT_AVAILABLE_SPLIT(, c), QT_AVAILABLE_SPLIT(, d)})
#endif

But I don't know how to transform i.e. "10.10" into 10,10" in the expanded macro, nor do I know any way to support variable arguments in a way that lets us drop the last argument.

So... can anyone do better than my versions? Patches very welcome. :)
-- 
Jake Petroules - jake.petroules at qt.io
The Qt Company - Silicon Valley
Qbs build tool evangelist - qbs.io




More information about the Development mailing list