Using #ifdefs, or your language's equivalent, is a common occurrence in code, not only when writing cross-platform code. #ifdefs (or #if in C#, Swift and Iodine and {$IF} in Oxygene), let you conditionally compile different parts of your project — for example to account for APIs not available on all platforms, or for code you want to run only in debug or in release mode.

The downside is, #ifdefs make for pretty ugly code:

#if COCOA
var formatter = new Foundation.NSDateFormatter();
formatter.dateFormat = format;
formatter.timeZone = timeZone;
return formatter.dateFromString(value);
#endif
#if JAVA
var formatter = new java.text.SimpleDateFormat(format, Locale.Invariant);
formatter.TimeZone = timeZone;
return DateTime(formatter.parse(value));
#endif

Elements 10 does away with the need for ugly #ifdef syntax by introducing a new defined() system function. With defined(), you can express conditional compilation as part of regular code flow – the compiler will take care of the rest.

if (defined("COCOA")) {
  var formatter = new Foundation.NSDateFormatter();
  formatter.dateFormat = format;
  formatter.timeZone = timeZone;
  return formatter.dateFromString(value);
} else if (defined("JAVA")) {
  var formatter = new java.text.SimpleDateFormat(format, Locale.Invariant);
  formatter.TimeZone = timeZone;
  return DateTime(formatter.parse(value));
}

That's already much nicer, right? Notice how this now flows just like regular conditional code would. But also notice how you can still use platform-specifc APIs in each section of code! That's because the compiler still takes this if clause apart, and will compile one part when targeting Cocoa, and the other when targeting Java.

The same of course works in all languages. E.g. Oxgyene:

if defined('COCOA') then begin
  var lFormatter := new Foundation.NSDateFormatter();
  lFormatter.dateFormat := aFormat;
  lFormatter.timeZone := timeZone;
  result := lFormatter.dateFromString(value);
end
else if defined('JAVA') then begin
  var lFormatter := new java.text.SimpleDateFormat(aFormat, Locale.Invariant);
  lFormatter.TimeZone =: timeZone;
  result := lFormatter.parse(value) as DateTime;
end;

But we can take this a step further:

public string getPathSeparator() {
  if (defined("COCOA") || Environment.OS == OperatingSystem.macOS) {
    return "/";
  } else if Environment.OS == OperatingSystem.Windows {
    return "\";
  }
}

In this (a bit contrived – but I have code like this all across our EBuild project) example, we're combining defined with regular application logic.

When building for Cocoa, the compiler will determine defined("COCOA") to be true, short-circuit the rest of the if clause, and simply emit the return "/". No runtime check happening at all.

When building for a different platform, it will emit code for the platform check, and the right path will be taken at runtime.

This allows for some pretty powerful and flexible cross-platform code that's also a lot easier to read than the #ifdef equivalent.

But it's not only cross-platform code that can benefit; consider for example the following:

var report = ...;
if (defined("DEBUG") || report.level == ReportLevel.critcal) {
  report.emailToCEO()
}

Here, how to react to a given "report" object depends both on the debug/release level of the app, but also on runtime logic. In debug builds, all reports will be sent, while in release builds the level will be taken into consideration at runtime.

Swift and #defined

While defined() is a system function provided by the Elements compiler and available on all languages and platforms, we've also added a second syntax specifically for Swift that is more aligned with existing #available() and similar constructs:

So in Swift, regardless of platform, you can use #defined() and omit the quotes, e.g.:

if #defined(COCOA) && #available(iOS 11.2) {
  ...
}

This works the same as defined("COCOA") would, just looks more Swifty.

#if Isn't Going Away

Of course #if and {$IF} continue to be supported as well, and still have their place when conditionally defining larger scopes than an if statement can handle – such as whole methods or classes.

But you will find that inside method bodies, the new syntax allows for much more expressive and easier to read conditional compilation – not to mention better integration of compile-time conditions with runtime decisions, as seen in the samples above.

 

The new defined() infrastructure is just one of many, many new features for Elements 10 already available in the current builds. Check it out and let us know what you think!