JXA:从 CoreServices 访问 CFString 常量

JXA: Accessing CFString constants from CoreServices

JXA 及其内置的 ObjC 桥,通过 $ 对象自动公开 Foundation 框架中的枚举和常量;例如:

$.NSUTF8StringEncoding  // -> 4

但是,在较低级别的 API 中也有一些有用的 CFString 常量不会自动导入,即 CoreServices 中的 kUTType* 常量定义了常用的 UTI values, such as kUTTypeHTML 对于 UTI "public.html".

虽然您可以 导入 它们 ObjC.import('CoreServices'),但它们的 字符串值 无法(轻易)访问,大概是因为它的类型是 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?

我还没有找到一种方法来获取返回内容的核心 stringObjC.unwrap($.kUTTypeHTML) 不起作用,ObjC.unwrap($.kUTTypeHTML[0]) 也不起作用(.deepUnwrap())。

我想知道:

$.kUTTypeHTML 出现在 return CFDictionary(见下文)中,因此您应该在以下位置找到可用的方法:

编辑:事实证明,JXA-ObjC-CF 交互中的一些类型复杂性意味着下面的代码片段不是学习 CF 对象引用类型的可靠或普遍适用的方法。 (请参阅下面的讨论)。

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"

虽然我不明白所有含义,但以下似乎有效:

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

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

kUTType*常量定义为CFStringRefCFStringGetCStringPtrreturn是CFString对象的指定编码的内部C字符串,if 可以提取 "with no memory allocations and no copying, in constant time" - 或者 NULL 否则。

使用内置常量,C 字符串(而不是 NULL)似乎总是 returned,这 - 由于 C 数据类型映射到 JXA 数据类型 -可直接用于 JavaScript:

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

关于背景信息(截至OSX 10.11.1),继续阅读。


JXA 本身不能识别 CFString 对象,即使它们可以是 "toll-free bridged" 到 NSString,JXA 可以识别的类型 认识。

您可以通过执行 $.NSString.stringWithString($.kUTTypeHTML).js 来验证 JXA 知道 CFStringNSString 的等价性,应该 return 输入字符串的副本,但失败并显示 -[__NSDictionaryM length]: unrecognized selector sent to instance.

不识别 CFString 是我们的出发点:$.kUTTypeHTMLCFString[Ref] 类型,但 JXA 不 return 一个 JS 的字符串表示,只有 [object Ref].

注意:以下部分是推测 - 如果我错了请告诉我。

不识别 CFString 有另一个副作用,即当调用 CF*() 函数接受 generic 类型(或 Cocoa 方法时接受 JXA 不知道的免费桥接 CF* 类型):
在这种情况下,如果参数类型与被调用函数的参数类型不完全匹配,JXA 显然隐式地包装 CFDictionary 实例中的输入对象,其唯一条目具有键 type,关联值包含原始对象。[1]

据推测,这就是上述 $.NSString.stringWithString() 调用失败的原因:它被传递给 CFDictionary 包装器而不是 CFString 实例。

另一个例子是 CFGetTypeID() 函数,它需要一个 CFTypeRef 参数:即 any CF* 类型。

由于 JXA 不知道可以按原样传递 CFStringRef 参数作为 CFTypeRef 参数,因此它错误地执行了上述包装,并有效地传递了 CFDictionary 实例改为:

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

这就是houthakker experienced in .

对于给定的 CF* 函数,您可以 绕过 默认行为,方法是使用 ObjC.bindFunction() 重新定义 感兴趣的函数:

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

现在,$.CFGetTypeID($.kUTTypeHTML) 正确 returns 7 (CFString).

注意:重新定义的$.CFGetTypeID()return是一个JSNumber实例,而原来的return是一个字符串 表示基础数字(CFTypeID 值)。

通常,如果您想非正式地了解给定 CF* 实例的具体类型,请使用 CFShow(),例如:

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

注意:CFShow() return什么都没有,而是直接打印到 stderr,所以你无法捕获输出JS.
您可以将 CFShow 重新定义为 ObjC.bindFunction('CFShow', ['void', [ 'void *' ]]) 以便不显示包装器字典。

对于本地识别的 CF* 类型——那些映射到 JS 基元的类型——你会直接看到特定类型(例如,CFBoolean 代表 false);对于未知的 - 因此被包装的 - 实例,您将看到如上所述的包装器结构 - 请继续阅读以了解更多信息。


[1] 运行 以下为您提供 idea 正在生成的包装器对象通过 JXA 传递未知类型时:

// 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)

类似地,使用已知的 JXA 等价 NSDictionaryCFDictionary

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

returns {"type":"{__CFString=}"},即具有属性 type 的 JS 对象,其值在此时 - 在 ObjC-bridge 调用之后 roundtrip - 仅仅是 string 表示可能是原始 CFString 实例。


还包含一个方便的代码片段,用于获取 CF* 实例的类型 name 作为字符串。

如果我们将其重构为一个函数并应用 CFGetTypeID() 的必要重新定义,我们将得到以下内容,但是:

  • 需要 hack 才能使其 return 成为可预测的值(参见注释和源代码)
  • 即便如此,随机字符有时也会出现在 returned 字符串的末尾,例如 CFString, 而不是 CFString

如果有人能解释为什么需要破解以及随机字符的来源,请告诉我。这些问题可能与内存管理有关,因为 CFCopyTypeIDDescription()CFStringCreateExternalRepresentation() return 都是 caller 必须释放的对象,而我没有知道 whether/how/when JXA 是这样做的。

/* 
  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
}

您可以通过首先 re-binding CFMakeCollectable 函数将 CF 类型强制转换为 NS 类型,这样它需要 'void *' 和 returns 'id',然后使用它执行强制转换的函数:

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

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

为了使其更易于在您的代码中使用,您可以在 Ref 原型上定义一个 .toNS() 函数:

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

下面是如何使用这个带有 CFString 常量的新函数:

ObjC.import('CoreServices')

$.kUTTypeHTML.toNS() // => $("public.html")