4

JXA, with its built-in ObjC bridge, exposes enumeration and constants from the Foundation framework automatically via the $ object; e.g.:

$.NSUTF8StringEncoding  // -> 4

However, there are also useful CFString constants in lower-level APIs that aren't automatically imported, namely the kUTType* constants in CoreServices that define frequently-used UTI values, such as kUTTypeHTML for UTI "public.html".

While you can import them with ObjC.import('CoreServices'), their string value isn't (readily) accessible, presumably because its type is CFString[Ref]:

ObjC.import('CoreServices') // import kUTType* constants; ObjC.import('Cocoa') works too
$.kUTTypeHTML  // returns an [object Ref] instance - how do you get its string value?

I have yet to find a way to get at the string at the heart of what's returned: ObjC.unwrap($.kUTTypeHTML) doesn't work, and neither does ObjC.unwrap($.kUTTypeHTML[0]) (nor .deepUnwrap()).

I wonder:

  • if there's a native JXA way to do this that I'm missing.
  • otherwise, if there's away to use ObjC.bindFunction() to define bindings for CFString*() functions that can solve the problem, such as to CFStringGetCString() or CFStringGetCStringPtr(), but it's not obvious to me how to translate the ObjC signatures.
mklement0
  • 382,024
  • 64
  • 607
  • 775

3 Answers3

5

While I don't understand all implications, the following seems to work:

$.CFStringGetCStringPtr($.kUTTypeHTML, 0) // -> 'public.html'

# Alternative, with explicit UTF-8 encoding specification
$.CFStringGetCStringPtr($.kUTTypeHTML, $.kCFStringEncodingUTF8) // ditto

The kUTType* constants are defined as CFStringRef, and CFStringGetCStringPtr returns a CFString object's internal C string in the specified encoding, if it can be extracted "with no memory allocations and no copying, in constant time" - or NULL otherwise.

With the built-in constants, it seems that a C string (rather than NULL) is always returned, which - by virtue of C data types mapping onto JXA data types - is directly usable in JavaScript:

 $.CFStringGetCStringPtr($.kUTTypeHTML, 0) === 'public.html' // true

For background information (as of OSX 10.11.1), read on.


JXA doesn't natively recognize CFString objects, even though they can be "toll-free bridged" to NSString, a type that JXA does recognize.

You can verify that JXA does not know the equivalence of CFString and NSString by executing $.NSString.stringWithString($.kUTTypeHTML).js, which should return a copy of the input string, but instead fails with -[__NSDictionaryM length]: unrecognized selector sent to instance.

Not recognizing CFString is our starting point: $.kUTTypeHTML is of type CFString[Ref], but JXA doesn't return a JS string representation of it, only [object Ref].

Note: The following is in part speculative - do tell me if I'm wrong.

Not recognizing CFString has another side effect, namely when invoking CF*() functions that accept a generic type (or Cocoa methods that accept a toll-free bridged CF* type that JXA is unaware of):
In such cases, if the argument type doesn't exactly match the invoked function's parameter type, JXA apparently implicitly wraps the input object in a CFDictionary instance, whose only entry has key type, with the associated value containing the original object.[1]

Presumably, this is why the above $.NSString.stringWithString() call fails: it is being passed the CFDictionary wrapper rather than the CFString instance.

Another case in point is the CFGetTypeID() function, which expects a CFTypeRef argument: i.e., any CF* type.

Since JXA doesn't know that it's OK to pass a CFStringRef argument as-is as the CFTypeRef parameter, it mistakenly performs the above-mentioned wrapping, and effectively passes a CFDictionary instance instead:

$.CFGetTypeID($.kUTTypeHTML) // -> !! 18 (CFDictionary), NOT 7 (CFString)

This is what houthakker experienced in his solution attempt.

For a given CF* function you can bypass the default behavior by using ObjC.bindFunction() to redefine the function of interest:

// Redefine CFGetTypeID() to accept any type as-is:
ObjC.bindFunction('CFGetTypeID', ['unsigned long', [ 'void *']])

Now, $.CFGetTypeID($.kUTTypeHTML) correctly returns 7 (CFString).

Note: The redefined $.CFGetTypeID() returns a JS Number instance, whereas the original returns a string representation of the underlying number (CFTypeID value).

Generally, if you want to know the specific type of a given CF* instance informally, use CFShow(), e.g.:

$.CFShow($.kUTTypeHTML) // -> '{\n    type = "{__CFString=}";\n}'

Note: CFShow() returns nothing and instead prints directly to stderr, so you can't capture the output in JS.
You may redefine CFShow with ObjC.bindFunction('CFShow', ['void', [ 'void *' ]]) so as not to show the wrapper dictionary.

For natively recognized CF* types - those that map onto JS primitives - you'll see the specific type directly (e.g., CFBoolean for false); for unknown - and therefore wrapped - instances, you'll see the wrapper structure as above - read on for more.


[1] Running the following gives you an idea of the wrapper object being generated by JXA when passing an unknown type:

// Note: CFShow() prints a description of the type of its argument
//  directly to stderr.
$.CFShow($.kUTTypeHTML) // -> '{\n    type = "{__CFString=}";\n}'

// Alternative that *returns* the description as a JS string:
$.CFStringGetCStringPtr($.CFCopyDescription($.kUTTypeHTML), 0) // -> (see above)

Similarly, using the known-to-JXA equivalence of NSDictionary and CFDictionary,

ObjC.deepUnwrap($.NSDictionary.dictionaryWithDictionary( $.kUTTypeHTML ))

returns {"type":"{__CFString=}"}, i.e., a JS object with property type whose value is at this point - after an ObjC-bridge call roundtrip - a mere string representation of what presumably was the original CFString instance.


houthakker's solution attempt also contains a handy snippet of code to obtain the type name of a CF* instance as a string.

If we refactor it into a function and apply the necessary redefinition of CFGetTypeID(), we get the following, HOWEVER:

  • A hack is needed to make it return a value predictably (see comments and source code)
  • Even then a random character sometimes appears as the end of the string returned, such as CFString, rather than CFString.

If anyone has an explanation for why the hack is needed and where the random characters come from, please let me know. The issues may be memory-management related, as both CFCopyTypeIDDescription() and CFStringCreateExternalRepresentation() return an object that the caller must release, and I don't know whether/how/when JXA does that.

/* 
  Returns the type name of the specified CF* (CoreFoundation) type instance.
  CAVEAT:
   * A HACK IS EMPLOYED to ensure that a value is consistently returned f
     those CF* types that correspond to JS primitives, such as CFNumber, 
     CFBoolean, and CFString:
     THE CODE IS CALLED IN A TIGHT LOOP UNTIL A STRING IS RETURNED.
     THIS SEEMS TO WORK WELL IN PRACTICE, BUT CAVEAT EMPTOR.
     Also, ON OCCASION A RANDOM CHARACTER APPEARS AT THE END OF THE STRING.
   * Only pass in true CF* instances, as obtained from CF*() function
     calls or constants such as $.kUTTypeHTML. Any other type will CRASH the
     function. 

  Example:
    getCFTypeName($.kUTTypeHTML) // -> 'CFString'  
*/
function getCFTypeName(cfObj) {

  // Redefine CFGetTypeID() so that it accepts unkown types as-is
  // Caution:
  //  * ObjC.bindFunction() always takes effect *globally*.
  //  * Be sure to pass only true CF* instances from then on, otherwise
  //    the function will crash.
  ObjC.bindFunction('CFGetTypeID', [ 'unsigned long', [ 'void *' ]])

  // Note: Ideally, we'd redefine CFCopyDescription() analogously and pass 
  // the object *directly* to get a description, but this is not an option:
  //   ObjC.bindFunction('CFCopyDescription', ['void *', [ 'void *' ]])
  // doesn't work, because, since we're limited to *C* types,  we can't describe
  // the *return* type in a way that CFStringGetCStringPtr() - which expects
  // a CFStringRef - would then recognize ('Ref has incompatible type').

  // Thus, we must first get a type's numerical ID with CFGetTypeID() and then
  // get that *type*'s description with CFCopyTypeIDDescription().
  // Unfortunately, passing the resulting CFString to $.CFStringGetCStringPtr()
  // does NOT work: it yields NULL - no idea why.
  // 
  // Using $.CFStringCreateExternalRepresentation(), which yields a CFData
  // instance, from which a C string pointer can be extracted from with 
  // CFDataGetBytePtr(), works:
  //  - reliably with non-primitive types such as CFDictionary
  //  - only INTERMITTENTLY with the equivalent types of JS primitive types
  //    (such as CFBoolean, CFString, and CFNumber) - why??
  //    Frequently, and unpredictably, `undefined` is returned.
  // !! THUS, THE FOLLOWING HACK IS EMPLOYED: THE CODE IS CALLED IN A TIGHT
  // !! LOOP UNTIL A STRING IS RETURNED. THIS SEEMS TO WORK WELL IN PRACTICE,
  // !! BUT CAVEAT EMPTOR.
  //    Also, sometimes, WHEN A STRING IS RETURNED, IT MAY CONTAIN A RANDOM
  //    EXTRA CHAR. AT THE END.
  do {
    var data = $.CFStringCreateExternalRepresentation(
            null, // use default allocator
            $.CFCopyTypeIDDescription($.CFGetTypeID(cfObj)), 
            0x08000100, // kCFStringEncodingUTF8
            0 // loss byte: n/a here
        ); // returns a CFData instance
    s = $.CFDataGetBytePtr(data)
  } while (s === undefined)
  return s
}
Community
  • 1
  • 1
mklement0
  • 382,024
  • 64
  • 607
  • 775
2

You can coerce a CF type to an NS type by first re-binding the CFMakeCollectable function so that it takes 'void *' and returns 'id', and then using that function to perform the coercion:

ObjC.bindFunction('CFMakeCollectable', [ 'id', [ 'void *' ] ]);

var cfString = $.CFStringCreateWithCString(0, "foo", 0); // => [object Ref]
var nsString = $.CFMakeCollectable(cfString);            // => $("foo")

To make this easier to use in your code, you might define a .toNS() function on the Ref prototype:

Ref.prototype.toNS = function () { return $.CFMakeCollectable(this); }

Here is how you would use this new function with a CFString constant:

ObjC.import('CoreServices')

$.kUTTypeHTML.toNS() // => $("public.html")
bacongravy
  • 893
  • 8
  • 13
  • Thanks, that works, but can you explain what `CFMakeCollectable`'s true purpose is, and whether using it has any side effects in this case? And why does adding a function to the prototype of `Ref` make it available on `$.kUTTypeHtml`? The prototype-modifying approach is convenient, but also treacherous. – mklement0 Jan 26 '16 at 04:55
  • Also, `toNS()` returns a _wrapped_ string; return `$.CFMakeCollectable(this).js;` fixes that. – mklement0 Jan 26 '16 at 04:59
  • 1
    `CFMakeCollectable` makes a newly-allocated Core Foundation object eligible for garbage collection. When called in a non-garbage-collected process, it has no effect and just returns the object that was passed into it. – bacongravy Jan 29 '16 at 16:42
  • 1
    In your script, `$.kUTTypeHtml` is an instance of Ref. All functions and properties defined on `Ref.prototype` are available on instances of `Ref`. For more information, search for information on the 'JavaScript prototype inheritance chain'. – bacongravy Jan 29 '16 at 16:50
  • I appreciate your answer and explanations. Personally, given the need to call `ObjC.bindFunction` first and that I'd rather not modify prototypes, I'll stick with `$.CFStringGetCStringPtr($.kUTTypeHTML, 0)` - unless you see a problem with that. One more question: Given that `kUTTypeHTML` is a _constant_, is it still safe to call `CFMakeCollectable` on it? – mklement0 Jan 29 '16 at 16:57
  • 1
    As I said, `CFMakeCollectable` has no effect when called in a non-garbage-collected process. Its lack of effect is not dependent on the type of argument passed to it; _constant_ or otherwise. In my testing it seemed to be safe. – bacongravy Jan 29 '16 at 17:06
  • Thanks; I guess I'm not clear on how the JavaScript side of things relates to the Objective-C garbage collection in this case. – mklement0 Jan 29 '16 at 17:08
  • 2
    JavaScript is garbage collected... but the Objective-C framework that implements JavaScript for Automation does not support Objective-C garbage collection, as can be seen from the output of this command: `otool -oV /System/Library/PrivateFrameworks/JavaScriptAppleEvents.framework | tail -3`. See [this](http://stackoverflow.com/questions/3129925) question for more information. – bacongravy Jan 29 '16 at 17:11
  • 1
    Thanks again, that clarifies it (in case someone else reads this: the command was missing the final path component: should be `otool -oV /System/Library/PrivateFrameworks/JavaScriptAppleEvents.framework/JavaScriptAppleEvents | tail - 3`). – mklement0 Jan 29 '16 at 18:44
1

$.kUTTypeHTML appears to return a CFDictionary (see below), so you should find useable methods at:

EDIT: It turns out that some typing complexities in JXA-ObjC-CF interactions mean that snippet below is NOT a reliable or generally applicable approach to learning the type of a CF Object reference. (See the discussion that follows).

https://developer.apple.com/library/mac/documentation/CoreFoundation/Reference/CFDictionaryRef/

ObjC.import('CoreServices')

var data = $.CFStringCreateExternalRepresentation(
        null, 
        $.CFCopyTypeIDDescription(
            $.CFGetTypeID($.kUTTypeHTML)
        ), 
        'UTF-8',
        0
    ); // CFDataRef


cPtr = $.CFDataGetBytePtr(data);

// --> "CFDictionary"
houthakker
  • 688
  • 5
  • 13
  • Thanks for your suggestion, but ultimately (a) I wasn't able to extract any meaningful value from treating `$.kUTTypeHTML` as a `CFDictionary` instance (via `$.NSDictionary.dictionaryWithDictionary($.kUTTypeHTML)`, thanks to toll-free bridging with `NSDictionary`, which returned `"type":{__CFString=}`, from which I could extract no value), and (b) the documentation states that the actual type is `CFStringRef`, which I was eventually able to get to work in my answer - so I'm not sure that your snippet works as intended. – mklement0 Dec 02 '15 at 04:11
  • 1
    Thanks ! Your solution is very helpful. Apologies for the over optimistic diversion. – houthakker Dec 02 '15 at 20:56
  • Not sure how to fix the type reporting: `$.CFGetTypeID($.kUTTypeHTML)` returns "18" rather than "7", and returns true on CFDictionary, as if JXA gets it through one additional level of indirection. – houthakker Dec 02 '15 at 22:39
  • Perhaps this sheds light: `$.NSDictionary.dictionaryWithDictionary( $.kUTTypeHTML ) --> $({"type":$("{__CFString=}")})` – houthakker Dec 02 '15 at 23:19
  • Because this comment thread was getting too long, I've cleaned up my comments here and summarized the findings in an update to my answer. Note the use of `CFShow()` and my redefinition of `CFGetTypeID()` using `ObjC.bindFunction()` to get the former to report the true type of `$.kUTTypeHTML`; however, there are still loose ends - feel free to comment. – mklement0 Dec 03 '15 at 21:31