use std::cell::Cell;
thread_local! {
static LOW: Cell<usize> = const { Cell::new(0) };
}
#[inline(never)]
fn sp() -> usize {
let marker = 0u8;
std::hint::black_box(&marker) as *const u8 as usize
}
#[cfg(target_os = "linux")]
fn stack_low() -> Option<usize> {
#[repr(C, align(16))]
struct Attr([u8; 128]);
extern "C" {
fn pthread_self() -> usize;
fn pthread_getattr_np(thread: usize, attr: *mut Attr) -> i32;
fn pthread_attr_getstack(attr: *const Attr, addr: *mut *mut u8, size: *mut usize) -> i32;
fn pthread_attr_destroy(attr: *mut Attr) -> i32;
}
let mut attr = Attr([0; 128]);
let mut addr: *mut u8 = std::ptr::null_mut();
let mut size = 0usize;
unsafe {
if pthread_getattr_np(pthread_self(), &mut attr) != 0 {
return None;
}
let r = pthread_attr_getstack(&attr, &mut addr, &mut size);
pthread_attr_destroy(&mut attr);
if r != 0 || addr.is_null() {
return None;
}
}
Some(addr as usize)
}
#[cfg(windows)]
fn stack_low() -> Option<usize> {
#[link(name = "kernel32")]
extern "system" {
fn GetCurrentThreadStackLimits(low: *mut usize, high: *mut usize);
}
let (mut low, mut high) = (0usize, 0usize);
unsafe { GetCurrentThreadStackLimits(&mut low, &mut high) };
(low != 0).then_some(low + 64 * 1024)
}
#[cfg(not(any(target_os = "linux", windows)))]
fn stack_low() -> Option<usize> {
None
}
fn low() -> usize {
LOW.with(|l| {
if l.get() == 0 {
let v = stack_low().unwrap_or_else(|| sp().saturating_sub(1 << 20));
l.set(v.max(1));
}
l.get()
})
}
pub fn remaining() -> usize {
sp().saturating_sub(low())
}
pub fn floor(red_zone: usize) -> usize {
low().saturating_add(red_zone)
}
#[inline(always)]
pub fn here() -> usize {
let marker = 0u8;
std::hint::black_box(&marker) as *const u8 as usize
}
#[inline]
pub unsafe fn maybe_grow<T>(red_zone: usize, segment: usize, f: impl FnOnce() -> T) -> T {
if remaining() >= red_zone {
f()
} else {
unsafe { on_new_stack(segment, f) }
}
}
struct AssertSend<T>(T);
unsafe impl<T> Send for AssertSend<T> {}
impl<T> AssertSend<T> {
fn into_inner(self) -> T {
self.0
}
}
pub unsafe fn on_new_stack<T, F: FnOnce() -> T>(segment: usize, f: F) -> T {
let mut slot = Some(f);
let job = AssertSend(&mut slot as *mut Option<F>);
let r = std::thread::scope(|s| {
std::thread::Builder::new()
.name("cameleer-stack".into())
.stack_size(segment)
.spawn_scoped(s, move || {
let job = job.into_inner();
let f = unsafe { (*job).take() }.expect("job");
AssertSend(f())
})
.map(|h| h.join())
});
match r {
Ok(Ok(v)) => v.into_inner(),
Ok(Err(panic)) => std::panic::resume_unwind(panic),
Err(_) => (slot.take().expect("job"))(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn remaining_shrinks_with_depth_and_fits_the_thread() {
fn deeper(n: u32) -> usize {
let pad = std::hint::black_box([0u8; 4096]);
if n == 0 {
remaining()
} else {
deeper(n - 1) + pad[0] as usize
}
}
let size = 4 << 20;
let (top, deep) = std::thread::Builder::new().stack_size(size).spawn(|| (remaining(), deeper(64))).unwrap().join().unwrap();
assert!(top <= size && top > size / 2, "top {top}");
assert!(top - deep >= 64 * 4096, "top {top} deep {deep}");
}
#[test]
fn maybe_grow_stays_on_the_caller_until_the_stack_is_low() {
use std::rc::Rc;
fn deep(n: usize, c: &Rc<std::cell::Cell<usize>>, threads: &mut Vec<std::thread::ThreadId>) -> usize {
let pad = std::hint::black_box([0u8; 2048]);
c.set(c.get() + 1);
let me = std::thread::current().id();
if !threads.contains(&me) {
threads.push(me);
}
if n == 0 {
return pad[0] as usize;
}
unsafe { maybe_grow(64 * 1024, 1 << 20, || deep(n - 1, c, threads)) + pad[1] as usize }
}
let shallow = std::thread::Builder::new()
.stack_size(1 << 20)
.spawn(|| {
let c = Rc::new(std::cell::Cell::new(0));
let mut t = Vec::new();
deep(10, &c, &mut t);
t.len()
})
.unwrap()
.join()
.unwrap();
assert_eq!(shallow, 1, "a shallow recursion stays on its thread");
std::thread::Builder::new()
.stack_size(1 << 20)
.spawn(|| {
let counter = Rc::new(std::cell::Cell::new(0usize));
let mut threads = Vec::new();
deep(2000, &counter, &mut threads);
assert_eq!(counter.get(), 2001);
assert!(threads.len() > 1, "{threads:?}");
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn panics_on_a_new_segment_reach_the_caller() {
let r = std::panic::catch_unwind(|| unsafe { on_new_stack(256 * 1024, || -> u8 { panic!("boom") }) });
assert!(r.is_err());
assert_eq!(unsafe { on_new_stack(256 * 1024, || 7) }, 7);
}
}