JXA:从CoreServices访问CFString常量

4

JXA通过内置的ObjC桥自动地将Foundation框架的枚举和常量暴露给$对象; 例如:

$.NSUTF8StringEncoding  // -> 4

然而,在更低级别的API中也有有用的CFString常量,它们不会自动导入,即CoreServices中的kUTType*常量,这些常量定义了经常使用的UTI值,例如UTI "public.html"kUTTypeHTML

虽然您可以使用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?

我还没有找到一种方法来获取返回值中的 字符串:

ObjC.unwrap($.kUTTypeHTML) 不起作用,ObjC.unwrap($.kUTTypeHTML[0])(以及 .deepUnwrap())也不起作用。

我想知道:

  • 是否有一种本地的JXA方法可以做到这一点,而我却没有发现。
  • 否则,如果有一种方法可以使用 ObjC.bindFunction() 来定义绑定到 CFString*() 函数的绑定,以解决问题,例如绑定到 CFStringGetCString() 或者 CFStringGetCStringPtr(),但是我不知道如何将ObjC签名翻译成相应的函数绑定。
3个回答

5
虽然我不理解所有的涵义,但以下内容似乎可以运作:
$.CFStringGetCStringPtr($.kUTTypeHTML, 0) // -> 'public.html'

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

"

kUTType*常量被定义为CFStringRefCFStringGetCStringPtr以指定编码返回CFString对象的内部C字符串,如果它可以在恒定时间内提取\"无需内存分配和复制\" - 否则返回NULL

使用内置常量时,似乎总是返回一个C字符串(而不是NULL),由于C数据类型映射到JXA数据类型,因此可直接在JavaScript中使用:

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

如需了解背景信息(截至OSX 10.11.1),请继续阅读。


JXA原生不识别CFString对象,尽管它们可以“无损桥接”到NSString类型,而这种类型JXA是认识的。您可以通过执行$.NSString.stringWithString($.kUTTypeHTML).js来验证JXA不知道CFString和NSString的等价性,该命令应返回输入字符串的副本,但实际上会失败并显示“-[__NSDictionaryM length]: unrecognized selector sent to instance”。不识别CFString是我们的起点:$.kUTTypeHTML的类型是CFString[Ref],但是JXA不会返回它的JS字符串表示,只有[object Ref]。注:以下部分内容是推测的,如果我说错了,请告诉我。
不认识CFString还会有另一个副作用,即在调用接受通用类型(或JXA不知道的过桥CF*类型的Cocoa方法)的CF*()函数时:
在这种情况下,如果参数类型与所调用的函数的参数类型不完全匹配,则JXA显然会隐式地将输入对象包装在一个CFDictionary实例中,该实例的唯一条目具有键“type”,相关值包含原始对象。[1] 据推测,这就是为什么上面的$.NSString.stringWithString()调用失败的原因:它被传递了CFDictionary包装器而不是CFString实例。
另一个例子是CFGetTypeID()函数,它期望一个CFTypeRef参数:即任何CF*类型。

由于JXA不知道直接将CFStringRef参数作为CFTypeRef参数传递是可以的,它会错误地执行上述包装操作,实际上传递了一个CFDictionary实例:

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

这是houthakker解决问题的尝试中的经历。
对于给定的CF*函数,您可以使用ObjC.bindFunction()绕过默认行为,重新定义感兴趣的函数。
// Redefine CFGetTypeID() to accept any type as-is:
ObjC.bindFunction('CFGetTypeID', ['unsigned long', [ 'void *']])

现在,$.CFGetTypeID($.kUTTypeHTML) 正确地返回 7 (CFString)。 注意:重新定义的$.CFGetTypeID()返回JS的Number实例,而原始返回底层数字的字符串表示形式(CFTypeID值)。 一般来说,如果您想非正式地知道给定CF*实例的具体类型,请使用CFShow(),例如:
$.CFShow($.kUTTypeHTML) // -> '{\n    type = "{__CFString=}";\n}'

注意:CFShow() 没有返回值,而是直接打印到 stderr,因此您无法在 JS 中捕获输出。
您可以使用 ObjC.bindFunction('CFShow', ['void', [ 'void *' ]]) 重新定义 CFShow,以便不显示包装字典。
对于本地识别的 CF* 类型 - 那些映射到 JS 原语的类型 - 您将直接看到特定类型(例如,falseCFBoolean);对于未知的 - 因此被包装的 - 实例,您将看到上面的包装结构 - 继续阅读获取更多信息。

[1] 运行以下代码可以让你对 JXA 生成的包装对象(wrapper object)有一个大致的概念,当传递一个未知类型时:

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

返回 {"type":"{__CFString=}"},即一个JS对象,其属性 type 的值是-在ObjC桥接调用“来回路程”后-仅仅是原始 CFString 实例的一个字符串表示。


这个解决方案包含一个有用的代码片段,可以将CF*实例的类型名称作为字符串获取。

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

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

如果有人能够解释为什么需要hack以及随机字符来自何处,请告诉我。问题可能与内存管理有关,因为CFCopyTypeIDDescription()CFStringCreateExternalRepresentation()都返回一个调用者必须释放的对象,我不知道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
}

2

你可以通过重新绑定CFMakeCollectable函数,使它接受'void *'类型并返回'id'类型,然后使用该函数来执行强制转换,将CF类型转换为NS类型:

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

1
CFMakeCollectable 使新分配的 Core Foundation 对象有资格进行垃圾回收。在非垃圾回收进程中调用它没有任何效果,只会返回传递给它的对象。 - bacongravy
1
在你的脚本中,$.kUTTypeHtmlRef 的一个实例。在 Ref.prototype 上定义的所有函数和属性都可用于 Ref 的实例上。要了解更多信息,请搜索“JavaScript原型继承链”的相关信息。 - bacongravy
1
正如我所说,当在非垃圾回收进程中调用时,CFMakeCollectable没有任何效果。它的无效并不取决于传递给它的参数类型,无论是常量还是其他类型。在我的测试中,它似乎是安全的。 - bacongravy
2
JavaScript是垃圾回收的...但实现JavaScript自动化的Objective-C框架不支持Objective-C垃圾回收,这可以从以下命令的输出中看出:otool -oV /System/Library/PrivateFrameworks/JavaScriptAppleEvents.framework | tail -3。有关更多信息,请参见问题。 - bacongravy
1
谢谢再次解释,这样就明确了(如果有其他人读到这个问题:命令缺少最终路径组件:应该是 otool -oV /System/Library/PrivateFrameworks/JavaScriptAppleEvents.framework/JavaScriptAppleEvents | tail - 3)。 - mklement0
显示剩余4条评论

1

$.kUTTypeHTML似乎返回一个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"

感谢您的建议,但最终我无法从将$.kUTTypeHTML视为CFDictionary实例(通过使用$.NSDictionary.dictionaryWithDictionary($.kUTTypeHTML)进行无缝桥接并返回"type":{__CFString=},其中我无法提取任何值)中提取任何有意义的价值,并且文档说明实际类型是CFStringRef,最终我能够在我的答案中使其正常工作 - 所以我不确定您的代码片段是否按预期工作。 - mklement0
1
谢谢!你的解决方案非常有帮助。对于过于乐观的偏离,我表示歉意。 - houthakker
不确定如何修复类型报告:$.CFGetTypeID($.kUTTypeHTML)返回的是“18”而不是“7”,并且在CFDictionary上返回true,就好像JXA通过一层额外的间接获取它。 - houthakker
也许这能解释一些问题:`$.NSDictionary.dictionaryWithDictionary( $.kUTTypeHTML ) --> $({"type":$("{__CFString=}")})` - houthakker
因为这个评论线程变得太长了,我在这里清理了我的评论,并在我的答案更新中总结了发现。请注意使用CFShow()和我的重新定义CFGetTypeID(),使用ObjC.bindFunction()来获取前者报告的$.kUTTypeHTML的真实类型;但是,仍然有一些未解决的问题-欢迎评论。 - mklement0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接