JXA:从CoreServices访问CFString常量 [英] JXA: Accessing CFString constants from CoreServices

查看:89

虽然我不理解所有含义,但以下内容似乎可行:

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

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

kUTType*常量定义为CFStringRefCFStringGetCStringPtr以指定的编码返回CFString对象的内部C字符串, if 可以将其提取-否则为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实例中隐式地包装输入对象,该实例的唯一条目具有键,并且关联值包含原始对象. [1]

大概是这就是上面的$.NSString.stringWithString()调用失败的原因:它是通过CFDictionary包装而不是CFString实例传递的.

另一个恰当的例子是CFGetTypeID()函数,该函数需要一个CFTypeRef自变量:即 any CF*类型.

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

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

这是 houthakker

注意: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)

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

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

返回{"type":"{__CFString=}"},即具有属性type的JS对象,其值在这一点上-在ObjC桥调用往返之后-仅 string 可能是原始CFString实例的表示形式.


houthakker的解决方案尝试还包含一个方便的代码段,用于获取类型为 name CF*实例作为字符串.

如果我们将其重构为函数并应用CFGetTypeID()的必要重新定义,则会得到以下信息:

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

如果有人对为什么需要破解以及随机字符来自何处有解释,请告诉我.这些问题可能与内存管理有关,因为CFCopyTypeIDDescription()CFStringCreateExternalRepresentation()都返回了调用者必须释放的对象,而我不知道JXA是否/何时/何时这样做./sup>

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

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.

解决方案

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
}

这篇关于JXA:从CoreServices访问CFString常量的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆