Check deployment target at runtime or compile time


(Ole Begemann) #1

In macOS 10.12 and iOS 10.0, class properties were introduced to Objective-C [1]. I noticed that the Objective-C runtime treats class properties differently based on the deployment target. Example:

     // MyClass is an Objective-C class that has a class property
     let metaclass = object_getClass(MyClass.self)
     var count: UInt32 = 0
     let properties = class_copyPropertyList(metaclass, &count)
     // Work with properties
     // Deallocate properties

When the deployment target is macOS 10.12, passing a metaclass to copyPropertyList() returns the class properties, but it returns an empty list on a lower deployment target.

I'd like to perform a check (either at runtime or compile time) which of these behaviors I'll get.

I didn't find a way to perform a compile-time check in Swift against the deployment target; if #available() checks against the SDK the code is linked against. Likewise, the usual runtime check using ProcessInfo().isOperatingSystemAtLeast() doesn't check against the deployment target.

The best I came up with is this:

/**
  Returns `true` if the current deployment target is at least the specified version that corresponds to the current platform.

  The arguments must be passed in the form of the `__MAC_XX_X`, `__IPHONE_XX_X`, etc. constants defined in Availability.h.
  */
public func isDeploymentTargetAtLeast(
     macOS macOSVersion: Int32, iOS iOSVersion: Int32,
     tvOS tvOSVersion: Int32, watchOS watchOSVersion: Int32) -> Bool {

     #if os(macOS)
         return __MAC_OS_X_VERSION_MIN_REQUIRED >= macOSVersion
     #elseif os(iOS)
         return __IPHONE_OS_VERSION_MIN_REQUIRED >= iOSVersion
     #elseif os(tvOS)
         return __TV_OS_VERSION_MIN_REQUIRED >= tvOSVersion
     #elseif os(watchOS)
         return __WATCH_OS_VERSION_MIN_REQUIRED >= watchOSVersion
     #else
         return false
     #endif
}

For example, to test if the current deployment target is at least macOS 10.12, iOS 10.0, tvOS 10.0, or watchOS 3.0:

guard isDeploymentTargetAtLeast(
     macOS: __MAC_10_12, iOS: __IPHONE_10_0,
     tvOS: __TVOS_10_0, watchOS: __WATCHOS_3_0) else {

     // Deployment target is lower
     ...
}

Is this correct? Is there a better way to do this?

[1]: http://useyourloaf.com/blog/objective-c-class-properties/


(Greg Parker) #2

In macOS 10.12 and iOS 10.0, class properties were introduced to Objective-C [1]. I noticed that the Objective-C runtime treats class properties differently based on the deployment target. Example:

   // MyClass is an Objective-C class that has a class property
   let metaclass = object_getClass(MyClass.self)
   var count: UInt32 = 0
   let properties = class_copyPropertyList(metaclass, &count)
   // Work with properties
   // Deallocate properties

When the deployment target is macOS 10.12, passing a metaclass to copyPropertyList() returns the class properties, but it returns an empty list on a lower deployment target.

That's right. The Objective-C runtime was written with the expectation that class properties would be added someday, but that code was buggy. On macOS 10.10 and iOS 8.x and older if libobjc sees a class property then it may crash. The fix was for clang and swiftc to leave class properties out of the ObjC metadata when compiling for those deployment targets.

macOS 10.11 and iOS 9.x deployment targets are both safe. (Class properties still did not exist then. Instead the bug that nobody had seen yet was unknowingly fixed when the code was rewritten for an unrelated optimization.)

Note that class property visibility depends on the deployment target used to compile that class's implementation. It is possible to see class properties from some classes but not others if the classes come from different executables with different deployment targets.

I'd like to perform a check (either at runtime or compile time) which of these behaviors I'll get.

I didn't find a way to perform a compile-time check in Swift against the deployment target; if #available() checks against the SDK the code is linked against. Likewise, the usual runtime check using ProcessInfo().isOperatingSystemAtLeast() doesn't check against the deployment target.

The best I came up with is this:

/**
Returns `true` if the current deployment target is at least the specified version that corresponds to the current platform.

The arguments must be passed in the form of the `__MAC_XX_X`, `__IPHONE_XX_X`, etc. constants defined in Availability.h.
*/
public func isDeploymentTargetAtLeast(
   macOS macOSVersion: Int32, iOS iOSVersion: Int32,
   tvOS tvOSVersion: Int32, watchOS watchOSVersion: Int32) -> Bool {

   #if os(macOS)
       return __MAC_OS_X_VERSION_MIN_REQUIRED >= macOSVersion
   #elseif os(iOS)
       return __IPHONE_OS_VERSION_MIN_REQUIRED >= iOSVersion
   #elseif os(tvOS)
       return __TV_OS_VERSION_MIN_REQUIRED >= tvOSVersion
   #elseif os(watchOS)
       return __WATCH_OS_VERSION_MIN_REQUIRED >= watchOSVersion
   #else
       return false
   #endif
}

For example, to test if the current deployment target is at least macOS 10.12, iOS 10.0, tvOS 10.0, or watchOS 3.0:

guard isDeploymentTargetAtLeast(
   macOS: __MAC_10_12, iOS: __IPHONE_10_0,
   tvOS: __TVOS_10_0, watchOS: __WATCHOS_3_0) else {

   // Deployment target is lower
   ...
}

Is this correct? Is there a better way to do this?

That code ought to work, assuming that the implementation of isDeploymentTargetAtLeast() and the implementations of the interrogated classes are in the same executable, or are all in executables compiled with the same deployment target.

···

On Feb 15, 2017, at 11:11 AM, Ole Begemann via swift-users <swift-users@swift.org> wrote:

--
Greg Parker gparker@apple.com Runtime Wrangler


(Ole Begemann) #3

In macOS 10.12 and iOS 10.0, class properties were introduced to Objective-C [1]. I noticed that the Objective-C runtime treats class properties differently based on the deployment target. Example:

  // MyClass is an Objective-C class that has a class property
  let metaclass = object_getClass(MyClass.self)
  var count: UInt32 = 0
  let properties = class_copyPropertyList(metaclass, &count)
  // Work with properties
  // Deallocate properties

When the deployment target is macOS 10.12, passing a metaclass to copyPropertyList() returns the class properties, but it returns an empty list on a lower deployment target.

That's right. The Objective-C runtime was written with the expectation that class properties would be added someday, but that code was buggy. On macOS 10.10 and iOS 8.x and older if libobjc sees a class property then it may crash. The fix was for clang and swiftc to leave class properties out of the ObjC metadata when compiling for those deployment targets.

macOS 10.11 and iOS 9.x deployment targets are both safe. (Class properties still did not exist then. Instead the bug that nobody had seen yet was unknowingly fixed when the code was rewritten for an unrelated optimization.)

Thank you, Greg. I only tested with 10.12 and 10.10, so I hadn't noticed that it would already work with 10.11.

···

On 15 Feb 2017, at 20:40, Greg Parker <gparker@apple.com> wrote:

On Feb 15, 2017, at 11:11 AM, Ole Begemann via swift-users <swift-users@swift.org> wrote:

Note that class property visibility depends on the deployment target used to compile that class's implementation. It is possible to see class properties from some classes but not others if the classes come from different executables with different deployment targets.

I'd like to perform a check (either at runtime or compile time) which of these behaviors I'll get.

I didn't find a way to perform a compile-time check in Swift against the deployment target; if #available() checks against the SDK the code is linked against. Likewise, the usual runtime check using ProcessInfo().isOperatingSystemAtLeast() doesn't check against the deployment target.

The best I came up with is this:

/**
Returns `true` if the current deployment target is at least the specified version that corresponds to the current platform.

The arguments must be passed in the form of the `__MAC_XX_X`, `__IPHONE_XX_X`, etc. constants defined in Availability.h.
*/
public func isDeploymentTargetAtLeast(
  macOS macOSVersion: Int32, iOS iOSVersion: Int32,
  tvOS tvOSVersion: Int32, watchOS watchOSVersion: Int32) -> Bool {

  #if os(macOS)
      return __MAC_OS_X_VERSION_MIN_REQUIRED >= macOSVersion
  #elseif os(iOS)
      return __IPHONE_OS_VERSION_MIN_REQUIRED >= iOSVersion
  #elseif os(tvOS)
      return __TV_OS_VERSION_MIN_REQUIRED >= tvOSVersion
  #elseif os(watchOS)
      return __WATCH_OS_VERSION_MIN_REQUIRED >= watchOSVersion
  #else
      return false
  #endif
}

For example, to test if the current deployment target is at least macOS 10.12, iOS 10.0, tvOS 10.0, or watchOS 3.0:

guard isDeploymentTargetAtLeast(
  macOS: __MAC_10_12, iOS: __IPHONE_10_0,
  tvOS: __TVOS_10_0, watchOS: __WATCHOS_3_0) else {

  // Deployment target is lower
  ...
}

Is this correct? Is there a better way to do this?

That code ought to work, assuming that the implementation of isDeploymentTargetAtLeast() and the implementations of the interrogated classes are in the same executable, or are all in executables compiled with the same deployment target.