swiftui-native 0.0.1

SwiftUI from pure Rust — no Swift bridge, no shim, only swift-runtime calls
Documentation
//! Diagnostics — human-readable Swift type names for errors and debug output.

use core::ffi::c_void;

/// Demangle a Swift symbol name to human-readable form.
/// Falls back to the raw mangled name if demangling fails.
pub fn demangle(mangled: &str) -> String {
    // swift_demangle expects the $s prefix for type manglings
    let prefixed = if mangled.starts_with("$s") || mangled.starts_with("_$s") {
        mangled.to_string()
    } else {
        format!("$s{mangled}")
    };
    let Ok(cstr) = std::ffi::CString::new(prefixed.as_bytes()) else {
        return mangled.to_string();
    };
    swift_runtime::demangle::demangle(&cstr).unwrap_or_else(|| mangled.to_string())
}

/// Demangle a mangled type name byte slice (as used in our MANGLED constants).
pub fn demangle_type(mangled: &[u8]) -> String {
    let s = std::str::from_utf8(mangled).unwrap_or("<invalid utf8>");
    demangle(s)
}

/// Get the human-readable name of a Swift type from its metadata pointer.
///
/// `metadata` must be type metadata the runtime produced, or null — null is
/// checked, because an empty slot is a common enough result to be worth naming in
/// a message rather than crashing on. Anything else is read as metadata.
///
/// Deliberately **not** `unsafe`, against `clippy::not_unsafe_ptr_arg_deref`.
/// This exists to go inside `assert!` and `panic!` messages, and every one of its
/// callers is already in unsafe code, holding metadata it has just been handed by
/// the runtime. Marking it `unsafe` would put an `unsafe { … }` inside a hundred
/// assertion messages and change no obligation — the precondition above is the
/// obligation, and it is the caller's either way. [`views::dynamic_type_name`]
/// is the form to reach for from safe code: a `ViewHandle` cannot hold anything
/// but metadata.
///
/// [`views::dynamic_type_name`]: crate::views::dynamic_type_name
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub fn type_name(metadata: *const c_void) -> String {
    if metadata.is_null() {
        return "<null>".to_string();
    }
    unsafe {
        match swift_runtime_sys::SwiftCCThunks::swift_getTypeName(metadata, true) {
            Ok((name, len)) if len > 0 => name.to_string(),
            _ => format!("<metadata {metadata:?}>"),
        }
    }
}

/// Demangle a dlsym symbol name for display in error messages.
pub fn symbol_name(sym: &core::ffi::CStr) -> String {
    let raw = sym.to_str().unwrap_or("<invalid>");
    let demangled = demangle(raw);
    if demangled == raw {
        raw.to_string()
    } else {
        format!("{demangled} ({raw})")
    }
}

/// A full recursive description of any Swift value, as `dump` writes it.
///
/// Swift's `dump` is generic over the value's type, so with the metadata in
/// hand it reflects anything — including a framework's private structure. That
/// is what it is for here: seeing what a `ShaderLibrary` or a `Font` is
/// actually made of, without a header to read.
///
/// Addresses are left in; [`crate::views::normalise_dump`] takes them out when
/// two dumps have to be compared.
///
/// # Safety
/// `value` must point to a live value of type `metadata`.
pub unsafe fn dump_any(value: *mut c_void, metadata: *const c_void, max_depth: u64) -> String {
    use crate::{abi, resolve};
    unsafe {
        let _pool = crate::objc::AutoreleasePool::new();
        // `dump<T, TargetStream: TextOutputStream>(_:to:name:indent:maxDepth:maxItems:) -> T`
        //   x0 = @in_guaranteed T, x1 = inout TargetStream, x2/x3 = name (String?),
        //   x4 = indent, x5 = maxDepth, x6 = maxItems, x7 = T.Type,
        //   [sp] = TargetStream.Type, [sp+8] = TargetStream: TextOutputStream WT,
        //   x8 = @out T
        let mut target = abi::swift_string("");
        let name = abi::swift_string("v");
        let n0 = u64::from_le_bytes(name[0..8].try_into().expect("name lo"));
        let n1 = u64::from_le_bytes(name[8..16].try_into().expect("name hi"));
        // `dump` returns its argument, so the result buffer has to be the
        // value's size, not a word.
        let mut returned = vec![0u8; abi::type_bytes(metadata)];
        core::arch::asm!(
            "sub sp, sp, #32",
            "stp {sm}, {wt}, [sp]",
            "blr {f}",
            "add sp, sp, #32",
            f = in(reg) resolve::dump_fn(),
            sm = in(reg) resolve::string_meta(),
            wt = in(reg) resolve::string_text_output_wt(),
            in("x0") value,
            in("x1") target.as_mut_ptr(),
            in("x2") n0,
            in("x3") n1,
            in("x4") 0u64,
            in("x5") max_depth,
            in("x6") 400u64,
            in("x7") metadata,
            in("x8") returned.as_mut_ptr(),
            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
            lateout("x17") _, lateout("lr") _, lateout("x20") _, lateout("x21") _,
            clobber_abi("C"),
        );
        // `dump` hands back its argument at +1. Nothing wants that copy, and
        // the harness calls this once per case, so let it go rather than leak
        // one per dump.
        let vwt = &*swift_runtime_sys::SwiftABI::get_value_witness_table(metadata);
        (vwt.destroy)(returned.as_mut_ptr() as *mut c_void, metadata);

        let text = abi::read_swift_string(target);
        // The target string was built here and nothing else refers to it.
        let string_meta = resolve::string_meta();
        let string_vwt = &*swift_runtime_sys::SwiftABI::get_value_witness_table(string_meta);
        (string_vwt.destroy)(target.as_mut_ptr() as *mut c_void, string_meta);
        text
    }
}