1I will make no_std no_alloc stack allocated async work. 2 3> What does bcsc stand for? 4 5Borrow checked structured concurrency 6 7Normal rust async is borrow checked, but this uses lifetimes (as opposed to spamming ref counting) to enforce structured concurrency in a more elegant, performant, and nostd compatible manner. 8 9> What is wrong with you asa?? 10 11I set out to build the best robotics framework in every aspect. I will do that. It's unfortunate that rust doesn't currently make it easy, but I will gladly fix the problems in rust first. 12 13> Ok(()) 14 15[multi-task vs intra-task concurrency](https://without.boats/blog/futures-unordered/) 16 17multi-task concurrency extends better to parallelism 18- in my case, you can also implement thread affinity for hardware 19 20`FuturesUnordered` requires Arc (std) 21 22[scoped/non-'static futures](https://github.com/rmanoka/async-scoped) 23 24why this (nostd structured concurrency) is unsound/impossible to do safetly 25 26- https://tmandry.gitlab.io/blog/posts/2023-03-01-scoped-tasks/ !! 27- https://without.boats/blog/the-scoped-task-trilemma/ !! 28- https://conradludgate.com/posts/async-stack !! 29- https://cglab.ca/~abeinges/blah/everyone-poops/ 30- https://sabrinajewson.org/blog/async-drop 31- https://blog.yoshuawuyts.com/the-waker-allocation-problem/ 32- https://faultlore.com/blah/linear-rust/ 33- https://blog.yoshuawuyts.com/linear-types-one-pager/ 34 35problem: i really want this feature, and am fine with unsound code 36 37sound options: 38[async nursery](https://github.com/najamelan/async_nursery) - still 'static and not ergonomic api, wraps `FuturesUnordered` 39[async-scoped](https://github.com/rmanoka/async-scoped) - wraps `FuturesUnordered`, stores in executor 40better? 41https://github.com/maroider/async_scoped_task/blob/master/src/lib.rs 42unsafe `async_scoped::scope_and_collect` is perfect (unsafe) but uses heap alloc 43 44[moro](https://github.com/nikomatsakis/moro) - wraps `FuturesUnordered`, relies on single threaded for invariants 45[task scope](https://docs.rs/task_scope/0.1.1/task_scope/) - scoped tasks but no drop guarantees unless blocking 46relevant rfc for Forget 47https://github.com/rust-lang/rfcs/pull/3782 !! 48 49outdated tracking issue 50https://github.com/rust-lang/compiler-team/issues/727 51 52other similar proposal for Leak 53https://zetanumbers.github.io/book/myosotis.html 54 55alternate way of fixing drop issue 56https://github.com/Matthias247/rfcs/pull/1 57 58other relevant work/rfc tracking pr 59https://github.com/rust-lang/rfcs/pull/2958 60 61why drop? 62https://without.boats/blog/wakers-i/ 63https://without.boats/blog/wakers-ii/ 64 65wakers are references to a Task/whatever the executor uses to wrap and enqueue Futures 66 67safe api: [Wake](https://doc.rust-lang.org/beta/std/task/trait.Wake.html) 68where `Task: Wake`, wakers are essentially `Weak<Task>` so they can wake the task while it exists (Weak won't get upgraded once the task goes out of scope, so this is safe) 69why can't there be a safe api with `Arc`? 70`&dyn Wake` doesn't work because concurrency (think: joins) involves multiple wakers for the same task (unless everything is spawned instead of joined!??) 71wakers must be cloned, but clone -> Self (Self vtable is unknown through `&dyn Wake` pointer) 72ok that explains *const (), but why remove the lifetimes? 73not sure?? it seems like it wouldn't make a difference, most futures are static anyway for afformentioned soundness reasons 74- what if wakers are an intrusive linked list that the task traverses to cancel when dropped? (requires `!Forget`)/leak safety 75- what if wakers were `&dyn Task` with no cloning, and all intra-task concurrency was moved to join handles for scoped spawns 76 - also note that stuff like join!() doesn't actually execute the specific future, the outermost task gets woken and then executes all subtasks, which return Pending if they aren't ready 77 - intra-task concurrency is evil?? 78 - still have to wait on concurrent join handles? -> join handles are part of nursery/scope, which stores its own waker-per-task -> subwakers/scope's wakers get called -> scope queues relevant tasks -> call higher level task waker 79there is no way to make existing `RawWaker`/`AtomicWaker` api safe because it cannot be "invalidated"? 80 81## What is this project? 82 83New async primitives that disallow intra-task concurrency, clone of `futures` and `futures-concurrency` for the new primitives. 84 85 86channels: need lifetimed receievers, probably needs `Forget` (arc-like channels would be unsafe) 87 88# Chapter 2: 89 90I am actually addicted to rewriting async rust at this point I need a new hobby 91 92why ch 1 failed: 93 94discussed in [waker allocation problem](https://blog.yoshuawuyts.com/the-waker-allocation-problem/) from earlier: Futures are self referential 95- i somehow did not realize this 96 97If you have `Task<'scope>`, which contains `Future<'scope>'` and `Waker<'scope>'`, where `Future::poll(wake: &'scope dyn Wake)`, there needs to be `&'scope` references to owned wakers inside `Task` being passed to owned futures (also in `Task`). This is useful because it prevents reactors (anything that registers a `&'scope dyn Wake`) from outliving the future/task containing them, preventing dangling pointers without `Arc`/alloc. 98However, it also is self referential: 99- `Task<'scope>` means your task must live `<= 'scope` 100- `Task { FutureImpl { Reactor { &'scope dyn Wake} } }` (the task must store a reactor at some point) means the task must live for `>= 'scope` 101 102This is fine, actually! Rust [does](https://sabrinajewson.org/blog/null-lifetime) allow self referential structs*. 103The catch is that, as you may have guessed, the task must live for exactly `'scope`. Since it has an immutable reference to parts of itself, and immutable references are disallowed from moving while they are held, the task may not move after the self-reference has been established. 104 105This usually makes self referential structs useless, but... not necessarily in this case 106 107```rust 108// suppose rustc generated impls for ScopedFuture 109async fn example() { 110 let f1 = my_future(1); 111 let f2 = my_future(2); 112 join!(f1, f2) // all async combinators would need to be macros that create self reference in-place 113 // actually this would also work because it isn't being polled 114} 115``` 116 117```rust 118// suppose rustc generated impls for ScopedFuture 119async fn example() { 120 let f1 = my_future(1); 121 let f2 = my_future(2); 122 join!(f1, f2).await; // no futures passed around, used in place! 123 // compiles fine!! 124} 125``` 126 127Cool properties: 128- no `Pin`, since the compiler guarantees the future will not move due to self reference for entire lifetime of 'scope 129- no unsafe code(?) - no pins 130 131Tradeoffs: 132- need tons of interior mutability, since immutable/can't move means `poll` cannot take `&mut self`, cells everywhere 133 - nvm lots of unsafe code, but nothing really unsound 134- potentially bad error messages? stuff like `join!` will have to output code that manually sets up the waker self ref 135 136## TODO: 137- [x] ScopedFuture 138- [x] static combinators (Join Race etc), see futures-concurrency 139- [x] `#[async_scoped]` or some compiler ScopedFuture generation 140- [ ] doubly linked list waker registration 141- [ ] repeating static time reactors - eg. make event poll every N seconds 142- [ ] io uring reactors 143- [ ] growable combinators (eg. `FutureGroup`, `FuturesUnordered`) (require alloc?) 144- [ ] unsound (needs `Forget`) multithreading 145- [ ] "rethinking async rust" 146- [ ] all of the above for streams 147- [ ] rfc? 148 149# Chapter 3 150 151man this really sucks 152 153i need better things to do 154 155issues from ch 2 156- works great[*] 157- *: incompatible with event loop - something still has to poll 158 - we're back to doubly linked list of waker registration in event loop 159 - this requires Forget 160- ScopedFuture - Future interop sucks 161 162 163structured concurrency with regular combinators: 164- scope holds tasks 165- scope cancels tasks when dropped 166- tasks are ran by central executor 167 168pure structured concurrency with borrow checker: 169- high level block_on(), any task wake wakes every level up 170- tasks have events 171 172how do the tasks register to an event loop? they don't fuck 173 174 175```rust 176struct Task<F: Future> { 177 inner: F, 178 prev: *const Task, 179 next: *const Task, 180 waker: Waker, 181} 182``` 183 184&waker is passed into all sub-tasks, calling wake clone etc panics!!! 185this is pretty jank 186 187also waker doesn't have a lifetime so a safe code could easily register to external source that outlives the task 188this is unsound 189 190we need 191```rust 192struct WakerRegister { 193 prev: *const WakerRegister, 194 next: *const WakerRegister, 195} 196``` 197 198# Borrow Checked Structured Concurrency 199 200An async system needs to have the following components: 201 202- event loop : polls events and schedules tasks when they are ready 203- tasks : state machines that progress and can await events from the event loop 204- task combinators : tasks that compose other tasks into useful logic structures 205 206Tasks will register themselves to events on the event loop, which will need to outlive tasks and register pointers to wake the tasks, so that they can again be polled. 207This is incompatible with the borrow checker because the task pointers (wakers) are being stored inside an event loop with a lifetime that may exceed the tasks'. 208 209`Waker` is effectively a `*const dyn Wake`. It is implemented using a custom `RawWakerVTable` rather than a `dyn` ptr to allow for `Wake::wake(self)`, which is not object safe. 210This method is necessary for runtime implementations that rely on the wakers to be effectively `Arc<Task>`, since `wake(self)` consumes `self` and decrements the reference count. 211 212There are two types of sound async runtimes that currently exist in rust: 213 214[tokio](https://github.com/tokio-rs) and [smol](https://github.com/smol-rs) work using the afformentioned reference counting system to ensure wakers aren't dangling pointers to tasks that no longer exist. 215 216[embassy](https://github.com/embassy-rs/embassy) and [rtic](https://github.com/rtic-rs/rtic) work by ensuring tasks are stored in `static` task pools for `N` tasks. Scheduled tasks are represented by an intrusively linked list to avoid allocation, and wakers can't be dangling pointers because completed tasks will refuse to add themselves back to the linked list, or will be replaced by a new task. This is useful in environments where it is desirable to avoid heap allocation, but requires the user annotate the maximum number of a specific task that can exist at one time, and fails to spawn tasks when they exceed that limit. 217 218An async runtime where futures are allocated to the stack cannot be sound under this model because `Future::poll` allows any safe `Future` implementation to store or clone wakers wherever they want, which become dangling pointers after the stack allocated future goes out of scope. In order to prevent this, we must have a circular reference, where the task (`Task<'scope>: Wake`) contains a `&'scope Waker` and the `Waker` contains `*const dyn Wake`. For that to be safe, the `Waker` must never be moved. This cannot be possible because something needs to register the waker: 219 220```rust 221// waker is moved 222poll(self: Pin<&mut Self>, cx: &mut Context<'_> /* '_ is the duration of the poll fn */) -> Poll<Self::Output> { 223 // waker is moved! 224 // can't register &Waker bc we run into the same problem, 225 // and the waker only lives for '_ 226 store.register(cx.waker()) 227} 228``` 229 230Effectively, by saying our waker can't move, we are saying it must be stored by the task, which means it can't be a useful waker. Instead, what we could do is have a waker-register (verb, not noun) that facilitates the binding of an immovable waker to an immovable task, where the waker is guaranteed to outlive the task: 231 232```rust 233pub trait Wake<'task> { 234 fn wake(&self); 235 fn register_waker(&mut self, waker: &'task Waker); 236} 237 238pub struct Waker { 239 task: *const dyn Wake, 240 valid: bool, // task is in charge of invalidating when it goes out of scope 241} 242 243pub struct WakerRegistration<'poll> { 244 task: &'poll mut dyn Wake, 245} 246 247impl<'poll> WakerRegistration<'poll> { 248 pub fn register<'task>(self, slot: &'task Waker) 249 where 250 'task: 'poll, 251 Self: 'task 252 { 253 *slot = Waker::new(self.task as *const dyn Wake); 254 *task.register_waker(slot) 255 } 256} 257``` 258 259This system works better because `WakerRegistration` only lives 260 261 262 263Experienced rust programmers might be reading this and thinking I am stupid (true) because `Forget` 264 265An astute (soon to be disappointed) reader might be thinking, as I did when I first learned about this sytem, "what if we ensured that there was only one `Waker` per `Task`, and gave the task a pointer to the waker, so that it could disable the waker when dropped?" 266 267Unfortunately, there are a multitude of issues with this system 268 269- In order to hold a pointer to the Waker from the 270- Preventing a `Waker` from moving means panicking on the 271 272Even if it was guaranteed that wakers could not be moved or `cloned` (by panicking on `clone`), and registration occured via `&Waker`, the task would still be unable to 273 274https://conradludgate.com/posts/async-stack 275 276## Structured Concurrency 277 278https://trio.discourse.group/t/discussion-notes-on-structured-concurrency-or-go-statement-considered-harmful/25 279https://kotlinlang.org/docs/coroutines-basics.html 280 281Notably, the structured concurrency pattern fits very nicely with our hypothetical unsound stack based async runtime. 282 283## WeakCell Pattern & Forget trait 284 285https://github.com/rust-lang/rfcs/pull/3782 286 287 288There are two solutions: 289 290- `Wakers` panic on `clone()` 291 292## Waker allocation problem & intra task concurrency 293 294we can't do intra task concurrency because WeakRegistrations 295 296