#![deny(missing_docs)]
use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;
thread_local! {
static FORBID: Cell<u32> = const { Cell::new(0) };
}
#[inline]
pub fn enter_no_alloc() {
FORBID.with(|f| f.set(f.get().saturating_add(1)));
}
#[inline]
pub fn exit_no_alloc() {
FORBID.with(|f| f.set(f.get().saturating_sub(1)));
}
#[inline]
pub fn is_forbidden() -> bool {
FORBID.with(|f| f.get()) > 0
}
#[inline]
pub fn allow<T>(f: impl FnOnce() -> T) -> T {
let saved = FORBID.with(|c| c.replace(0));
let guard = Restore(saved);
let out = f();
drop(guard);
out
}
struct Restore(u32);
impl Drop for Restore {
#[inline]
fn drop(&mut self) {
FORBID.with(|c| c.set(self.0));
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct YoAlloc;
impl YoAlloc {
pub const fn new() -> YoAlloc {
YoAlloc
}
}
#[cold]
#[inline(never)]
fn violation(layout: Layout, what: &str) -> ! {
use std::io::Write as _;
let mut buf = [0u8; 32];
let n = write_usize(&mut buf, layout.size());
let mut err = std::io::stderr().lock();
let _ = err.write_all(b"yo: allocation on a shard thread: ");
let _ = err.write_all(what.as_bytes());
let _ = err.write_all(b" of ");
let _ = err.write_all(&buf[..n]);
let _ = err.write_all(
b" bytes.\nThis is Y7: no global allocator call on a command path.\n\
Move the allocation to setup, or wrap it in yo_alloc::allow if it is\n\
genuinely off the command path.\n",
);
let _ = err.flush();
std::process::abort()
}
fn write_usize(buf: &mut [u8; 32], mut v: usize) -> usize {
if v == 0 {
buf[0] = b'0';
return 1;
}
let mut tmp = [0u8; 32];
let mut n = 0;
while v > 0 {
tmp[n] = b'0' + (v % 10) as u8;
v /= 10;
n += 1;
}
for i in 0..n {
buf[i] = tmp[n - 1 - i];
}
n
}
unsafe impl GlobalAlloc for YoAlloc {
#[inline]
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
if is_forbidden() {
violation(layout, "alloc");
}
unsafe { System.alloc(layout) }
}
#[inline]
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
if is_forbidden() {
violation(layout, "alloc_zeroed");
}
unsafe { System.alloc_zeroed(layout) }
}
#[inline]
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
if is_forbidden() {
violation(layout, "realloc");
}
unsafe { System.realloc(ptr, layout, new_size) }
}
#[inline]
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn starts_permitted() {
assert!(!is_forbidden());
}
#[test]
fn enter_and_exit_are_balanced() {
assert!(!is_forbidden());
enter_no_alloc();
assert!(is_forbidden());
enter_no_alloc();
assert!(is_forbidden());
exit_no_alloc();
assert!(is_forbidden(), "one exit must not undo two enters");
exit_no_alloc();
assert!(!is_forbidden());
}
#[test]
fn allow_permits_and_restores() {
enter_no_alloc();
assert!(is_forbidden());
let v = allow(|| {
assert!(!is_forbidden());
vec![1u8, 2, 3]
});
assert_eq!(v.len(), 3);
assert!(is_forbidden(), "allow must restore the previous state");
exit_no_alloc();
}
#[test]
fn allow_nests() {
enter_no_alloc();
allow(|| {
allow(|| assert!(!is_forbidden()));
assert!(!is_forbidden());
});
assert!(is_forbidden());
exit_no_alloc();
}
#[test]
fn allow_restores_when_the_body_panics() {
enter_no_alloc();
let r = std::panic::catch_unwind(|| {
allow(|| panic!("boom"));
});
assert!(r.is_err());
assert!(
is_forbidden(),
"a panic inside allow must not leave the thread permitted"
);
exit_no_alloc();
}
#[test]
fn the_flag_does_not_cross_threads() {
enter_no_alloc();
let other = std::thread::spawn(is_forbidden).join().unwrap();
assert!(!other, "another thread saw this thread's flag");
exit_no_alloc();
}
#[test]
fn integers_render_without_allocating() {
let mut buf = [0u8; 32];
for (v, want) in [
(0usize, "0"),
(7, "7"),
(1024, "1024"),
(2097152, "2097152"),
] {
let n = write_usize(&mut buf, v);
assert_eq!(std::str::from_utf8(&buf[..n]).unwrap(), want);
}
}
}