`#if os(Darwin)`, a shorthand for checking for Darwin platforms

There's some technical debate within Apple about whether "Darwin" is an appropriate condition to be testing for. It would help if people could talk about the sorts of code that they would put inside a check like this.

6 Likes

I would use it primarily to check for the open source implementation of Foundation (to cope with the API and behavioral differences). Right now I’m substituting it with canImport(ObjectiveC), which happens to be a good surrogate in practice, although the semantics is wrong.

6 Likes

One case that I would find it convenient for is as a replacement for:

#if os(iOS) || os(macOS) || os(tvOS) || os(watchOS)

which is used in the standard library.

In other cases, we use the inverse for this condition as:

#if !os(Android) && !os(Linux) && !os(Windows)

A third spelling for this that I seem to recall is

#if canImport(Darwin)

Now it could be argued that this might be better spelt as vendor(Apple), which perhaps would work but might complicate future development (e.g. if something would be incompatible with the set for whatever reason).

I think any spelling we pick for this is at risk of future-proofing issues, unless the code inside the #if blocks is extremely closely tied to the directive.

For example,

#if canImport(Darwin)
import Darwin
#endif

easily passes this test, but something like


#if os(iOS) || os(macOS) || os(tvOS) || os(watchOS)
// okay to use 'someFunction' here because of implementation
// details on Apple platforms.
someFunction()
#endif

is a bit more suspect.

So I think it’s important to focus on John’s request for information about what goes inside this check, rather than what condition, precisely, this check would be an alias for.

2 Likes

Ah, sorry, I must have deleted that part of the message. This pattern is used in various places in Foundation, bits in Dispatch, and I've run into it in a few places in the standard library. As to the code that sits under the path, it tends to be calls into the system itself, using APIs that are target dependent in different ways - e.g. libc APIs, kernel integration, etc. But all of the cases are system specific behaviour that are needed to support the higher level abstractions or to deal with ABI constraints.

1 Like

The thing I really like about the canImport approach is that it is a capability-based system.

When I use canImport(Darwin) or canImport(UIKit), all I'm expressing is that I need an implementation of the Darwin/UIKit interfaces; it is not bound to any specific platform. We'll try to build that code for iOS, iPadOS, tvOS, macOS (catalyst), any future Apple platforms which provide those interfaces, and even non-Apple platforms (if somebody writes a compatible interface for that platform).

As for why Apple's code might explicitly list OSes - I can only speculate, but given that many frameworks are shipped as part of the OS itself (stdlib, Foundation, System, etc), these guards are performing a very different duty to the kind of capability-testing that 3rd party, cross-platform libraries perform. For Apple's frameworks, APIs guarded by #if os(X) can define what is exposed by the OS, so there might be additional value in listing each of those OSes individually.

Or, as I said before, it might just be a random style choice that nobody's thought too hard about. My point is that it's difficult to extrapolate from patterns seen in Apple's code.

In other words:

  • Third-party code is a consumer of platform APIs, so a capability-based guard (canImport) makes a lot of sense.
  • Apple's first-party code is a provider of platform APIs, so explicitly listing each platform might be more appropriate in some cases.

When you say "appropriate condition to be testing for", do you mean for third-party code, first-party code (say, as part of internal coding standards), or both?

6 Likes

This is general language feature, so we're almost exclusively concerned about how developers outside of Apple would use this feature. Apple has a specific idea of what "Darwin" means that isn't 100% equivalent to "a full-featured end-user graphical OS released by Apple", and we want to understand whether that's correlated with what people are testing for, or whether we should suggest a different name.

3 Likes

This is a good characterisation of what my mental model of what "Darwin" is. It has nothing to do with the UI frameworks or capabilities (checks for the framework seem appropriate for that). It is about the kernel + system libraries and interfaces.

That is, I would expect that if os(Darwin) returns true I can assume that low level system behaviour is roughly similar (e.g. mach ports are available). If there are differences that are os variant specific, os(iOS) or os(macOS) are valid checks and may be combined to check for specifics. If there are further differences that are important, there are checks such as canImport and environ that can help identify if the execution environment is catalyst or a simulator or if a particular module is available.

Then platform or kernel should be a better name? I think it’s good to keep the exclusiveness of os.

Also we may consider something like exec(MachO). In most cases it performs identically to the proposed os(Darwin).

Sure, I don't have a problem with platform though kernel might be too specific (I would expect that the Darwin "os" would still have libSystem and libc++ for example).

Having the ability to check for the executable format would be nice, but that is orthogonal. I can have MachO binaries on Windows - x86_64-unknown-windows-macho is a valid triple.

1 Like

Bringing this thread back up again because I’m in the process of porting my code to visionOS and part of it is going through and changing all my #if os(iOS) to #if os(iOS) || os(visionOS). That’s fine but what’s really annoying is all the Swift package dependencies I use. Most are not updated to support visionOS and the few that are have not released the changes yet. That means I am now in the process of forking every project, adding boilerplate, and sending a PR hoping the project is still maintained or if I have to keep a fork forever. It would be great if I don’t have to do this again the next time Apple adds a platform.

Also in Objective-C, TARGET_OS_IOS is defined for all iOS-derived platforms such as tvOS and visionOS. It would be great if Swift has something similar. Maybe if not os(Darwin) we can have something like osFamily(iOS) or osFamily(Darwin) and so on.

7 Likes

It would be interesting to know how many of those conditions could have been replaced with canImport.

Perhaps we need clearer guidance about building libraries which only conditionally depend on Apple system frameworks?

As this thread looks somewhat revived, I'd like to chime in with an idea I mentioned in the Future Directions section of cross-compilation SE-0387 proposal. IMO adding these ad-hoc checks doesn't seem to be scalable. There were previous pitches for checking if a platform is 64-bit, it also makes sense to check if a platform is little-endian or big-endian, what libc and what version of libc is it using, what kernel version etc. Even for 64-bit checks, do you mean 64-bit registers or 64-bit addressing?

A Swift SDK declaring a dictionary of attributes that describe all of these things seems much more scalable than adding separate capabilities to #if compile-time checks each time, potentially requiring an evolution proposal and a lengthy review when a need for a new one appears.

1 Like

Wrong link? I think you mean this.

1 Like

Thanks, I've updated the link. To elaborate, if a given Swift SDK in its metadata declares a dictionary of platform properties such as this:

"platform": {
  "kernel": "Linux",
  "libcFlavor": "Glibc",
  "libcMinVersion": "2.36",
  "cpuArchitecture": "aarch64"
  // more platform capabilities defined here...
}

then I'm interested if we could come up with some syntax that can check for properties in this dictionary when building against such SDK, for example:

#if platform(kernel: "Linux") && platform(libcFlavor: "Glibc")

Then what's being proposed in this thread would become

#if platform(vendor: "Apple")

And if someone wants to port Swift to their favorite OS, they'd just create a Swift SDK for it with appropriate metadata and wouldn't have to pitch an addition of a new #if os(MyFavoriteOS).

This would simplify a lot of code in the stdlib and other low-level libraries too, where we frequently need to have different implementations for 64-bit and 32-bit environments, or little-endian and big-endian.

Right now it's just a chain of ugly and error-prone expressions like arch(arm64) || arch(arm64_32) || arch(x86_64). And in some cases you need arch(arm64) || arch(x86_64) instead, which again highlights the need to distinguish between support for 64-bit registers and 64-bit pointers, and a need for a more general solution for platform checks so that these could become #if platform(pointerBitWidth: 64) and #if platform(gpRegisterBitWidth: 64) respectively.

7 Likes

Darwin to me still means Darwin, the open source "subset" (variant, really) of MacOS X. It didn't have a GUI or most of the Objective-C frameworks - it was more like a basic headless BSD distro (IIRC).

Darwin no longer exists publicly (although you can still download some of what would be its source, right up to the current macOS version), though living community forks of it do (e.g. PureDarwin).

The use of the term 'Darwin' is thus (to me) kind of an anachronism, at least outside Apple.

The use-cases I've had for OS checks in general have been:

  • The classic Darwin vs Glibc (for which canImport is clearly the better solution).
  • As a complete and fragile hack to try to handle different implementations of Foundation.
  • As a blunt instrument to detect e.g. compiling on Windows, and just erroring out with a "Sorry, this package doesn't support Windows [yet]". Which is probably really just about the above two, when you get down to it.

I'm all for a more literally concise and semantically accurate way to express what's otherwise a long string of if os(…) || os(…) || …, but if os(Darwin) feels semantically wrong. Generally canImport is a better solution, but there does still need to be a solution for the case where you can import a module (e.g. Foundation), but there are - for better or worse, intentional or not - major API or functional differences in that module across platforms / vendors / whatever. Some kind of (figurative) if foundationCanDo(…)

In Objective-C land you can get a long way with tools like respondsToSelector:, but Swift is by its nature less flexible and introspective like that.

I'm with @Karl et al that a capabilities-based approach is so much better than magic constants. It's much less susceptible to the problems @philipturner (here), @wowbagger (here), and @jrose (here) pointed out when a new platform (or whatever) comes into existence, and much friendlier to the existing set of Swift-supported platforms (the number of Swift packages that would actually work on Linux, or Windows, if not for careless #if checks… :confused:).

The approach suggested by @Max_Desiatov seems fine to me for other reasons, but I don't think it can be a full solution. Likewise @SDGGiesbrecht's suggested enhancements are great for other reasons and might be part of a solution but can't be sufficient alone.

2 Likes

People are already switching over long checks like #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) to #if canImport(Darwin), so clearly there is a need for something like this.

1 Like

Can you elaborate on how this shows an unfulfilled need?

To my eyes, it seems that almost every single one of those conditions are guarding literal imports of the Darwin module, APIs it contains, or APIs which depend on definitions exported by it. As far as I can tell, switching to canImport(Darwin) makes sense, and an alternative os(Darwin) would not improve any of those conditions.

I can only spot 2 cases where it is not being used to guard uses of the Darwin module specifically:

  1. In some cases it is used to guard calls to the autoreleasepool function. Technically, that function lives in the ObjectiveC module (which is being imported via Foundation) so that would be a better condition, but I'll admit it's also a bit pedantic.

  2. NIOCrashTester seems to use a pattern of #if !canImport(Darwin) || os(macOS) to exclude all Apple platforms except macOS. It is possible that something like Max's idea could provide a better constraint for what they actually need, but I find it hard to see how os(Darwin) would make that particular case any better.


SE-0075:

Inclusive OS tests (if os1 || os2 || os3...) must be audited each time the set of possible platforms expands. In addition, compound build statements are harder to write, to validate, and are more confusing to read. They are more prone to errors than a single test that's tied to the API capabilities used by the code it guards.

That last bit - "a single test that's tied to the API capabilities used by the code it guards" - that is canImport.

If it is lacking in some way, we should discuss improving it, but please come with some specifics about how it is lacking. So far it seems there is nothing to this discussion - merely observations about some Apple project doing one thing, and some other project (incidentally, also an Apple project) doing something else...

4 Likes

I was referencing this post and others like it from past summer that say canImport(Darwin) means something different than the longer line it's replacing, though there's obviously overlap.

I did not check all those to see which actually differ: it was simply to show that people are getting fed up and switching to canImport(Darwin) despite it not always applying.

I disagree, that probably refers to something much more fine-grained like #if platform(selector: "kqueue"), so that NIO module would just start working on BSD because it defines its default selector as kqueue. Swift, like most languages before it, has manifestly failed at enabling such capability-oriented versioning, though I would like to still see Swift make an effort to enable it.

What we are discussing here is how to partition capability sets, tied to various technologies like kernels, C libraries, and operating systems. I can't speak to how that would work on Darwin platforms, as I may be the only person in this forum who's never used Swift on any Darwin platform.

I can say how I'd like it to look on linux-based platforms like Android though. Where I would like to see Android end up is to have three conditions for its capability sets: platform(kernel: "linux") for all linux kernel APIs, canImport(Bionic) for Bionic libc APIs, and os(Android) for all other Android-specific behavior. Currently, only the last one exists.

If and when Swift is ever ported to other linux-based OS's like Tizen or Wear OS, all code versioned with platform(kernel: "linux") would just keep working.

If Swift is going to keep growing to new platforms, I agree with @Max_Desiatov that we need to overhaul how we version this code for various capability sets.

8 Likes

To expand on this with a concrete example, since you asked for some, I just saw this usage in the SwiftPM tests, because Darwin platforms all use libtool as their archiver. Obviously, that has nothing to do with the Darwin module, but is a separate commonality between these platforms.

Now, you could argue that these situations are sufficiently uncommon that people should just write out all the Darwin platforms separately for those few cases- I don't know how common they are since I lack experience with Darwin- but that is why people here are arguing for os(Darwin).

2 Likes