fix(macos): tolerate NULL CoreText font names

EV
2d ago
+89-7

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 CTFontCopyFamilyName and CTFontCopyPostScriptName directly in the audited core_text_calls leaf, with Option<NonNull<CFString>> results, so NULL is representable.
  • Add font_family_name / font_postscript_name helpers that convert a returned reference to an owned Option<String>, and use the PostScript helper in the glyph atlas's exact-font matcher.
  • candidate_from_font skips a face that lacks either name (it cannot be matched by the resolver).
  • The generic-family path returns None when 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_matches as 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, and None (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 extern declarations, CFRetained::from_raw conversions, and fallback_family_matches logic was checked with cargo check --target aarch64-apple-darwin against objc2-core-foundation 0.3.2 / objc2-core-text 0.3.2 and 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_font as well, but the reported panic was the family name; other CoreText Copy calls (for example CTFontCopyFullName) 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.