[swift unboxed]

Safely unboxing the Swift language & standard library


Simulator Detection with targetEnvironment

New in Swift 4.1: easier detection and conditional compilation for targeting the simulator.

29 March 2018 ∙ Swift Internals ∙ written by Swift 4.1

The Swift compiler supports conditions on whether to build certain parts of the code. Unlike regular if and else which execute at run time, #if and #else are checked at compile time.

That means you can exclude some code based on the operating system or architecture. Perhaps you have an iOS application that uses the camera or Metal, and you need to exclude some code from running on the simulator? Or maybe there’s some code you only want to compile and run in the simulator?

#if (arch(x86_64) || arch(i386)) && os(iOS)
  // Simulator!
#endif

iOS running on Intel (x86_86 or i386) is a good way to detect the simulator. But why should we have to know about processor architectures? Can we do better?

Swift Evolution proposal SE-0190 brings a new targetEnvironment platform check to see if you’re running in a simulator environment. As of Swift 4.1 you can now do this:

#if targetEnvironment(simulator)
  // Simulator!
#endif

How do these platform conditions work? And how did heroes of the day Erica Sadun and Graydon Hoare get it all working?

Let’s dive into some C++ code and look at the implementation. What are all the conditional compilation flags? How many items are in a triple? The answers may surprise you! 🔎

Target Triples

The compiler needs to know about the final target when doing its work. Is this code going to run on a Mac? On Windows? On iOS? On an iOS device, or the simulator?

A target triple usually has three components to specify a compile target:

  1. Architecture
  2. Vendor
  3. Operating system

You can have sub-architectures too, and operating systems can have versions attached, but those are the big three components.

There’s an optional fourth component to specify environment (or maybe ABI, depending on which set of docs you believe).

Compiling a tvOS app for the device might have this target: arm64-apple-tvos.

But compiling that same app for the simulator might look like this: x86_64-apple-tvos-simulator.

That’s Architecture-Vendor-OS-Environment. A four-element triple. 🤯

Simulator Environment

The first piece we need is to support “simulator” as a possible environment. Diff D39143 on the LLVM project by Bob Wilson adds a new Simulator case to the EnvironmentType enumeration and then adds some helpers:

bool isSimulatorEnvironment() const {
  return getEnvironment() == Triple::Simulator;
}

And like any good feature addition, it comes with tests!

T = Triple("x86_64-apple-ios10.3-simulator");
EXPECT_TRUE(T.isiOS());
T.getiOSVersion(Major, Minor, Micro);
EXPECT_EQ((unsigned)10, Major);
EXPECT_EQ((unsigned)3, Minor);
EXPECT_EQ((unsigned)0, Micro);
EXPECT_TRUE(T.isSimulatorEnvironment());

Given the platform triple "x86_64-apple-ios10.3-simulator", we expect to know it’s iOS 10.3 running on the simulator.

Extra credit: Curious about all the possible architectures, sub-architectures, vendors, environments, and more? Have a look at include/llvm/ADT/Triple.h for all the enumerations.

Pre-Environment

If support for the “simulator” environment is new, how did the compiler know whether it was building for the simulator vs device back in the old days?

The answer is in Platform.cpp. Spoiler: it will look very familiar to you.

bool swift::tripleIsiOSSimulator(const llvm::Triple &triple) {
  llvm::Triple::ArchType arch = triple.getArch();
  return (triple.isiOS() &&
          (arch == llvm::Triple::x86 || arch == llvm::Triple::x86_64));
}

First, get the current architecture in arch. Then return true if we’re on iOS and the architecture is either x86 or x86_64. This is pretty much the same thing we used to write in Swift:

#if os(iOS) && (arch(i386) || arch(x86_64))

As they say, there’s nothing new under the sun. 😉

Post-Environment

Now that we can detect the simulator environment directly, what change do we need to make to the compiler to pick it up?

Here’s the same tripleIsiOSSimulator() function after adding environment support:

bool swift::tripleIsiOSSimulator(const llvm::Triple &triple) {
  llvm::Triple::ArchType arch = triple.getArch();
  return (triple.isiOS() &&
          // FIXME: transitional, this should eventually stop testing arch, and
          // switch to only checking the -environment field.
          (triple.isSimulatorEnvironment() ||
           arch == llvm::Triple::x86 || arch == llvm::Triple::x86_64));
}

Note the only change is to add the triple.isSimulatorEnvironment() check, but the old “checking the architecture” strategy is still there for a transition period.

Ideally we could check isSimulatorEnvironment() and that would be the one-stop shop to see if we’re in any kind of simulator — either TV, watch, or iDevice. In practice, there’s still a little transitional code here as well:

bool swift::tripleIsAnySimulator(const llvm::Triple &triple) {
  // FIXME: transitional, this should eventually just use the -environment
  // field.
  return triple.isSimulatorEnvironment() ||
         tripleIsiOSSimulator(triple) ||
         tripleIsWatchSimulator(triple) ||
         tripleIsAppleTVSimulator(triple);
}

So, there’s all the support we need to detect whether we’re in a simulator. Now on to the new syntax! 📝

Compiler Support

Next, we need to add support for the new targetEnvironment condition in LangOptions.h. There are already five platform conditions and we’re adding one more:

enum class PlatformConditionKind {
  /// The active os target (OSX, iOS, Linux, etc.)
  OS,
  /// The active arch target (x86_64, i386, arm, arm64, etc.)
  Arch,
  /// The active endianness target (big or little)
  Endianness,
  /// Runtime support (_ObjC or _Native)
  Runtime,
  /// Conditional import of module
  CanImport,
  /// Target Environment (currently just 'simulator' or absent)
  TargetEnvironment,
};

And then specify the possible options in LangOptions.cpp:

static const StringRef SupportedConditionalCompilationTargetEnvironments[] = {
  "simulator",
};

There’s only one option for the moment: targetEnvironment(simulator).

We have all the pieces now:

  • We can tell if we’re compiling for the simulator with tripleIsAnySimulator().
  • The compiler recognizes the targetEnvironment condition.

All that’s left is to query whether we’re in the simulator at the right moment.

Extra credit: what is the link between targetEnvironment and the call to tripleIsAnySimulator()? Is there a direct function call, or is it more indirect? See this commit.

Final Polish

The new targetEnvironment feature is there, but what of all that legacy code that checks for simulators the old-fashioned way?

Not to worry — your heroes have thought of this and as a final bit of polish, added a warning diagnostic.

Here’s what the text of the warning will look like, in DiagnosticsParse.def:

WARNING(likely_simulator_platform_condition,none,
    "plaform condition appears to be testing for simulator environment; "
    "use 'targetEnvironment(simulator)' instead",
    ())

Now the question is, how does the compiler know to trigger this warning?

You can look at the full commit but the gist of it is to look for code that looks like our old friend:

#if os(iOS) && (arch(x86_64) || arch(i386))

If we’re looking for a particular operating system (iOS, tvOS, or watchOS) and a particular architecture (i386 or x86_64) then that’s a simulator test. If the compiler finds that combination, it should trigger the warning:

if (Expr *Test = findAnyLikelySimulatorEnvironmentTest(Condition)) {
  diagnose(Test->getLoc(),
           diag::likely_simulator_platform_condition)
    .fixItReplace(Test->getSourceRange(),
                  "targetEnvironment(simulator)");
}

The findAnyLikelySimulatorEnvironmentTest() helper will look for the OS + architecture combination. If found, trigger the diagnostic.

And for extra credit: note the replacement fix-it so that Xcode will offer to automatically update the platform check code. Such convenience and service! ❤️

The Closing Brace

Like many seemingly simple features, this one has many levels of the stack that need some work to implement what we want:

  • Supporting a new “simulator” environment in the target triple
  • Reading the environment part of the triple in the compiler
  • Adding support for a new targetEnvironment platform check keyword
  • Hooking up the keyword to perform the environment check
  • Adding a diagnostic and fix-it to support migration

If you’re curious about all the possible platform conditions — OS, architectures, endianness, runtimes, environments — have a look at LangOptions.cpp for all the options.

As for me, I work on an app with plenty of AVFoundation capture and Metal code, and we do some simulator checking; I look forward to a smooth Swift 4.1 migration with lots of fix-its to do the work for me. 😎

}