High Sierra AppleScriptObjC Bugs

The latest version of macOS, High Sierra (10.13), has been released, and there are a series of bugs to look out for. Several involve AppleScriptObjC, so if you don’t use it — or any libraries or applications built with it — you will not be affected. If you do, there are coding workarounds.

  1. enum NSNotFound. This is most commonly used when getting the index of an object in an array: a result of NSNotFound means what its name implies. In 10.13.0, NSNotFound is returning the wrong value.

  2. Code that returns the frames of things like views, which in Objective-C are normally returned as type known as a struct. The specific struct is NSRect. Because AppleScript does not support the concept of structs, they are bridged to records. But in 10.13.0, NSRects are bridged to lists instead.

  3. Methods that take multiple parameters with a trailing nil, or missing value in AppleScript. These are methods like arrayWithObjects: and dictionaryWithObjectsAndKeys:. I suspect these are rarely used — there are simpler alternatives — but in 10.13.0 they are no longer supported. The best solution is likely to be to use the alternative methods, such as arrayWithArray: — the way support for all instances has been removed suggests this might be a deliberate decision.

You will probably be aware of where you might face these issues in your own code, but there is no convenient way of checking code saved as run-only. Applications that involve interfaces — those written in Xcode, or those that deal with images or custom dialogs — are highly likely to be susceptible to the NSRect bug. There’s no general indicator for the presence of the NSNotFound bug.

The root of the problems is the same in all cases. When you include a use framework statement in a script, AppleScript loads some information about the relevant framework. This includes an XML file called <framework name>.bridgesupport, which includes the names and values for all enums declared by the framework, information on how its structs are defined, and how some atypical methods need to be handled. The issues discussed here are all the result of changes to this file.

Because the issues stem from the .bridgesupport file, they will potentially pose problems in other languages that use the .bridgesupport files.

NSNotFound’s value

In the case of the enum NSNotFound, before 10.13 the entry in Foundation.bridgesupport looked like this:

<enum name='NSNotFound' value='2147483647' value64='9223372036854775807'/>

The value64 entry is the one used for 64-bit macOS. In 10.13.0 this now appears as:

<enum name='NSNotFound' value='2147483647' value64='-1'/>

Now let’s run a little code on a pre-10.13 system and see what happens:

use framework "Foundation"
current application's NSNotFound

The result appears as 9.22337203685478E+18, which is neither -1 nor 9223372036854775807. But it’s close to the latter; AppleScript does not support 64-bit integers, so the value is converted to a real, which inevitable loses some precision.

In practice, this imprecision generally doesn’t matter. For example, run this code:

use framework "Foundation"
set anArray to current application's NSArray's arrayWithArray:{1, 2, 3}
anArray's indexOfObject:4

Again, the result is 9.22337203685478E+18. So when you do this sort of thing pre-10.13, it all works:

use framework "Foundation"
set anArray to current application's NSArray's arrayWithArray:{1, 2, 3}
if (anArray's indexOfObject:4) = current application's NSNotFound then
    --
end if

The lack of precision doesn’t cause problems because it affects both values equally, and there’s no chance of confusion because it is impossible to build arrays as large as anything remotely approaching 9223372036854775807 items.

So now that we understand the issue, how can we avoid it? One way would be to avoid NSNotFound. For example, the above could also be written as:

use framework "Foundation"
set anArray to current application's NSArray's arrayWithArray:{1, 2, 3}
if (anArray's indexOfObject:4) is greater than or equal to anArray's |count|() then
    --
end if

It’s a little less efficient, but it does the same thing.

There’s another solution for Script Debugger users, and it’s one you might already have been using. That involves turning on the option to use properties for Cocoa terms in completion, and to ensure you use code-completion. When you do that, and you use code-completion to enter NSNotFound, the following code gets inserted in your script:

property NSNotFound : a reference to 9223372036854775807

And when you compile, it becomes:

property NSNotFound : a reference to 9.22337203685478E+18

This bypasses the system’s value, using the correct value stored within Script Debugger itself. So using properties for Cocoa terms, our earlier example would look like this:

use framework "Foundation"

-- classes, constants, and enums used
property NSNotFound : a reference to 9.22337203685478E+18
property NSArray : a reference to current application's NSArray

set anArray to NSArray's arrayWithArray:{1, 2, 3}
if (anArray's indexOfObject:4) is NSNotFound then
    --
end if

Now you would expect that to work. The correct value has been entered, and although it has been rounded a bit to 9.22337203685478E+18, we have already seen that the result returned by indexOfObject:4 also returns 9.22337203685478E+18. Surely they’re equal.

But it’s not that simple. Although indexOfObject:4 and NSNotFound (pre-10.13) seem to return 9.22337203685478E+18, the value they return is a bit more precise than that, and therefore closer to 9223372036854775807. It’s just that AppleScript does some rounding when presenting very large real values in scientific form.

And the catch is that AppleScript also does this rounding at compile time. So the compiler turns 9223372036854775807 to 9.22337203685478E+18, and then at run time converts the 9.22337203685478E+18 to its (more or less) exact equivalent, 922337203685478000. In short, you cannot directly create a real value with more than 15 significant digits from a string — it can only be the result of a calculation.

As of version 6.0.5, Script Debugger behaves as above, including in 10.13. The problem will be fixed in the upcoming version 6.0.6, where instead Script Debugger will insert:

property NSNotFound : a reference to 9223372036854770000 + 5807

Which will compile to:

property NSNotFound : a reference to 9.22337203685477E+18 + 5807

So then the working version of the above code will look like this:

use framework "Foundation"

-- classes, constants, and enums used
property NSNotFound : a reference to 9.22337203685477E+18 + 5807
property NSArray : a reference to current application's NSArray

set anArray to NSArray's arrayWithArray:{1, 2, 3}
if (anArray's indexOfObject:4) is NSNotFound then
    --
end if

Until the release of 6.0.6, you should manually amend the NSNotFound property value.

It’s worth noting that there is still some rounding happening here — if you change the 5807 to 5907, for example, the code will still work. But because the rounding is applied equally in all cases, and you’re never like have lists approaching anything like this value, it doesn’t matter. (For the record, AppleScript reals will accurately store integer values up to 2 ^ 53 – 1, whereas 9223372036854775807 is 2 ^ 64 – 1.)

NSRect a list, not record

The issue with NSRect is not as simple to deal with. Here’s a snippet of code that demonstrates the problem:

use framework "Foundation"
use framework "AppKit"
set theFrame to current application's NSScreen's mainScreen()'s visibleFrame()

Before 10.13, this returns a record like this:

{origin:{x:0.0, y:0.0}, |size|:{width:2524.0, height:1417.0}}

Under 10.13.0, it returns a list like this:

{{0.0, 0.0}, {2524.0, 1417.0}}

So if your code is expecting a record, and doing something like extracting the |size| of theFrame, it will now throw an error.

There are several ways of working around this, such as using a try block, or testing the class of the result. For example:

use framework "Foundation"
use framework "AppKit"
set theFrame to current application's NSScreen's mainScreen()'s visibleFrame()
if class of theFrame is record then
    set {origin:{x:theX, y:theY}, |size|:{width:theWidth, height:theHeight}} to theFrame
else
    set {{theX, theY}, {theWidth, theHeight}} to theFrame
end if

You can also use the NSRect related functions, and this is especially useful if you don’t require all four values. The functions are NSWidth(), NSHeight(), NSMinX() and NSMinY(). So if you only require the width and height, you could use something like this:

use framework "Foundation"
use framework "AppKit"
set theFrame to current application's NSScreen's mainScreen()'s visibleFrame()
set theWidth to current application's NSWidth(theFrame)
set theHeight to current application's NSHeight(theFrame)

That’s a whisker slower — although we’re only talking a small fraction of a millisecond per call — but it’s more compact and readable.

You will notice the example I have used here involves the AppKit framework, and that is where you are most likely to use methods involving NSRect. If you generally only use Foundation framework, you probably don’t have to worry.

Also, other structs — NSSize, NSPoint, NSRange, and so on — are still converted to records under 10.13.0, as always. It’s only the case of NSRect that is problematic.

Hopefully both these bugs will be fixed in 10.13.1. They’re especially irritating because the fix is so simple — minor changes to an XML file — and they were reported early in the beta cycle but still remain unfixed.

If you think you may be affected — and especially if you use applications written in AppleScriptObjC under Xcode — holding off on upgrading until they are fixed is probably the safest course of action.

The property fix in practice

As mentioned above, using Script Debugger’s ability to automatically make properties of Cocoa terms provides a potential fix for the most common of these problems, and it also offers some future-proofing. For that reason, it’s worth recapping.

First, you turn the feature on by going to Preferences -> Editor and checking Use properties for Cocoa terms in completion, clippings. You can also turn it on or off for a particular document by going to the Edit menu and choosing AppleScriptObjC -> Use Properties for Cocoa Terms.

Let’s start with a snippet of code we used earlier:

use framework "Foundation"

-- classes, constants, and enums used
property NSNotFound : a reference to 9.22337203685477E+18 + 5807 -- manually amended pre 6.0.6
property NSArray : a reference to current application's NSArray

set anArray to NSArray's arrayWithArray:{1, 2, 3}
if (anArray's indexOfObject:4) is NSNotFound then
    --
end if

At first blush, there’s an immediate problem: what if you want to copy-and-paste some code from one script to another or to post to a forum, or simple email to someone? The code you want and the property declarations might be a long way apart, and it all sounds a bit like hard work.

In fact, it’s quite simple. As long as the code is compiled, you can select the section of code you want to copy, then go to the Edit menu and choose AppleScriptObjC -> Copy as Standalone Code. You clipboard will then contain the code modified so that current application's is inserted where needed, and it will work without the property declarations. You can then paste it into another script, a forum post, or an email.

That’s all well and good, you might say, but once you paste the code into another script, won’t it be a mixture of styles? Again, the solution is simple: compile, then from the Edit menu choose AppleScriptObjC and either Migrate to Properties or Migrate to Properties in Selection. Script Debugger will clean up your code accordingly.

Ironically, one of the features of this arrangement — the fact that you can easily convert to standalone code — is why you probably won’t see it used much in sample code posted in the Script Debugger support forum and other public script forums and mailing lists. But that does not mean it’s not being used.