5

I'd like to know how a composable event-driven design with callbacks can be used in Rust. From my existing experimentation, I have come to suspect that the ownership system in Rust is more suited to top-down procedural code and has problems with references to parent objects that are needed for callbacks in event-driven design.

Essentially, I would like to see the Rust equivalent for the following C++ code. The code implements an EventLoop which dispatches Timer events using a busy-loop, a Timer class with a timer_expired callback, and a User class that schedules a timer in intervals of 500ms.

#include <stdio.h>
#include <assert.h>
#include <list>
#include <chrono>
#include <algorithm>

using namespace std::chrono;

// Wrapping code in System class so we can implement all functions within classes declarations...
template <typename Dummy=void>
struct System {
    class Timer;

    class EventLoop {
        friend class Timer;

    private:
        std::list<Timer *> m_running_timers;
        bool m_iterating_timers;
        typename std::list<Timer *>::iterator m_current_timer;

        void unlink_timer (Timer *timer)
        {
            auto it = std::find(m_running_timers.begin(), m_running_timers.end(), timer);
            assert(it != m_running_timers.end());
            if (m_iterating_timers && it == m_current_timer) {
                ++m_current_timer;
            }
            m_running_timers.erase(it);
        }

    public:
        EventLoop()
        : m_iterating_timers(false)
        {
        }

        milliseconds get_time()
        {
            return duration_cast<milliseconds>(system_clock::now().time_since_epoch());
        }

        void run()
        {
            while (true) {
                milliseconds now = get_time();

                m_iterating_timers = true;
                m_current_timer = m_running_timers.begin();

                while (m_current_timer != m_running_timers.end()) {
                    Timer *timer = *m_current_timer;
                    assert(timer->m_running);

                    if (now >= timer->m_expire_time) {
                        m_current_timer = m_running_timers.erase(m_current_timer);
                        timer->m_running = false;
                        timer->m_callback->timer_expired();
                    } else {
                        ++m_current_timer;
                    }
                }

                m_iterating_timers = false;
            }
        }
    };

    struct TimerCallback {
        virtual void timer_expired() = 0;
    };

    class Timer {
        friend class EventLoop;

    private:
        EventLoop *m_loop;
        TimerCallback *m_callback;
        bool m_running;
        milliseconds m_expire_time;

    public:
        Timer(EventLoop *loop, TimerCallback *callback)
        : m_loop(loop), m_callback(callback), m_running(false)
        {
        }

        ~Timer()
        {
            if (m_running) {
                m_loop->unlink_timer(this);
            }
        }

        void start (milliseconds delay)
        {
            stop();
            m_running = true;
            m_expire_time = m_loop->get_time() + delay;
            m_loop->m_running_timers.push_back(this);
        }

        void stop ()
        {
            if (m_running) {
                m_loop->unlink_timer(this);
                m_running = false;
            }
        }
    };

    class TimerUser : private TimerCallback {
    private:
        Timer m_timer;

    public:
        TimerUser(EventLoop *loop)
        : m_timer(loop, this)
        {
            m_timer.start(milliseconds(500));
        }

    private:
        void timer_expired() override
        {
            printf("Timer has expired!\n");
            m_timer.start(milliseconds(500));
        }
    };
};

int main ()
{
    System<>::EventLoop loop;
    System<>::TimerUser user(&loop);
    loop.run();
    return 0;
}

The code works as standard C++14 and I believe is correct. Note, in a serious implementation I would make the running_timers an intrusive linked-list not a std::list for performance reasons.

Here are some properties of this solution which I need to see in a Rust equivalent:

  • Timers can be added/removed without restrictions, there are no limitations on how/where a Timer is allocated. For example one can dynamically manage a list of classes each using their own timer.
  • In a timer_callback, the class being called back has full freedom to access itself, and the timer it is being called from, e.g. to restart it.
  • In a timer_callback, the class being called also has freedom to delete itself and the timer. The EventLoop understands this possibility.

I can show some things I've tried but I don't think it will be useful. The major pain point I'm having is satisfy the borrowing rules with all the references to parent objects involved for callback traits.

I suspect a RefCell or something similar might be part of a solution, possibly one or more special classes with internal unsafe parts, that allow gluing stuff together. Maybe some parts of reference safety typically provided by Rust could only be guaranteed at runtime by panicking.

Update

I have created a prototype implementation of this in Rust, but it is not safe. Specifically:

  • Timers and the EventLoop must not be moved. If they are accidentally moves undefined behavior occurs due to use pointers to these. It is not possible to even detect this within Rust.
  • The callback implementation is a hack but should work. Note that this allows the same object to receive callbacks from two or more Timers, something that is not possible if traits were used for callbacks instead.
  • The callbacks are unsafe due to use of pointers that point to the object that is to receive the callback.
  • It is theoretically possible for an object to delete itself, but this actually seems unsafe in Rust. Because if a Timer callback ends up deleting itself, there would be a &self reference at one point that is not valid. The source of this unsafety is the use of pointers for callbacks.
Ambroz Bizjak
  • 7,809
  • 1
  • 38
  • 49
  • Did you take a look at some events librarys https://crates.io/search?q=event ? Do they not meet your needs? – malbarbo Apr 30 '16 at 10:57
  • Do you mean that a `timer_callback` can arbitrarily call `delete` on itself, or just that a `timer_callback` may invoke `unlink_timer` on the `Timer` it belongs to, from within its `timer_expired`-method? – Thierry Apr 30 '16 at 11:54
  • Also, your `get_time` method seems to simply return the system time. Hence, it doesn't have to be an instance method, or even a member of `EventLoop` at all, right? Am I missing something there? – Thierry Apr 30 '16 at 11:54
  • @Thierry 1) yes timer_callback can end up calling delete on this or a parent structure can do that, or something else which destructs the object. This is a very common thing to do in event-driven design. For example consider you have a Client class corresponding to a connected TCP class, which has member classes Connection and Timer. The Timer callback would delete the Client upon inactivity and likewise an error callback for the Connection would also delete the Client. – Ambroz Bizjak Apr 30 '16 at 12:05
  • @Thierry Yes I know get_time need not be an instance method, this is quick and dirty code just here to illustrate the architecture. – Ambroz Bizjak Apr 30 '16 at 12:05
  • @AmbrozBizjak I think Rust will require you to change the strategy regarding the deletion somewhat. Unless I'm missing something (obvious), the possibility to delete something must be made explicit: you must be the (sole) owner of it. This means that a `fn timer_expired(&mut self)` won't work. You could use something like `fn timer_expired(self) -> Option`, though, where you explicitly either allow the `TimerCallback` to outlive the invocation, or don't. (Of course, `unsafe` code allows you to translate your code literally from C++, but I'm assuming you don't want that.) – Thierry Apr 30 '16 at 15:24
  • From my experience, if you cannot properly model ownership, it is likely that there is a risk of shooting yourself in the foot (ie, deleting yourself indirectly by accident). Have you thought about combining `Rc`/`Weak` with `RefCell`? – Matthieu M. Apr 30 '16 at 17:51
  • @MatthieuM. I do not want to use Rc because AFAIK that involves dynamic allocation. I am looking at Rust as something to use instead of C++ for low-level event-driven code possibly running in microcontrollers, which must use NO dynamic memory and have high performance. Rust promises zero-overhead safety but it seems to be completely failing for what I need. – Ambroz Bizjak Apr 30 '16 at 18:32
  • @MatthieuM. I am currently trying to just implement the equivalent of this using unsafe code and then I'll see if it's possible to make some abstractions that would hide the unsafe bits. – Ambroz Bizjak Apr 30 '16 at 18:33
  • @MatthieuM. I have developed a set of patterns/rules that allow me to write nice and reliable event-driven code in C/C++, including deleting objects from their callbacks. For example when calling callbacks, I always directly "return" the callback, so that after the callback control effectively returns straight to the event loop, without risk that something unsafe happen on the return path. If I need to do something after the callback runs, I do it by scheduling such an action via the event loop. – Ambroz Bizjak Apr 30 '16 at 18:37
  • @AmbrozBizjak: You are using a `std::list` which will allocate memory... if you want to avoid allocating you could use an intrusive list (though in Rust it'll require some `unsafe` code it can be implemented). Could all timer objects live longer than the loop? (if so we could just take reference to them in Rust, although mutability would still be an issue of course) – Matthieu M. Apr 30 '16 at 20:07
  • @AmbrozBizjak: With regards to 'the class being called back has full freedom to access itself': you can use pattern matching to obtain simultaneous mutable references to as many subfields of the parent object as you need: http://stackoverflow.com/a/32404611/109549 – zslayton Apr 30 '16 at 23:32
  • @MatthieuM. The std::list was just the quickest thing I found to make it work. I have mentioned in the question I would typically use an intrusive list. – Ambroz Bizjak May 04 '16 at 17:11
  • @zslayton: That is good to know, but I think it doesn't happen here. The main issue with regard to references seems to be how to "go from" references from lower-level to references to higher-level objects - in this example, from Timer to TimerUser. I don't see any scalable way without using pointers "upwards" in the object hierarchy. – Ambroz Bizjak May 04 '16 at 17:13

0 Answers0