fix(macos): tolerate NULL CoreText font names
Problem
Neomacs 0.0.20 aborts on launch on macOS 26.7.2 (25G332, M1 Pro) while laying out the first frame:
thread 'neomacs-evaluator' panicked at objc2-core-text-0.3.2/src/generated/CTFont.rs:983:17:
function was marked as returning non-null, but actually returned NULL
...
neomacs_layout_engine::font_backend::macos::CoreTextBackend::list_candidates
neomacs_layout_engine::font::resolver::FontResolver::observe_exact_font
...
neomacs_layout_engine::gui_chrome::position_menu_items
Refs #673.
Reproduction
The report includes the full backtrace and the installed release metadata. I could not reproduce it here: this environment is Linux with no macOS host, and the failing call needs the reporter's macOS 26.7.2 font catalog. No synthetic CoreText font returning NULL for CTFontCopyFamilyName is available through the public API, so the NULL branch could not be exercised locally.
GNU Emacs Reference
This is not an Elisp-semantics change, but GNU's CoreText backend establishes the contract for a NULL name:
src/macfont.m:2783—family_name = CTFontCopyFamilyName (macfont); if (family_name) { ... }; a NULL result is treated as absent and does not abort font metrics.src/macfont.m:3759—name1 = CTFontCopyPostScriptName (font1); if (name1) { ... }; the same tolerance for the PostScript name.
So GNU treats these CoreText results as nullable, and the generated objc2 0.3.2 binding is wrong to mark them non-null (it calls expect on None).
Root Cause
objc2-core-text generates CTFont::family_name and CTFont::post_script_name with a non-null assumption and panics when CoreText returns NULL. Neomacs calls family_name from candidate_from_font while enumerating CoreText candidates, and from the generic-family and best-match fallback paths. On the reporter's catalog one face produced NULL, aborting layout instead of skipping an unusable face.
Solution
- Declare
CTFontCopyFamilyNameandCTFontCopyPostScriptNamedirectly in the auditedcore_text_callsleaf, withOption<NonNull<CFString>>results, so NULL is representable. - Add
font_family_name/font_postscript_namehelpers that convert a returned reference to an ownedOption<String>, and use the PostScript helper in the glyph atlas's exact-font matcher. candidate_from_fontskips a face that lacks either name (it cannot be matched by the resolver).- The generic-family path returns
Nonewhen the resolved font has no family name. - The best-match fallback now accepts a face only when the requested family is catalogued or CoreText resolves to that exact family; a NULL family can prove neither.
- Added
fallback_family_matchesas the pure decision for that last rule.
Regression Tests
macOS-only tests added to crates/neomacs-layout-engine/src/font_backend/macos/tests/coretext_fixture_test.rs:
fallback_family_accepts_only_available_or_exactly_resolved_names— covers available, exact (case-insensitive), different, andNone(NULL) resolutions.coretext_font_names_are_reported_as_options— exercises both new helpers against the guaranteed system Helvetica face.
These were not executed: they require a macOS host. A red-before/green-after test for the NULL branch itself is not possible without the reporter's font catalog, because CoreText's public API does not expose a way to manufacture a font whose family name is NULL. This limitation is stated rather than hidden.
Verification
Executed locally:
cargo fmt -p neomacs-layout-engine -- --check— passed.cargo check -p neomacs-layout-engine— passed (existing crate warnings only).- A throwaway crate (not part of this PR) containing the same
unsafe externdeclarations,CFRetained::from_rawconversions, andfallback_family_matcheslogic was checked withcargo check --target aarch64-apple-darwinagainstobjc2-core-foundation 0.3.2/objc2-core-text 0.3.2and passed, confirming the binding types and signatures.
Not run: the macOS-only unit tests; a full macOS build. rustup run 1.96.1 cargo check -p neomacs-layout-engine --target aarch64-apple-darwin does not reach this crate because the Nix-wrapped clang fails while building ring's C sources for the Apple target (glibc headers), an environment limitation.
Limitations
- The NULL-family path is not exercised by a test on available hardware; the fix mirrors GNU's checked behavior and the binder contract, but end-to-end confirmation needs the reporter's macOS environment.
- A NULL PostScript name is handled in
candidate_from_fontas well, but the reported panic was the family name; other CoreText Copy calls (for exampleCTFontCopyFullName) are not used in this path and were not changed. - The renderer's glyph atlas now uses the same nullable CoreText PostScript lookup (
crates/neomacs-renderer-wgpu/src/glyph_atlas/macos.rs), so a nameless collection member is skipped instead of panicking.