canine's cogent cognizances

home / Rust nightmares

published December 13, 2023 in rust, programming post thumbnail

note: hyperbole for entertainment

Oh dear. You stayed up too late fixing borrow checker errors. It’s the night before an important final, but Ferris has other plans for you, which, unfortunately, are disjoint from studying. While staring at the hopeless ten line snippet preventing you from doing anything ever again, you drift off, dreaming about your aborted attempt to capture a goddamn variable inside a future – that can be passed into another function. As you drift off, you ruminate: some people think Rust is overcomplicated. No, you believe, it just isn’t complicated enough. You’ve frantically researched all available material on higher rank trait bounds – there is simply a dearth. You’ve tried to add two lifetimes – you cannot. Your efforts, inane in the eyes of the unholy crab, have been requited by note: due to current limitations in the borrow checker, this implies a `'static` lifetime.

Ferris has you by the throat. After a restless blink, you awake, shackled to the cavernous walls of Ferris’ torture chamber. You feel like you’re in a familiar place, but this notion does not offer any respite from your growing trepidation. Blood drips from the roughly hewn grotto, culminating in a pool of broken code, Arc<Mutex<Box<dyn ... + Send>>>>, or move |...| Box::pin(async move { ... }). You’ve grown numb to the pain of stable rust and the gatekeeping of 5 year old features still in the works. But that isn’t the problem. Despite your denial, you know that zero cost and ease of use are hopeless ideals. You know that vec.drain() doesn’t drain if the iterator is thrown into a reference cycle, instead leaving the vector without its upper elements. You know exactly when

macro_rules!{
  ($self: ident, $ex: expr) => {
    impl Trait for Type { fn (&mut $self) { $ex } }
  }
}

doesn’t work when it normally would ($self is _, for instance). You both cherish and detest associated types for enabling you to implicitly bound generics. You dream of greener grass, but your view is also blocked by #[feature] flags and the stormy darkness of nightly. You wonder why we don’t have bitsets, async block return values, or a Leak trait. As if reading your thoughts, Ferris eerily stares at you with a lethargic grin and penetrating eye stalks, nibbling on a carcass clanking against your chains. But the putrid stench of the oppressive dungeon occupies an small space in your mind compared to the monolithic stress of Rust, daily corroding your sanity.

You look back at Ferris, vaguely wondering how he escaped from the military-grade crab-cloning test chamber. Are his claws that powerful? These rational inquires clear your mind, and you slowly come to your senses, remembering why you are here. You shout in a panic, “Ferris, my liege… All I wanted was to capture a variable inside an async closure passed to another function, where that closure also takes a reference! Surely this is a common thing to do – I can do it so easily in any GCed language. Please…” Sensing his weakness to Rust code, you suggest, “something like this:”

async fn run<Fut, F>(f: F)
  where for<'b> F: FnOnce(&'b usize) -> Fut + 'b,
    Fut: Future<Output=()> + Send {

  let t = 0;
  f(&t).await;
}

async fn main() {
  let test_capture = 0;
  run(|x| async {
    println!("captured {}, {}", test_capture, x);
  }).await;
}

You know that this won’t work – Rust never works. But it has an effect: enamored by these incantations, the mighty crab slowly gazes through your skull. But recalling the inhuman speed with which he chased you from your house into the basement of this abandoned barn, you know this is only a facade. A deep, glottal rumble shakes the stone.

“TIS’ VERBOTEN TO ADD A LIFETIME TO THE USAGE OF A GENERIC TYPE PARAMETER”

Sensible, you think. Who needs those? Only those who have been transported here to perish and therefore only those will no longer taint Rust’s userbase. Ferris inches crabwise towards you, pincers raised, as you strain to devise another example. You try to make Ferris pause by using indirection. “Um. Ok, what about:”

async fn run<F>(f: F)
  where for<'b> F: FnOnce(&'b usize) -> Pin<Box<dyn Future<Output=()> + 'b>> {

  let t = 0;
  f(&t).await;
}

async fn main() {
  let test_capture = 0;
  run(|x| Box::pin(async {
    println!("captured {}, {}", test_capture, x);
  })).await;
}

You wish it wouldn’t happen, but you anticipate the inevitable compile error.

test_capture DOES NOT LIVE LONG ENOUGH. YOU ARE AT THE END OF THE LINE. AFTER HOURS OF DEBUGGING AND A LUXURIOUS NAP, YOUR OFFERINGS ARE SHAMEFULLY INSUFFICIENT. PREPARE TO BE ELIDED.”

What? Yes, anything can be substituted for 'b in the for<'b> expression, but surely it would be a crime to use something that lives longer, like 'static, since run can’t just make up a 'static variable at runtime! Then you see it: what if the supplied value is static, and the returned future is stored in, say, a globally accessible struct. But if you can’t use 'b + 'a or for<'b: 'a>, what can you do? While straining your scant remaining brain cells to the utmost, your phone begins buzzing – your family has no doubt finally noticed your prolonged disappearance – and you seize Ferris’s reaction to the execution of non-Rust code, too reduced for his oxidized mind, to search for the solution.

Scrolling past beginners having comparably easier lifetime issues, you miraculously stumble across this relevant post describing how to let async closures be both accepted as a function argument and borrow data! Gleefully, you glance at the proposed solution, only to be shocked. It is identical to yours! Or, it should be:

fn call_twice_async(op: impl for<'a> FnMut(&'a str) -> Box<dyn Future<Output = ()> + 'a>)

“YOUR RELIANCE ON SOCIETY’S CRUMBLING SOFTWARE STACK WOULD DRIVE ME TO PITY IF YOU HAD NOT BEEN PAMPERED BY THEIR ENDORSEMENT OF INEFFICIENT, BLOATED, AND HUMAN CENTRIC SOFTWARE. HAPPILY, YOUR CONTRIBUTION TO THIS MOST IMPROPITIOUS FOUNDATION ENDS NOW.”

Shit. Ferris controls his voice to be serious, but he cannot mask his ever-widening smile at your predicament. You click on the extended example of the blog post linked above, and make an effort to thoroughly scan it despite your sleep-deprived fugue:

pub fn scope_fn<'env, R, B>(body: B) -> ScopeBody<'env, R>
where
    R: Send + 'env,
    B: for<'scope> FnOnce(&'scope Scope<'scope, 'env, R>) -> BoxFuture<'scope, R> {
  ...
}

If this doesn’t work, you reason, then how could there be over 300 downloads? Then you realize your 4-year old, unmaintained and decrepit crate errer that you highly advise against using (although you have not reflected this sentiment elsewhere) has racked up 17k+ downloads. Still, it must function.

Then you see it: &'scope Scope<'scope, 'env, R> implies that 'scope: 'env! Because 'scope is used as the lifetime of a reference to an object with lifetime parameter 'env, 'scope cannot live longer than 'env. You brilliantly weave a makeshift trait to fix your example, chanting to your critical audience:

struct ShortLived<'a, 'b> {x: &'b usize, b: PhantomData<&'a usize>}

async fn run<'a, F>(f: F)
  where for<'b> F: FnOnce(&'b ShortLived<'a, 'b>) -> Pin<Box<dyn Future<Output=()> + 'b>> {

  let t = 0;
  f(&ShortLived {x: &t, b: PhantomData}).await;
}

#[tokio::main]
async fn main() {
  let test_capture = 0;
  run(|x| Box::pin(async {
    println!("captured {}, {}", test_capture, x.x);
  })).await;
}

Gloriously, it compiles. You wait for Ferris to parse your spiel.

“DELECTABLE. YOU MAY BE RETURNED FROM THIS SCOPE, BUT YOUR LIFETIME IS BOUNDED. TEMPORARY PENANCE WILL NOT GUARANTEE SAFETY WHEN WE MEET AGAIN.”

You perform poorly on your final.