【问题标题】:JXA: Accessing CFString constants from CoreServicesJXA:从 CoreServices 访问 CFString 常量
【发布时间】:2015-10-24 04:41:17
【问题描述】:

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

$.NSUTF8StringEncoding  // -> 4

但是,在低级 API 中也有一些有用的 CFString 常量不会自动导入,即 CoreServices 中的 kUTType* 常量定义了常用的 UTI 值,例如 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())也不起作用。

我想知道:

  • 如果有本地 JXA 方法可以做到这一点,我就错过了。
  • 否则,如果无法使用ObjC.bindFunction() 定义可以解决问题的CFString*() 函数的绑定,例如CFStringGetCString()CFStringGetCStringPtr(),但我不知道如何翻译ObjC 签名.

【问题讨论】:

    标签: objective-c core-services javascript-automation


    【解决方案1】:

    虽然我不了解所有含义,但以下方法似乎有效:

    $.CFStringGetCStringPtr($.kUTTypeHTML, 0) // -> 'public.html'
    
    # Alternative, with explicit UTF-8 encoding specification
    $.CFStringGetCStringPtr($.kUTTypeHTML, $.kCFStringEncodingUTF8) // ditto
    

    kUTType* 常量定义为CFStringRefCFStringGetCStringPtr 以指定编码返回一个CFString 对象的内部C 字符串,如果 可以提取"with no memory allocations and no copying, in constant time" - 或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 是否知道CFStringNSString 的等价性,这应该返回输入字符串的副本,而是以-[__NSDictionaryM length]: unrecognized selector sent to instance 失败。

    不认识CFString 是我们的起点:$.kUTTypeHTMLCFString[Ref] 类型,但JXA 不返回它的JS 字符串表示,只返回[object Ref]

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

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

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

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

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

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

    这就是houthakkerhis solution attempt 所经历的。

    对于给定的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 原语的那些 - 您将直接看到特定类型(例如,CFBoolean 对应于 false);对于未知的 - 因此包装的 - 实例,您将看到如上所示的包装器结构 - 继续阅读以了解更多信息。


    [1] 运行以下命令可以了解在传递未知类型时由 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 ))
    

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


    houthakker's solution attempt 还包含一个方便的 sn-p 代码,用于获取 CF* 实例的类型 name 作为字符串。

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

    • 需要一个 hack 使其返回一个可预测的值(参见 cmets 和源代码)
    • 即便如此,随机字符有时也会作为返回字符串的结尾出现,例如 CFString, 而不是 CFString

    如果有人解释为什么需要破解以及随机字符来自哪里,请告诉我。这些问题可能与内存管理有关,因为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】:

      您可以将 CF 类型强制转换为 NS 类型,方法是首先重新绑定 CFMakeCollectable 函数,使其接受 'void *' 并返回 '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")
      

      【讨论】:

      • CFMakeCollectable 使新分配的核心基础对象有资格进行垃圾回收。在非垃圾回收进程中调用时,它没有任何作用,只是返回传递给它的对象。
      • 在您的脚本中,$.kUTTypeHtml 是 Ref.在Ref.prototype 上定义的所有函数和属性都在Ref 的实例上可用。如需更多信息,请搜索“JavaScript 原型继承链”信息。
      • 正如我所说,CFMakeCollectable 在非垃圾收集进程中调用时无效。它缺乏效果并不取决于传递给它的参数的类型。 常量 或其他。在我的测试中,它似乎是安全的。
      • JavaScript 被垃圾回收了...但是实现自动化 JavaScript 的 Objective-C 框架不支持 Objective-C 垃圾回收,从这个命令的输出可以看出:otool -oV /System/Library/PrivateFrameworks/JavaScriptAppleEvents.framework | tail -3。有关更多信息,请参阅this 问题。
      • 再次感谢,它澄清了它(以防其他人读到:命令缺少最终路径组件:应该是otool -oV /System/Library/PrivateFrameworks/JavaScriptAppleEvents.framework/JavaScriptAppleEvents | tail - 3)。
      【解决方案3】:

      $.kUTTypeHTML 似乎返回一个 CFDictionary(见下文),因此您应该在以下位置找到可用的方法:

      编辑:事实证明,JXA-ObjC-CF 交互中的一些打字复杂性意味着下面的 sn-p 不是学习 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"
      

      【讨论】:

      • 感谢您的建议,但最终 (a) 我无法从将 $.kUTTypeHTML 视为 CFDictionary 实例中提取任何有意义的价值(通过 $.NSDictionary.dictionaryWithDictionary($.kUTTypeHTML),感谢免费电话与NSDictionary 桥接,它返回"type":{__CFString=},我无法从中提取任何值),并且(b)文档指出实际类型是CFStringRef,我最终能够在我的答案中工作 -所以我不确定你的 sn-p 是否按预期工作。
      • 谢谢!您的解决方案非常有帮助。对过度乐观的转移表示歉意。
      • 不确定如何修复类型报告:$.CFGetTypeID($.kUTTypeHTML) 返回“18”而不是“7”,并且在 CFDictionary 上返回 true,就好像 JXA 通过一个额外的间接级别获得它一样。跨度>
      • 也许这能说明问题:$.NSDictionary.dictionaryWithDictionary( $.kUTTypeHTML ) --> $({"type":$("{__CFString=}")})
      • 因为这个评论线程太长了,我已经在这里清理了我的 cmets 并在我的答案的更新中总结了这些发现。注意CFShow()的使用和我使用ObjC.bindFunction()CFGetTypeID()的重新定义,让前者报告$.kUTTypeHTML的真实类型;但是,仍有未完成的部分 - 请随时发表评论。
      猜你喜欢
      • 1970-01-01
      • 2016-11-14
      • 2023-04-07
      • 2013-11-23
      • 1970-01-01
      • 2022-07-10
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多