Delivering events
The next problem is how to deliver events. One approach is to deliver an event to the destination as soon as it occurs, as a direct method call. However this results in nested callbacks on the stack, i.e. one event causes an event handler to be called on another object, which in responding to that event causes other events to be generated, calling event handlers in other objects, all before the original event handler has completed. The main problem with this approach is that it can lead to re-entrant calls to the same object, which in other languages can result in very hard to understand bugs.
However in Rust there is a bigger problem, because a re-entrant call
will require borrowing the same object twice. If we use RefCell
,
we'll only discover this bug if we're lucky enough to come across it
in testing. However if we use qcell instead, there is
absolutely no way to construct a program that will execute a
re-entrant call on an object. So letting Rust do compile-time
borrowing checks on data behind Rc
pays off, because it forces us
to adopt a design that has none of the problems that are easy to
produce in other languages accidentally.
So to avoid borrowing problems and re-entrant calls, event sending and
event delivery must be separated, which means that a queue is
necessary. The most fundamental queue would be one that stores a list
of FnOnce
closures to execute later. It's possible to make this
efficient by storing the closures in a flat Vec<u8>
, which means
that no allocations are required to send a message, so long as the
queue buffer has grown big enough.
This also demonstrates another pattern in Rust, that Rust's rules seem to lead to shallow call-stacks. This is because when a borrow is active, that often restricts access to other things. To get access to those things again means dropping back down the stack again. Also when borrows are passed deeper and deeper into the code, they seem to become more and more invasive and restrictive, as you end up having to annotate more and more functions and structures with lifetimes.
Using a FnOnce
queue to defer operations untangles all of this and
means that each operation is run directly from the main loop with the
minimum restrictions from the borrow checker.