use std::io;
use std::thread::{Builder, JoinHandle, Scope, ScopedJoinHandle};
pub const PARSE_STACK_SIZE: usize = 16 * 1024 * 1024;
pub const RUNTIME_STACK_SIZE: usize = 32 * 1024 * 1024;
const _: () = assert!(RUNTIME_STACK_SIZE >= PARSE_STACK_SIZE);
pub fn builder() -> Builder {
Builder::new().stack_size(RUNTIME_STACK_SIZE)
}
pub fn spawn<F, T>(body: F) -> JoinHandle<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
builder().spawn(body).expect("failed to spawn thread")
}
pub fn scope<'env, F, T>(body: F) -> T
where
F: for<'scope> FnOnce(RuntimeScope<'scope, 'env>) -> T,
{
std::thread::scope(|inner| body(RuntimeScope { inner }))
}
#[derive(Clone, Copy)]
pub struct RuntimeScope<'scope, 'env: 'scope> {
inner: &'scope Scope<'scope, 'env>,
}
impl<'scope, 'env> RuntimeScope<'scope, 'env> {
pub fn spawn<F, T>(&self, body: F) -> ScopedJoinHandle<'scope, T>
where
F: FnOnce() -> T + Send + 'scope,
T: Send + 'scope,
{
builder()
.spawn_scoped(self.inner, body)
.expect("failed to spawn scoped thread")
}
pub fn spawn_named<F, T>(
&self,
name: impl Into<String>,
body: F,
) -> io::Result<ScopedJoinHandle<'scope, T>>
where
F: FnOnce() -> T + Send + 'scope,
T: Send + 'scope,
{
builder().name(name.into()).spawn_scoped(self.inner, body)
}
}
pub fn on_vm_stack<R: Send>(body: impl FnOnce() -> R + Send) -> R {
scope(|scope| {
scope
.spawn_named("harn-vm-contract-stack", body)
.expect("spawn a thread holding the VM stack contract")
.join()
.unwrap_or_else(|payload| std::panic::resume_unwind(payload))
})
}
#[cfg(test)]
mod tests {
#[inline(never)]
fn burn_stack(depth: usize) -> u8 {
let mut frame = [0u8; 16 * 1024];
frame[depth % frame.len()] = depth as u8;
let frame = std::hint::black_box(frame);
if depth == 0 {
return frame[0];
}
frame[0].wrapping_add(burn_stack(depth - 1))
}
const PROBE_CHILD: &str = "HARN_RUNTIME_STACK_PROBE_CHILD";
const PROBE_BYTES: usize = 8 * 1024 * 1024;
#[test]
fn every_spawn_form_holds_the_runtime_stack() {
if std::env::var_os(PROBE_CHILD).is_some() {
let depth = PROBE_BYTES / (16 * 1024);
super::spawn(move || burn_stack(depth))
.join()
.expect("spawn");
super::builder()
.name("probe".to_owned())
.spawn(move || burn_stack(depth))
.expect("builder spawn")
.join()
.expect("builder");
super::scope(|scope| {
scope
.spawn(move || burn_stack(depth))
.join()
.expect("scope spawn");
scope
.spawn_named("probe", move || burn_stack(depth))
.expect("spawn_named")
.join()
.expect("scope spawn_named");
});
super::on_vm_stack(move || burn_stack(depth));
return;
}
let probe = |child_env: Option<&str>| {
let mut command =
std::process::Command::new(std::env::current_exe().expect("test executable"));
command
.args([
"--exact",
"runtime_stack::tests::every_spawn_form_holds_the_runtime_stack",
"--test-threads=1",
])
.env_remove("RUST_MIN_STACK");
if let Some(value) = child_env {
command.env(PROBE_CHILD, value);
}
command.status().expect("re-exec the probe")
};
let status = probe(Some("1"));
assert!(
status.success(),
"a runtime_stack thread overflowed {PROBE_BYTES} bytes with RUST_MIN_STACK \
unset ({status}), so it took Rust's 2 MiB default"
);
}
#[test]
fn the_probe_overflows_a_default_stack() {
if std::env::var_os(PROBE_CHILD).is_some() {
return;
}
let status = std::process::Command::new(std::env::current_exe().expect("test executable"))
.args([
"--exact",
"runtime_stack::tests::default_stack_probe_child",
"--test-threads=1",
])
.env_remove("RUST_MIN_STACK")
.env(PROBE_CHILD, "1")
.status()
.expect("re-exec the default-stack probe");
assert!(
!status.success(),
"the probe survived Rust's 2 MiB default stack, so it proves nothing"
);
}
#[test]
fn default_stack_probe_child() {
if std::env::var_os(PROBE_CHILD).is_none() {
return;
}
let depth = PROBE_BYTES / (16 * 1024);
let _ = std::thread::spawn(move || burn_stack(depth)).join();
}
}