Hagia
log in
morj / local-alloc-example
overview
files
history
wiki
Viewing at
use std::alloc::Allocator;
use crate::string::String;

// Copy-pasting some example code from wzmach: it's pretty minimal, I don't know
// if I could make a minimaler meaningful example.

// In wzmach, I run a main processing loop which reads `Event`s and outputs
// `Action`s like this. In this version I unwrap all errors for simplicity.
pub trait Action<A: Allocator> {
/// Useful for debugging and tests
fn description(&self, alloc: A) -> String<A>;
/// Useful for the whole program working at all
fn execute(&self);
}
// Let's not start about enums vs traits ok? Just take my word that this is the
// best solution for the problem at hand (as I gaslight myself).

/// This action when executed runs one shell command
struct ShellAction<A: Allocator> {
cmd: String<A>,
}
/// This one runs a whole program
struct CommandAction<A: Allocator> {
path: String<A>,
args: Vec<String<A>, A>,
}
// They two implement `Action`, it's nothing interesting

// This is the structure holding the main program loop. When writing wzmach, I
// structured the main loop as a big pipeline of iterators, and this is one such
// iterator adapter
struct ActionLoop<I> {
// Input iterator we're adapting
input: I,
// Inner state or something
counter: usize,
}

// Here I actually can't make it a real iterator: I need an allocator provided
// in the `next` call, and iterators don't support additional arguments. Instead
// I do my own `next` method with additional arguments, and don't use the `for`
// loop I guess.
//
// But /do I/ really need to provide the arena externally? I might have implied
// with simple examples in this post that yes, but actually this is a separate
// giant question for another time. Arena, part 2: now with more Pins, coming in 2025.
impl<I> ActionLoop<I>
where
I: Iterator,
{
// In this method, we allocate everything in the arena, which gives us a
// nice speedup
//
// Did you know? `Box<dyn Trait>` is a sugar for `Box<dyn Trait + 'static>`.
// If in your code you see weird requirements for types needing to be
// static, you can explicitly request `Box<dyn Trait + 'a>`
fn next<'a, A: Allocator + Copy + 'a>(
&mut self,
alloc: A,
) -> Option<Box<dyn Action<A> + 'a, A>> {
// Poll the input iterator for "events" that we ignore for this example
let _event = self.input.next()?;

self.counter += 1;
// Dispatch actions dynamically based on some runtime value. The
// textbook case for boxing, and the textbook case of doing boxing
// wrong. Remember rule №1 of high performance: don't allocate in the
// hot loop!
if self.counter % 2 == 0 {
Some(Box::new_in(
ShellAction {
// dangerous(ly fun)
cmd: format_in!(
alloc,
"ls -a | sed -n {}p | xargs rm -rf",
self.counter
)
.unwrap(),
},
alloc,
))
} else {
// Did you notice how often you write `"string literal".to_owned()`
// (or to_string() or into())? Isn't it kind of wasteful to call
// malloc for such a trivial thing? How grateful I am for the
// existence of arenas to save us
let path = String::new_in("systemctl", alloc);
// Ooops, `vec!` doesn't support allocators
let mut args = Vec::with_capacity_in(1, alloc);
args.push(String::new_in("halt", alloc));
Some(Box::new_in(CommandAction { path, args }, alloc))
}
}
}

// Tests are not only good for verifying your code, but also for demonstrating
// how to use it to its future readers, like you my dear friend.
#[cfg(test)]
mod test {
#[test]
fn run_the_loop() {
let mut arena = crate::Arena::new(1024);

// Fake events
let event_stream = 0..10;
// Non-fake adapter of events to actions
let mut looper = super::ActionLoop {
input: event_stream,
counter: 0,
};

// This is a pretty reasonable way to write this, right?
/*
while let Some(action) = looper.next(&arena) {
action.execute();
arena = arena.reclaim();
};
*/
// Not according to rustc: arena is borrowed not for the duration of the
// call to `next`, but for the whole body of the loop. What sense does
// it even make? I remember reading that it's a common footgun for
// mutexes. Argh.
loop {
// Actually it's the same here, arena is borrowed for the whole if
// body, so we have to escape the body first, instead of doing work
// directly there
let action = if let Some(x) = looper.next(&arena) {
x
} else {
break
};

action.execute();
// Now this one makes more sense: arena is borrowed while `action`
// exists, so we need to drop it first
std::mem::drop(action);
// And we clean the arena on every frame
arena = arena.reclaim();
}
}
}

// And then there are the boring `Action` impls that I'm not copying. You can
// find them in the accompanying repo.

impl<A: Allocator> Action<A> for ShellAction<A> {
fn description(&self, alloc: A) -> String<A> {
format_in!(alloc, "sh \"{}\"", self.cmd)
// Did you know that in stdlib the call to `format!` unwraps the
// result? I learned it today wondering why I have to unwrap here
// but not in std.
.unwrap()
}

fn execute(&self) {
// execve stands for execute whatever
eprintln!("sh \"{}\"", self.cmd)
}
}

// Basically the same as above
impl<A: Allocator> Action<A> for CommandAction<A> {
fn description(&self, alloc: A) -> String<A> {
format_in!(alloc, "cmd {} {:?}", self.path, self.args).unwrap()
}

fn execute(&self) {
// Actually this one is execve, the one above should be `system`. But
// actually they are both execves, the one above calling to exec("sh",
// command)
eprintln!("cmd {} {:?}", self.path, self.args)
}
}

pub mod benches {
pub fn run_arena(mut arena: crate::Arena, size: usize) {
let event_stream = 0..size;
let mut looper = super::ActionLoop {
input: event_stream,
counter: 0,
};
loop {
let action = if let Some(x) = looper.next(&arena) {
x
} else {
break
};

let _descr = action.description(&arena);
std::mem::drop(action);
std::mem::drop(_descr);
arena = arena.reclaim();
}
}

pub fn run_in<A: std::alloc::Allocator>(alloc: A, size: usize) {
let event_stream = 0..size;
let mut looper = super::ActionLoop {
input: event_stream,
counter: 0,
};
loop {
let action = if let Some(x) = looper.next(&alloc) {
x
} else {
break
};

let _descr = action.description(&alloc);
std::mem::drop(action);
}
}
}