fix: C allocator contract, shadertoy overloads/MSL stride, minidumps
Closes the remaining library rows of the final completeness audit (docs/rewrite/notes/final-Z3.md): - libghostty-vt: OSC parser storage goes through the caller-supplied C allocator via a ByteStorage abstraction; ghostty_osc_new reports GHOSTTY_OUT_OF_MEMORY and failed grows invalidate the OSC like Zig. Tested with a counting/failing/limiting C allocator. - shadertoy: sampler parameter rewriting is keyed by full signature so overloads work; the Metal uniform block keeps std140 strides for iChannelTime[4] (MSL offsets checked against the upload layout). - tripwire.zig ported (named trip points, hit counts, verification) and used to port the previously skipped allocation-failure tests that have a fallible Rust path; the rest are documented. - crash: fatal signals capture siginfo + registers and a forked helper writes a minidump (minidump-writer) as an envelope attachment, like sentry-native/Breakpad; tests parse it with the minidump crate. - kitty LoadingImage::debug_dump (debug builds) and the Darwin Instruments signpost around benchmark runs. Test Plan: - cargo test -p ghostty-vt-capi --test c_allocator - cargo test -p ghostty-crash --test minidump
This commit is contained in:
1 parent
5568f99bae
commit
8af6ab68d3
22 files changed
+2079
-57
No files matched your search
@@ -3,12 +3,22 @@
|
||||
//! Wrapper objects handed out through the C API (parsers, events, encoders)
|
||||
//! and the SGR parameter copy are allocated with the caller's allocator when
|
||||
//! one is given and with the Rust global allocator otherwise (Zig: libc
|
||||
//! `c_allocator` / `wasm_allocator`). Interior Rust collections (e.g. the OSC
|
||||
//! parser's buffer) always use the global allocator.
|
||||
//! `c_allocator` / `wasm_allocator`). Parser-owned byte storage (the OSC
|
||||
//! payload buffer, [`CByteStorage`]) and the copies handed back to C (the OSC
|
||||
//! window title, [`CBlock`]) are allocated through the same allocator, so the
|
||||
//! caller's limits and failures govern them: a failed allocation surfaces as
|
||||
//! `GHOSTTY_OUT_OF_MEMORY` where Zig can report it (object creation) and
|
||||
//! invalidates the OSC being parsed where Zig's writer fails.
|
||||
//!
|
||||
//! Transient values built while a command is parsed (the payload copies the
|
||||
//! per-command parsers make before `ghostty_osc_end` returns) use the global
|
||||
//! allocator; nothing of them outlives that call.
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::{alloc::Layout, ffi::c_void, ptr::NonNull};
|
||||
|
||||
use ghostty_vt::osc::{ByteStorage, StorageFull};
|
||||
|
||||
/// C: `GhosttyAllocatorVtable`
|
||||
#[repr(C)]
|
||||
pub struct AllocatorVtable {
|
||||
@@ -133,3 +143,141 @@ impl Alloc {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A block of bytes allocated through an [`Alloc`] (Zig: an allocator-owned
|
||||
/// slice). Freed with the same allocator on drop.
|
||||
pub struct CBlock {
|
||||
alloc: Alloc,
|
||||
ptr: NonNull<u8>,
|
||||
cap: usize,
|
||||
}
|
||||
|
||||
impl CBlock {
|
||||
/// Allocate `cap` bytes (alignment 1); `None` when the allocator fails.
|
||||
pub fn new(alloc: Alloc, cap: usize) -> Option<CBlock> {
|
||||
let layout = Layout::from_size_align(cap, 1).ok()?;
|
||||
let ptr = NonNull::new(alloc.alloc_raw(layout))?;
|
||||
if cap > 0 {
|
||||
// SAFETY: freshly allocated `cap` bytes; zeroing makes the whole
|
||||
// block initialized so slices over it are sound.
|
||||
unsafe { ptr.as_ptr().write_bytes(0, cap) };
|
||||
}
|
||||
Some(CBlock { alloc, ptr, cap })
|
||||
}
|
||||
|
||||
/// A copy of `bytes` followed by a NUL terminator.
|
||||
pub fn from_bytes_z(alloc: Alloc, bytes: &[u8]) -> Option<CBlock> {
|
||||
let mut b = CBlock::new(alloc, bytes.len() + 1)?;
|
||||
b.as_mut_slice()[..bytes.len()].copy_from_slice(bytes);
|
||||
b.as_mut_slice()[bytes.len()] = 0;
|
||||
Some(b)
|
||||
}
|
||||
|
||||
pub fn as_ptr(&self) -> *const u8 {
|
||||
self.ptr.as_ptr()
|
||||
}
|
||||
|
||||
pub fn capacity(&self) -> usize {
|
||||
self.cap
|
||||
}
|
||||
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
// SAFETY: `ptr` is a live, zero-initialized allocation of `cap` bytes.
|
||||
unsafe { std::slice::from_raw_parts(self.ptr.as_ptr(), self.cap) }
|
||||
}
|
||||
|
||||
pub fn as_mut_slice(&mut self) -> &mut [u8] {
|
||||
// SAFETY: as above, with unique access through `&mut self`.
|
||||
unsafe { std::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.cap) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CBlock {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: allocated by `self.alloc` with this layout in `new`.
|
||||
unsafe {
|
||||
self.alloc.free_raw(
|
||||
self.ptr.as_ptr(),
|
||||
Layout::from_size_align_unchecked(self.cap, 1),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// OSC payload storage backed by the C allocator. Starts with `base` bytes
|
||||
/// (the parser's fixed buffer, allocated together with the parser so creation
|
||||
/// can fail with out-of-memory) and grows by allocate-copy-free.
|
||||
pub struct CByteStorage {
|
||||
block: CBlock,
|
||||
len: usize,
|
||||
base: usize,
|
||||
}
|
||||
|
||||
// SAFETY: the storage is uniquely owned by its parser. Moving a parser to
|
||||
// another thread is allowed under the same contract as Zig's: the caller's
|
||||
// allocator must be usable from the thread that uses the parser.
|
||||
unsafe impl Send for CByteStorage {}
|
||||
|
||||
impl std::fmt::Debug for CByteStorage {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("CByteStorage")
|
||||
.field("len", &self.len)
|
||||
.field("cap", &self.block.capacity())
|
||||
.field("base", &self.base)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl CByteStorage {
|
||||
/// `None` if the initial `base` bytes cannot be allocated.
|
||||
pub fn new(alloc: Alloc, base: usize) -> Option<CByteStorage> {
|
||||
Some(CByteStorage {
|
||||
block: CBlock::new(alloc, base)?,
|
||||
len: 0,
|
||||
base,
|
||||
})
|
||||
}
|
||||
|
||||
fn grow_to(&mut self, needed: usize) -> Result<(), StorageFull> {
|
||||
if needed <= self.block.capacity() {
|
||||
return Ok(());
|
||||
}
|
||||
let cap = needed.max(self.block.capacity().saturating_mul(2));
|
||||
let mut new = CBlock::new(self.block.alloc, cap).ok_or(StorageFull)?;
|
||||
new.as_mut_slice()[..self.len].copy_from_slice(&self.block.as_slice()[..self.len]);
|
||||
self.block = new;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ByteStorage for CByteStorage {
|
||||
fn bytes(&self) -> &[u8] {
|
||||
&self.block.as_slice()[..self.len]
|
||||
}
|
||||
|
||||
fn try_reserve(&mut self, additional: usize) -> Result<(), StorageFull> {
|
||||
self.grow_to(self.len.checked_add(additional).ok_or(StorageFull)?)
|
||||
}
|
||||
|
||||
fn try_extend(&mut self, bytes: &[u8]) -> Result<(), StorageFull> {
|
||||
self.grow_to(self.len.checked_add(bytes.len()).ok_or(StorageFull)?)?;
|
||||
self.block.as_mut_slice()[self.len..self.len + bytes.len()].copy_from_slice(bytes);
|
||||
self.len += bytes.len();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.len = 0;
|
||||
// Give back memory acquired by growth (Zig deinits the allocating
|
||||
// writer). If the small block cannot be allocated, keep the big one.
|
||||
if self.block.capacity() > self.base
|
||||
&& let Some(b) = CBlock::new(self.block.alloc, self.base)
|
||||
{
|
||||
self.block = b;
|
||||
}
|
||||
}
|
||||
|
||||
fn clone_box(&self) -> Box<dyn ByteStorage> {
|
||||
Box::new(self.bytes().to_vec())
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ use std::{
|
||||
use ghostty_vt::osc::{Command, Parser};
|
||||
|
||||
use crate::{
|
||||
alloc::{Alloc, Allocator},
|
||||
alloc::{Alloc, Allocator, CBlock, CByteStorage},
|
||||
result::GhosttyResult,
|
||||
};
|
||||
|
||||
@@ -28,9 +28,11 @@ pub struct OscParser {
|
||||
/// string payloads, as Zig's sentinel slices are) and stays valid until the
|
||||
/// next `ghostty_osc_end`/`ghostty_osc_reset`/`ghostty_osc_free`.
|
||||
pub struct OscCommand {
|
||||
command: Command,
|
||||
/// NUL-terminated window title (only for `ChangeWindowTitle`).
|
||||
title_z: Vec<u8>,
|
||||
/// `GhosttyOscCommandType` of the last command.
|
||||
key: c_int,
|
||||
/// NUL-terminated window title (only for `ChangeWindowTitle`), allocated
|
||||
/// with the caller's allocator.
|
||||
title_z: Option<CBlock>,
|
||||
}
|
||||
|
||||
/// C: `GhosttyOscCommandData`
|
||||
@@ -62,12 +64,18 @@ pub unsafe extern "C" fn ghostty_osc_new(
|
||||
let alloc = unsafe { Alloc::from_c(allocator) };
|
||||
// C-built parsers have an allocator (Zig: `.init(alloc)`), so commands
|
||||
// that need unbounded buffers (OSC 52, ...) are accepted.
|
||||
// The parser's payload buffer (Zig's inline fixed buffer / allocating
|
||||
// writer) lives in memory from the same allocator; if either allocation
|
||||
// fails the caller gets OUT_OF_MEMORY.
|
||||
let Some(storage) = CByteStorage::new(alloc, Parser::MAX_BUF) else {
|
||||
return GhosttyResult::OutOfMemory;
|
||||
};
|
||||
let Some(p) = alloc.create(OscParser {
|
||||
alloc,
|
||||
parser: Parser::init(true),
|
||||
parser: Parser::with_storage(Box::new(storage)),
|
||||
command: OscCommand {
|
||||
command: Command::Invalid,
|
||||
title_z: Vec::new(),
|
||||
key: 0,
|
||||
title_z: None,
|
||||
},
|
||||
}) else {
|
||||
return GhosttyResult::OutOfMemory;
|
||||
@@ -124,12 +132,16 @@ pub unsafe extern "C" fn ghostty_osc_end(
|
||||
return std::ptr::null_mut();
|
||||
};
|
||||
let cmd = p.parser.end(Some(terminator)).unwrap_or(Command::Invalid);
|
||||
p.command.title_z.clear();
|
||||
p.command.title_z = None;
|
||||
p.command.key = cmd.key() as c_int;
|
||||
if let Command::ChangeWindowTitle(t) = &cmd {
|
||||
p.command.title_z.extend_from_slice(t);
|
||||
p.command.title_z.push(0);
|
||||
match CBlock::from_bytes_z(p.alloc, t) {
|
||||
Some(b) => p.command.title_z = Some(b),
|
||||
// The title cannot be materialized with the caller's allocator:
|
||||
// report an invalid command rather than a dangling title.
|
||||
None => p.command.key = Command::Invalid.key() as c_int,
|
||||
}
|
||||
}
|
||||
p.command.command = cmd;
|
||||
&raw mut p.command
|
||||
}
|
||||
|
||||
@@ -141,7 +153,7 @@ pub unsafe extern "C" fn ghostty_osc_end(
|
||||
pub unsafe extern "C" fn ghostty_osc_command_type(command: *mut OscCommand) -> c_int {
|
||||
// SAFETY: caller guarantees validity (or null).
|
||||
match unsafe { command.as_ref() } {
|
||||
Some(c) => c.command.key() as c_int,
|
||||
Some(c) => c.key,
|
||||
None => 0, // invalid
|
||||
}
|
||||
}
|
||||
@@ -165,12 +177,11 @@ pub unsafe extern "C" fn ghostty_osc_command_data(
|
||||
};
|
||||
match data {
|
||||
OscCommandData::Invalid => false,
|
||||
OscCommandData::ChangeWindowTitleStr => match &command.command {
|
||||
Command::ChangeWindowTitle(_) if !out.is_null() => {
|
||||
OscCommandData::ChangeWindowTitleStr => match &command.title_z {
|
||||
Some(title) if !out.is_null() => {
|
||||
// SAFETY: caller guarantees `out` is a `const char **`.
|
||||
unsafe {
|
||||
out.cast::<*const u8>()
|
||||
.write_unaligned(command.title_z.as_ptr());
|
||||
out.cast::<*const u8>().write_unaligned(title.as_ptr());
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user