Skip to content

Using Objective C (ObjC) with JXA

Andreas Heissenberger edited this page Mar 11, 2021 · 36 revisions
← previous · · · · · · · · · · · · · · next →

Under Construction

  • 2017-06-24 10:38 PM CT -- As the sign says, this page is under construction, and this is the FIRST, very rough, draft.

Updated 2017-07-01 7:16 PM CT

  • Awesome contribution made today by @Sancarn in the Fundamentals section.

Authors

While we are in draft mode, I'd ask that each person who contributes identify him/herself here just so all know who is working on this page. I'd love to turn over leadership of this page to a person more knowledgeable than I in JXA ObjC.


Table of Contents

  1. Purpose
  2. What is ObjC, and Why Should I use It?
  3. JXA-ObjC bridge - The Fundamentals
  4. Expanding Asynchronous Operations using the Objective-C Bridge
  5. Using the C part of the Objective-C bridge
  6. References
  7. AppleScript ObjC Examples
  8. Search Tools
  9. Related Issues

Purpose

Use of Objective-C, or "ObjC" as I'll refer to it, with JXA has very limited documentation, yet it is very powerful as we have seen with AppleScript Objective-C (ASObjC). My proposed intent for this JXA page is to provide a major reference for this subject. It probably will be a combination of links to references, and some key examples. If we get enough content, then maybe we will need some sub-pages. If you see the intent of this page differently, then please speak (write) out, and let us all know your thoughts.

I don't even have a good outline yet, so I'll just list the section titles that I see we need, without regard to order at this point. At some point we will likely need to reorder the sections.

Please contribute! I am not an expert nor experienced user with ObjC, let alone with ObjC and JXA. So, while I'll share what little I know, and what references I have, please contribute what you have.

So, I am going to list the outline as h2 paragraphs, and we can fill in the blanks as we go, and reorder at any time.

What is ObjC, and Why Should I use It?

JXA has a built-in Objective-C bridge that enables you to access the file system and Cocoa frameworks, such as Foundation and AppKit. This access is more powerful, and often much faster, that the traditional scripting-bridge that has long been used by AppleScript. In many, if not most, cases where you might normally use a reference to the Finder, System Events, or a Shell Script, JXA ObjC can provide much faster results.

JXA ObjC will also allow you to do things which are not possible otherwise. For example, using the JXA-ObjC bridge enables you design user interfaces for scripts that have the same look and feel of any other Cocoa app.

JXA-ObjC Bridge - The Fundamentals

Here we will cover the most important fundamentals you will need to get started with the JXA-ObjC bridge.

The $ Object

The $ object is the main access point for all Objective C function calls. For example:

nsStr = $.NSString.alloc.initWithUTF8String('foo')
nsStr.writeToFileAtomicallyEncodingError('/tmp/foo', true, $.NSUTF8StringEncoding, $())

writing to a file with encoding utf-8

The ObjC Object

The ObjC Object deals with the bridge and how the JavaScript engine has access to / interprets Objective C objects. An analogy to how the $ and ObjC objects work together is that the ObjC object builds the bridges which the $ object uses to walk across.

The most used part of the ObjC object is it's ability to load external frameworks. For example:

ObjC.import('Cocoa')
$.NSBeep()

By default if you tried to call the $.NSBeep() function you would receive an error, !! Error on line 1: TypeError: undefined is not a function (evaluating '$.NSBeep()'). This is because NSBeep() function is part of the Cocoa framework (Cocoa.framework). To enable you to use the NSBeep() function you need to first import Cocoa using ObjC.import() function.

Note: ObjC.import() 'installs' ALL functions and constants into the $ object. It is impossible to list all available identifiers to $ without trying all alphabetical indices.

You can also import system frameworks and custom frameworks by file path. See the frameworks section of the documentation. In some cases these functions will have to be bound to the JavaScript environment using ObjC.bindFunction()

Syntax for calling ObjC functions

Let's say we want to initiate a window object, because we are making a GUI. While looking through the documentation we find out about the init() function. How do we call this function via the ObjC-JXA bridge?

From the documentation we can see the Objective C definition for this function is as follows:

init(contentRect:styleMask:backing:defer:)

The general rule for the conversion of these functions into JXA is:

  1. Capitalise the first letter of each name separated by a :.
  2. Move the brackets to the end of the function call.

Ultimately, in this example, we get the following:

initContentRectStyleMaskBackingDefer()

In the ObjectiveC definition each 'name' before each colon represents a specific variable type that the function is expecting. We can see each of these types in the declaration:

init(contentRect: NSRect, 
styleMask style: NSWindow.StyleMask, 
backing backingStoreType: NSWindow.BackingStoreType, 
defer flag: Bool)

Ultimately:

  • The 1st parameter given to our function 'contentRect' should be of type NSRect
  • The 2nd parameter given to our function 'styleMask' should be of type NSWindow.StyleMask
  • The 3rd parameter given to our function 'backing' should be of type NSWindow.BackingStoreType
  • The 4th parameter given to our function 'defer' should be of type Boolean

When we call our function therefore we need to provide these parameters in the order they are defined in the declaration.

In Objective C the above function would be typically executed in the following manner:

NSRect frame = NSMakeRect(0, 0, 200, 200);
NSWindow* window  = [[NSWindow alloc] initWithContentRect:frame
                    styleMask:NSBorderlessWindowMask
                    backing:NSBackingStoreBuffered
                    defer:NO]

To give you a deeper understanding of the [... ...] syntax the name on the left hand side is a class, object or variable. The stuff on the right is a message which is sent to the class, object or variable on the left, causing that class to execute some function. So let's take this code apart:

[NSWindow alloc]

Here we send the 'alloc' message to the 'NSWindow' class which creates a new instance of the class, returning a NSWindow object.

[<ourNewWindowObject> init...]

Here we send the init message to the new instance of the window object. The JavaScript equivalent of this would be:

(new NSWindow).init(...)

However, in JXA, Objective-C classes are not implemented like this. Instead we do the following:

$.NSWindow.alloc.initContentRectStyleMaskBackingDefer(...)

Note: alloc is called without parentheses. If you try to call alloc as if it were a function alloc(), you will get an error: TypeError: Object is not a function. The alloc property in JXA is actually a special object. It is not a method as in Objective-C. It points back to the class but the use of .alloc. is still required to be able to call init methods.

This funky syntax utterly confuses the picture of what is really going on behind the scenes, however it's simply part of the way JXA was implemented.

To finish off, here's an example using this function from Tyler Gaw's "Building OSX apps with JS" article:

var styleMask = $.NSTitledWindowMask | $.NSClosableWindowMask | $.NSMiniaturizableWindowMask;
var windowHeight = 85;
var windowWidth = 600;
var window = $.NSWindow.alloc.initWithContentRectStyleMaskBackingDefer(
  $.NSMakeRect(0, 0, windowWidth, windowHeight),
  styleMask,
  $.NSBackingStoreBuffered,
  false
);

Passing variables by reference

Let's say we want a function which, given a filepath, determines whether:

  1. That file path exists on the filesystem.
  2. Whether that file path is a directory or an extension-less file.

Fortunately we are in luck because Objective-C provides a function exactly for doing this! That function is:

- (BOOL)fileExistsAtPath:(NSString *)path 
             isDirectory:(BOOL *)isDirectory;

In the documentation for this function we can also see an example of this function being used:

BOOL isDir;
NSFileManager *fileManager = [[NSFileManager alloc] init];
if ([fileManager fileExistsAtPath:fontPath isDirectory:&isDir] ...

Interestingly we can see the ampersand & symbol is next to the variable isDir. In Objective-C (and other C-like languages) this means we are passing the variable 'by reference' to the function. See the difference between passing variables 'by reference' and 'by value' here.

In JavaScript all variables, except objects, are passed to functions 'by value'. Passing objects to the function isn't the solution either:

isDir={}
$.NSFileManager.alloc.init.fileExistsAtPathIsDirectory("/Users/Sancarn/Desktop/D.png",isDir)
console.log(Object.keys(isDir)) //==> []

The solution to this problem, in JXA, is to use the special Ref() function provided by the JXA team.

isDir=Ref()  //set up a variable which can be passed by reference to ObjC functions.
$.NSFileManager.alloc.init.fileExistsAtPathIsDirectory("/Users/Sancarn/Desktop/D.png",isDir)
isDir[0]     //get the data from the variable

All data returned by ObjectiveC will always be in <varName>[0].

Expanding Asynchronous Operations using the Objective-C Bridge

Arguably the biggest development over the history of JavaScript has been the gradual introduction of asynchronous logic to the language from the early 2000s onward. This started with callbacks, introducing the setTimeout() method.

Compared to web JS and NodeJS, JXA has a significant lack of asynchronous timer utilities. Promise is available, but there is no native setTimeout. The only tool provided is delay(), a blocking wait function that allows nothing else to happen.

The lack of setTimeout can be polyfilled using $.NSTimer. See this StackOverFlow answer for an idea about what you need to do. Since the asynchronous execution model is kept, everything from closures to callbacks should work.

Alternatively, if you are looking to do a non-JS task asynchronously, you can simply do $.system("yourCommand &"). This tells the shell to run the command in the background. You will not be able to get the return value of the command in any callback.

Using the C part of the Objective-C bridge

The Objective-C bridge naturally has some degree of C support. A list of system headers are available in the JXA Objective-C Bridge documentation, and they should suffice for most lightweight scripting applications.

More on Ref

The Ref() function generates a proxy to handle read-write indexing of pointers. Its behavior is correct for the type it is given as a string argument. (See rule.) The readonly type property can be accessed to determine its type:

Ref('double').type // "d"
Ref('void**').type // "^^v"
Ref('NSString').type // "S"
Ref('CFString').type // "C" (this is the "rawbuffer" type)

The Ref function also accepts types of the form it produces. It crashes the entire script when it encounters invalid types such as CFNumber**, as of macOS 10.15.1. The "C" type indexes by bytes similar to a JavaScript ArrayBuffer. Unknown types are also indexed as if they are "C".

Dereferencing a Ref that points to a pointer produces another Ref:

Ref('void**')[0].type // "^v"

A new Ref object is filled with zero in its memory cell. Dereferences a NULL pointer crashes the entire script.

Using pointers

Since the void type always dereferences to undefined by Ref, all functions declared by bindFunction returning an void* returns a byte buffer view (type C) instead. For an example involving pointers and allocation, see issue 27.

ObjC.import('stdlib')
ObjC.bindFunction('malloc', ['void*', ['int']])
$.malloc(4).type    // "C"
$.malloc(4)[0].type // undefined

It is unknown whether pointers can be effectively coerced between each other and into integer types. Possible approaches to try include:

  • Type plays on methods defined by registerSubclass.
  • A custom Objective-C Framework.

Byte data can always be converted from/to ArrayBuffer in a loop. The JavaScript DataView API should make it easy to work with typed data in there.

Using external libraries

This section intends to cover the basics of C interoperability, up to the point of introducing dlopen() for open-ended extension. It is possible to import dyld, but dlfcn is not defined; bindFunction may be enough for it though. Constants and macros will need manual attention, although someone at this point should have come up with a C macro parsing toy.


References

Apple

  1. JXA Objective-C Bridge, Apple macOS 10.10 Release Notes, Updated: 2015-09-16.

Other

AppleScript ObjC Examples

Hopefully we can convert each of these into JXA ObjC scripts. Please post links to all useful ASObjC scripts that you know of.

Examples by Category

Files and Folders

Web Page Tools

1. Getting HTML Source from a URL without a Web Browser

This is very powerful, and much faster as I have found. Just set the URL of the web page, and get the HTML source code without ever opening any web browser.

JXA ObjC Script

var urlStr = "https://stackoverflow.com/";
var htmlStr = getHTMLSource(urlStr);
htmlStr.substring(0,200);


//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
function getHTMLSource(pURL) {    //  @HTML @Web @URL @OBjC
/*      Ver 1.0    2017-06-24
---------------------------------------------------------------------------------
  PURPOSE:  Get the HTML Source Code for the specified URL.
  PARAMETERS:
    • pURL    | string  |  URL of Page to get HTML of
  RETURNS:    | string  |  html source of web page
  REF:  
    1.  AppleScript Handler by @ccstone
        on getUrlSource:urlStr
—————————————————————————————————————————————————————————————————————————————————
*/

//  framework "Foundation" is built-in to JXA, and is referenced by the "$"

var nsURL     = $.NSURL.URLWithString(pURL);
var nsHTML     = $.NSData.dataWithContentsOfURL(nsURL);
var nsHTMLStr = $.NSString.alloc.initWithDataEncoding(nsHTML, $.NSUTF8StringEncoding);

var htmlStr   = ObjC.unwrap(nsHTMLStr);

return htmlStr;
  
} //~~~~~~~~~~~~~~~ END OF function getHTMLSource ~~~~~~~~~~~~~~~~~~~~~~~~~

RESULTS

"<!DOCTYPE html>\r\n<html>\r\n\r\n<head>\r\n\r\n<title>Stack Overflow</title>\r\n    <link rel=\"shortcut icon\" href=\"https://cdn.sstatic.net/Sites/stackoverflow/img/favicon.ico?v=4f32ecc8f43d\">\r\n    <link rel=\"app"

Corresponding AppleScript ObjC

  use framework "Foundation"
  use scripting additions

  set urlStr to "https://stackoverflow.com/"
  set htmlStr to my getUrlSource:urlStr

on getUrlSource:urlStr
  set theURL to current application's class "NSURL"'s URLWithString:urlStr
  set theData to current application's NSData's dataWithContentsOfURL:theURL
  set theString to current application's NSString's alloc()'s initWithData:theData encoding:(current application's NSUTF8StringEncoding)
  set theString to theString as text
  return theString
end getUrlSource:

Dealing with displays

This repository contains 4 examples of how to manipulate and harvest information from the displays. This can only be done with the ObjC libraries and are great use cases for using the JXA-ObjC bridge.

GUIs

Tyler Gaw's GUI Examples

Search Tools

Related Issues

  1. How to work with ObjC functions with pointer parameters?
  2. NSMatrix selectCell
  3. read file as class utf8
  4. Reading/setting input language ? (Requires reading CFStringRef or casting to NSString)
  5. getClipboardTypes()
← previous · · · · · · · · · · · · · · next →