Wrap() and unwrap() functions for easier AppleScript use of ObjC functions

Access to ObjC library functions is facilitated, in JavaScript for Automation by the ObjC.wrap() and ObjC.unwrap functions, which automatically convert to and from the matching ObjC datatypes.

ObjC.wrap() in JS can also be used in a syntactic sugar abbreviation to the $() operator, so that we can simply write:

$(strPath).stringByStandardizingPath

Where $() converts a JS string to an NSString object.

Similarly we can abbreviate the use of ObjC.unwrap by simply appending the three-character affix .js to an ObjC object, to convert it straight back to the appropriate JavaScript data type.

For some reason, however, AppleScript has not been provided with out-of-the-box wrap() or unwrap() functions, or with any equivalent syntactic sugaring.

Here is one approach to facilitating AS access to ObjC libraries by writing wrap() and unwrap() for ourselves.

We can start a script with:

use framework "Foundation"

and then include these two functions

-- wrap :: AS value -> NSObject
on wrap(v)
    set ca to current application
    ca's (NSArray's arrayWithObject:v)'s objectAtIndex:0
end wrap

-- unwrap :: NSObject -> AS value
on unwrap(objCValue)
    if objCValue is missing value then
        return missing value
    else
        set ca to current application
        return item 1 of ((ca's NSArray's arrayWithObject:objCValue) as list)
    end if
end unwrap

(These functions benefit from an insight by Shane Stanley, expounded on the AppleScript users list in response to a question by JMichaelTX, that we can delegate the selection of matching datatypes to methods of NSArray)

With these two helper functions we can, for example, replace the more verbose expansion of a file path tilde, usually something like:

use framework "Foundation"
set strPath to "~/Desktop"
set oPath to (current application's NSString's stringWithString:strPath)'s ¬
	stringByStandardizingPath

which leads to a value like «class ocid» id «data optr0000000020BCE14DDA7F0000» and still needs to be converted back to an AppleScript string

Instead, with our own wrap() and unwrap(), we can more simply write:

set strFullPath to unwrap(wrap("~/Desktop")'s stringByStandardizingPath)

Which immediately gives an AppleScript result like:

"/Users/houthakker/Desktop"

Here is a fuller example, which uses wrap() and unwrap() to simplify use of an ObjC contentsOfDirectoryAtPath() function.

use framework "Foundation"

-- Example of simplified access from AppleScript to ObjC functions
-- using generic wrap(ASObject) and unwrap(NSObject) functions

-- (In the manner of JavaScript for Automation)

on run
    set strFullPath to unwrap(wrap("~/Desktop")'s stringByStandardizingPath)
    
    return directoryContents(strFullPath)
end run

-- wrap :: AS value -> NSObject
on wrap(v)
    set ca to current application
    ca's (NSArray's arrayWithObject:v)'s objectAtIndex:0
end wrap

-- unwrap :: NSObject -> AS value
on unwrap(objCValue)
    if objCValue is missing value then
        return missing value
    else
        set ca to current application
        return item 1 of ((ca's NSArray's arrayWithObject:objCValue) as list)
    end if
end unwrap

-- directoryContents :: Unix Path String -> [FileName]
on directoryContents(strPath)
    set ca to current application
    set strFullPath to wrap(strPath)'s stringByStandardizingPath
    
    filter(my notDotted, unwrap(ca's NSFileManager's defaultManager's ¬
        contentsOfDirectoryAtPath:strFullPath |error|:(missing value)))
end directoryContents

on notDotted(strPath)
    text item 1 of strPath is not "."
end notDotted


-- filter :: (a -> Bool) -> [a] -> [a]
on filter(f, xs)
    set mf to mReturn(f)
    
    set lst to {}
    set lng to length of xs
    repeat with i from 1 to lng
        set v to item i of xs
        if mf's lambda(v, i, xs) then
            set end of lst to v
        end if
    end repeat
    return lst
end filter

-- Lift 2nd class function into 1st class wrapper 
-- handler function --> first class script object
on mReturn(f)
    if class of f is script then return f
    script
        property lambda : f
    end script
end mReturn

5 Likes

Similarly, by adding wrap() and unwrap() to the file, we can simplify our AppleScript versions of writePlist() and readPlist()

use framework "Foundation"

-- writePlist :: Object -> String -> IO ()
on writePlist(asObject, strPath)
	set cClass to class of asObject
	
	if (cClass is record or cClass is list) then
		set objcObject to wrap(asObject)
	else
		return missing value
	end if
	
	(objcObject)'s ¬
		writeToFile:(wrap(strPath)'s ¬
			stringByStandardizingPath()) atomically:true
end writePlist

-- readPlist :: String -> Object
on readPlist(strPath)
	set oPath to wrap(strPath)'s ¬
		stringByStandardizingPath()
	
	set ca to current application
	set maybeDict to (ca's NSDictionary's dictionaryWithContentsOfFile:oPath)
	if (maybeDict is not missing value) then return maybeDict as record
	
	set maybeArray to (ca's NSArray's arrayWithContentsOfFile:oPath)
	if (maybeArray is not missing value) then return maybeArray as list
end readPlist


-- wrap :: AS value -> NSObject
on wrap(v)
	set ca to current application
	ca's (NSArray's arrayWithObject:v)'s objectAtIndex:0
end wrap

-- unwrap :: NSObject -> AS value
on unwrap(objCValue)
	if objCValue is missing value then
		return missing value
	else
		set ca to current application
		item 1 of ((ca's NSArray's arrayWithObject:objCValue) as list)
	end if
end unwrap
3 Likes

Thank you — the more straightforward illustrative examples of using both Objective-C and JXA the better. This is turning into a site for some serious multi-language scripting information, resources, and discussions. I hope @peternlewis doesn’t mind the co-option.

1 Like

Final example: plugging a gap to provide AppleScript with equivalents of two basic JavaScript string methods:

  • toUpperCase()
  • toLowerCase()

Both of these functions are, of course, provided by Keyboard Maestro actions, but it can be useful to have them inline inside an execute script action, for example in the context of getting a case-insensitive string equality check by first normalising each string to the same case.

use framework "Foundation"

on run
    {toUpperCase("Straße"), toLowerCase("HELLO")}
end run

-- toUpperCase :: String -> String
on toUpperCase(str)
    set ca to current application
    unwrap(wrap(str)'s ¬
        uppercaseStringWithLocale:(ca's NSLocale's currentLocale))
end toUpperCase

-- toLowerCase :: String -> String
on toLowerCase(str)
    set ca to current application
    unwrap(wrap(str)'s ¬
        lowercaseStringWithLocale:(ca's NSLocale's currentLocale))
end toLowerCase


-- wrap :: AS value -> NSObject
on wrap(v)
    set ca to current application
    ca's (NSArray's arrayWithObject:v)'s objectAtIndex:0
end wrap

-- unwrap :: NSObject -> AS value
on unwrap(objCValue)
    if objCValue is missing value then
        return missing value
    else
        set ca to current application
        item 1 of ((ca's NSArray's arrayWithObject:objCValue) as list)
    end if
end unwrap
1 Like