What a difference between NSFont and CTFont and why CTFont hangs-up NSTextFieldCell?

542 views Asked by At

I want to show fonts in NSTableView. If I use fonts inited by NSFont(name: fontName, size: size) everything is OK. But In this case I can use only fonts installed in system. So I made an NSFont Extension:

public extension NSFont {
    static func read(from path: String, size: CGFloat) throws -> NSFont {
        guard let dataProvider = CGDataProvider(filename: path) else {
            throw NSError(domain: "file not found", code: 77, userInfo: ["fileName" : path])
        }
        guard let fontRef = CGFont ( dataProvider ) else {
            throw NSError(domain: "Not a font file", code: 77, userInfo: ["fileName" : path])
        }
        return CTFontCreateWithGraphicsFont(fontRef, size, nil, nil) as NSFont
    }
}

It seems to work, fonts made this way founds their place in [NSFont] arrays. But if try to bind them to NSTextFieldCell font in NSTableView program explodes:

(this goes forever and throws Thread 1: EXC_BAD_ACCESS (code=2, address=0x7ffeef3ffff8))
......
......
#261509 0x00007fff6c023e63 in -[NSCTFont isEqual:] ()
#261510 0x00007fff4539908c in _CFNonObjCEqual ()
#261511 0x00007fff6c023e63 in -[NSCTFont isEqual:] ()
#261512 0x00007fff4539908c in _CFNonObjCEqual ()
#261513 0x00007fff6c023e63 in -[NSCTFont isEqual:] ()
#261514 0x00007fff6bfccf63 in -[NSAttributeDictionary isEqualToDictionary:] ()
#261515 0x00007fff6bfccc11 in attributeDictionaryIsEqual ()
#261516 0x00007fff475c6712 in hashProbe ()
#261517 0x00007fff475c6518 in -[NSConcreteHashTable getItem:] ()
#261518 0x00007fff6bfc5be8 in +[NSAttributeDictionary newWithDictionary:] ()
#261519 0x00007fff6bff7cbe in -[_NSCachedAttributedString initWithString:attributes:] ()
#261520 0x00007fff6bfdac1f in __NSStringDrawingEngine ()
#261521 0x00007fff6bff7380 in _NSStringDrawingCore ()
#261522 0x00007fff42b16477 in _NSDrawTextCell2 ()
#261523 0x00007fff42b15328 in __45-[NSTextFieldCell _drawForegroundOfTextLayer]_block_invoke ()
#261524 0x00007fff42a8c529 in -[NSFocusStack performWithFocusView:inWindow:usingBlock:] ()
#261525 0x00007fff42b14bff in -[NSTextFieldCell _drawForegroundOfTextLayer] ()
#261526 0x00007fff42b1445a in -[NSTextFieldCell updateLayerWithFrame:inView:] ()
#261527 0x00007fff42b14322 in -[NSControl updateLayer] ()
#261528 0x00007fff42afe301 in _NSViewUpdateLayer ()
......
......

I think there is something missing in CTFont what NSFont has. But what?

2

There are 2 answers

0
Łukasz On

I didn’t found an answer. But because infinite loop started when I tried to use NSPredicate, on comparision NSCTFont isEqual:, I made a controllers for fonts and used them to compare by NSPredicate. No infinite loop anymore.

1
rob mayoff On

You're not doing anything wrong. This is a bug in macOS.

You can cast a CTFont to NSFont because these types are “toll-free bridged”. This means a CTFont is laid out in memory in a way that matches the requirements of Objective-C instances. One of those requirements is that the first word of the object contain a pointer (called the “isa” pointer) to the object's class. In the case of a CTFont, that class is named NSCTFont, and it is a subclass of NSFont.

NSCTFont (defined in the UIFoundation private framework) overrides the isEqual: method. If you look at a disassembly of that function (and if you understand x86 assembly), you'll see that it is defined roughly like this:

- (BOOL)isEqual:(NSObject *)other {
    if (other == 0) { return NO; }
    if (other == self) { return YES; }
    return _CFNonObjCEqual(self, other);
}

So, if the objects are not obviously different (because other is nil) and are not obviously the same (because they are the same pointer), then this isEqual: method calls _CFNonObjCEqual, which is a private Core Foundation function. It so happens that _CFNonObjCEqual is part of the open source release of Core Foundation, so we can look at its implementation:

Boolean _CFNonObjCEqual(CFTypeRef cf1, CFTypeRef cf2) {
    //cf1 is guaranteed to be non-NULL and non-ObjC, cf2 is unknown
    if (cf1 == cf2) return true;
    if (NULL == cf2) { CRSetCrashLogMessage("*** CFEqual() called with NULL second argument ***"); HALT; }
    CFTYPE_OBJC_FUNCDISPATCH1(Boolean, cf2, isEqual:, cf1);
    CFTYPE_SWIFT_FUNCDISPATCH1(Boolean, cf2, NSObject.isEqual, (CFSwiftRef)cf1);
    __CFGenericAssertIsCF(cf1);
    __CFGenericAssertIsCF(cf2);
    if (__CFGenericTypeID_inline(cf1) != __CFGenericTypeID_inline(cf2)) return false;
    if (NULL != __CFRuntimeClassTable[__CFGenericTypeID_inline(cf1)]->equal) {
        return __CFRuntimeClassTable[__CFGenericTypeID_inline(cf1)]->equal(cf1, cf2);
    }
    return false;
}

The comment tells us what's expected: the cf1 argument must be known to be a Core Foundation type that's not a native Objective-C instance, but the cf2 argument might be a native Objective-C instance.

The line that matters is this one:

    CFTYPE_OBJC_FUNCDISPATCH1(Boolean, cf2, isEqual:, cf1);

This is a C macro, and we don't get to see its real definition because the real definition has been removed from the open source release. But we can guess that it probably expands to something like this:

if ([cf2 respondsToSelector:@selector(isEqual:)]) {
    return [cf2 isEqual:cf1];
}

And that is going to be a problem if cf2 is also an NSCTFont, because then it's recursively calling -[NSCTFont isEqual:] (with the arguments swapped), which will call back in to _CFNonObjCEqual, ad nauseum (until you get the sort of stack overflow for which this web site is named).

You can file a bug report at https://feedbackassistant.apple.com/, or using the Feedback Assistant app, if you wish.