Marrying JXA with ObjC

Marrying JXA with ObjC

There are a lot of things that you can do with JXA. And there is a lot as well that you cannot do, for example everything for which there are neither JavaScript methods available nor commands in the usual scripting tools like System Events. You can find a very simple example in the Basics part which writes Unicode data to a file:

ObjC.import('Foundation');
const filename = "/Users/me/Path to Folder/My File.txt";
const str = $.NSString.alloc.initWithUTF8String('Unicode Text 广州');
str.writeToFileAtomicallyEncodingError(filename,
    true, $.NSUTF8StringEncoding, null);

Although it should be possible to write UTF-8 encoded text directly to files with JavaScript (and it is in AppleScript), the above currently seems to be the only way to get that working. It is also a good example to explain some of the concepts of the JXA-ObjC binding.

Preparing to use ObjC from JXA#

To use ObjC in a JXA script, you have to import the relevant framework(s). In the example above, this is the Foundation framework, and it is imported with ObjC.import('Foundation'). That is used as an example only, as the Foundation framework is always available even if you do not import it.

Obviously, ObjC is a global object available automatically in every JXA script. Another global object is $ (Apple calls this one as well as ObjC sometimes „property“, which is not what they are). It gives you access to all ObjC classes, methods and properties of the frameworks that were imported before.

To find out what a framework provides you should consult Apple’s developer documentation. For example, the documentation for Foundation is available here. When consulting the documentation make sure that the language selector on the top is set to „Objective-C“. Otherwise, you’ll be reading the Swift documentation, which is not helpful in this context.

Calling ObjC methods from JXA#

How you call an ObjC method depends on the number of its parameters. If it has no parameter, you do not really call it, you just write its name as if it were a property. So, to create a new NSString object, you’d write let string = $.NSString.alloc, since the alloc method does not take an argument. DO NOT use empty parenthesis here, they’ll break everything.

And you do need to call alloc in this context, because you have to get an object, i.e. an instance of the class NSString. So the JavaScript line above just says „send the NSString class the message ‘alloc’ and store the result of this in my local variable string“.

But you’ll never use alloc without some variant of init. As Apple explains, „you must use an init… method to complete the initialization process“. Many of these init… methods have at least one parameter, so you cannot call them as if they were properties. Instead, you have to look up their parameter definitions. The method initWithUTF8String from above takes only one parameter, so you call it like a method passing this single parameter.

Stitching it all together, you end up with the line
const str = $.NSString.alloc.initWithUTF8String('Unicode Text 广州');
to build a JavaScript variable storing an UTF8 NSString.

If a method has more than one parameter, the real fun begins. For example, have a look at the declaration of writeToFile:atomically:encoding:error: in the documentation for NSString and compare it to the last line in the small JavaScript example above. As you can see, all the parameters are run together, the first letter of each one capitalized. Thus, writeToFile:atomically:encoding:error: in ObjC becomes writeToFileAtomicallyEncodingError in JXA-ObjC parlance. After you’ve build that monster, you provide all parameters in parenthesis like shown before.

To summarize:#

If an ObjC method

  • has no parameter, call it by simply writing it down as if it were a property. DO NOT append empty parenthesis!
  • has one parameter, call it like method(parameter).
  • has more than one parameter, look up its declaration. Uppercasing the first letter of every parameter name and removing the colons between them gives you the name of the method to call. Pass all parameters in parenthesis.
The above holds true only for ObjC methods. Functions like $.CGRectMake are called like normal JavaScript functions: The name remains exactly as it is in the ObjC documentation and the function call requires parenthesis regardless of the number of parameters.

Passing parameters between JXA and ObjC#

Simple parameter types are automatically converted from JavaScript types if possible. This works for booleans, integers and floats. For some other ObjC types, there are shorthand conversions from JavaScript:

  • $("JavaScript") produces a NSString object,
  • $([1,2,3]) produces a NSArray object,
  • $({property1: value1, property2: value2}) produces a NSDictionary object.

Conversely, these ObjC objects are converted by appending .js to them:

const str = $('Unicode Text 广州');`
/* str is a `NSString` object */
const jsString = str.js;
/* jsString is the JavaScript string 'Unicode Text 广州'*/

The longer versions of $() and .js are the methods ObjC.wrap() and ObjC.unwrap(), both taking the corresponding values as parameters.

All ObjC types that have not been mentioned so far, like NSData, have to be created and initialized using their respective ObjC methods.

ObjC methods returning structs#

If an ObjC method or function returns a struct, you access its parts like the elements of a JavaScript object:

const NSrect = $.NSRectMake(20, 40, 600, 800)
/* NSrect: 
{"origin":{"x":20,"y":40},
 "size":{"width":600,"height":800}} */

Passing references to ObjC methods#

Some ObjC methods expect a reference to an ObjC object, for example an error parameter. In that case, pass it a nil reference created like so error = $(); After the method returns, you can access the parameter like a normal JavaScript object, for example using error.code. If an ObjC method requires a reference to a scalar JavaScript type like boolean or integer, pass it an object created by the Ref() method in JXA like so let ref = Ref(); and access the value of this object as ref[0] after the method returned.

ObjC stuff not working (reliably) with JXA#

You can’t pass code blocks (aka callbacks) to ObjC methods from JXA. Or at least I do not know how this might be possible. There’s a block method defined for the global $ object in JXA, but all it seems to do is return its second argument as a string. No idea what it does to its first argument. This restriction currently prevents usage of Apple’s Speech framework from JXA.

As Stephen Kaplan pointed out to me, it is in fact possible to use JavaScript code blocks when using ObjC methods. I’m quite certain that it didn’t work in earlier versions of macOS, but in Ventura (13.6), it’s possible to pass named and anonymous functions to block parameters of ObjC methods.