Skip to content

ui: dedupe 248 copies of str_from_header (returns &'static str over a movable GC payload) into one shared reader #8430

Description

@proggeramlug

Problem

The eight UI crates carry 248 copy-pasted definitions of str_from_header with 766 call sites (ui-macos 44 defs/118 calls, ui-ios 41/94, ui-visionos 37/76, ui-windows 36/105, ui-gtk4 32/98, ui-tvos 27/55, ui-android 21/113, ui-watchos 6/37; plus 1 in perry-audio-miniaudio). Every copy open-codes the string layout and returns a &'static str manufactured over a movable GC payload:

// e.g. crates/perry-ui-macos/src/app.rs:63-73 — repeated ~248 times across the tree
fn str_from_header(ptr: *const u8) -> &'static str {
    if ptr.is_null() { return ""; }
    unsafe {
        let header = ptr as *const crate::string_header::StringHeader;
        let len = (*header).byte_len as usize;
        let data = ptr.add(std::mem::size_of::<crate::string_header::StringHeader>());
        std::str::from_utf8_unchecked(std::slice::from_raw_parts(data, len))
    }
}

Problems: (1) the 'static lifetime erases the GC hazard — Perry strings are relocated by the copying minor, and a borrow held across any allocation through the runtime dangles (the "borrowed heap slice" class, crates/perry-runtime/src/object/field_get_set.rs:11-27); (2) 248 copies of the layout means any StringHeader change is a 248-site flag day; (3) ui-macos keeps its own struct mirror (crates/perry-ui-macos/src/string_header.rs:5) "to avoid a dep cycle", and perry-audio-miniaudio another (crates/perry-audio-miniaudio/src/lib.rs:32).

Suggested fix

  1. One shared helper. Options, in preference order: (a) perry-ffi (it already mirrors StringHeader at types.rs:34 and exposes read_string; check whether the UI crates can depend on it without a cycle — it is a leaf crate, so they should); (b) a tiny new perry-string-abi crate holding the #[repr(C)] struct + reader if perry-ffi drags unwanted deps.
  2. Return an owned String, not a borrow. UI FFI entry points are not hot paths; String::from_utf8_lossy(...).into_owned() (or unchecked + .to_string() where WTF-8 is impossible) kills the entire 'static hazard class in the same stroke. Most call sites immediately convert to NSString/owned data anyway.
  3. Migrate mechanically, crate by crate (this can land as one PR per UI crate — say so in each PR title). Delete the local struct mirrors and point at the shared one, keeping a const size assert (== 20) at the shared definition.

Validation

  • Cross-host UI crates don't build on every host — validate what the host can: on macOS, cargo check -p perry-ui-macos -p perry-ui-ios -p perry-ui-tvos ... (check-only works for cross targets where full builds don't; if a crate can't even check on the host, note it in the PR and let CI's matrix cover it).
  • At least one behavioral smoke on the host platform (a perry/ui example compiles and shows a window with a non-ASCII title, exercising the reader).
  • Grep-count str_from_header definitions after: expect ~0 in migrated crates. If the byte-access ratchet script (filed separately) has landed, its baseline update is the proof of progress.

Workflow

PR = code + tests + changelog.d/<PR>-<slug>.md; no version bump (maintainer bumps at merge). Note the 2000-line file cap (scripts/check_file_size.sh) when touching large UI files.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew capability or improvement

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions