wip
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