Rust 中带有回调的事件驱动设计

Amb*_*jak 5 callback event-driven rust

我想知道如何在 Rust 中使用带有回调的可组合事件驱动设计。根据我现有的实验,我开始怀疑 Rust 中的所有权系统更适合自上而下的过程代码,并且在事件驱动设计中回调所需的父对象的引用方面存在问题。

本质上,我希望看到以下 C++ 代码的 Rust 等效项。该代码实现了一个使用繁忙循环调度 Timer 事件的 EventLoop、一个带有 timer_expired 回调的 Timer 类以及一个以 500 毫秒为间隔调度计时器的 User 类。

#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;
}
Run Code Online (Sandbox Code Playgroud)

该代码按照标准 C++14 工作,我相信是正确的。请注意,在认真的实现中,出于性能原因,我将使 running_timers 成为侵入性链接列表而不是 std::list 。

以下是该解决方案的一些属性,我需要在 Rust 等效项中看到它们:

  • 可以无限制地添加/删除计时器,对计时器的分配方式/位置没有限制。例如,可以动态管理一系列类,每个类都使用自己的计时器。
  • 在timer_callback中,被回调的类可以完全自由地访问自身以及调用它的计时器,例如重新启动它。
  • 在timer_callback中,被调用的类也可以自由地删除自身和计时器。EventLoop 理解这种可能性。

我可以展示一些我尝试过的东西,但我认为它没有用。我遇到的主要痛点是通过回调特征涉及的所有父对象的引用来满足借用规则。

我怀疑 RefCell 或类似的东西可能是解决方案的一部分,可能是一个或多个具有内部不安全部分的特殊类,允许将东西粘合在一起。也许 Rust 通常提供的引用安全的某些部分只能在运行时通过恐慌来保证。

更新

我已经用 Rust 创建了一个原型实现,但它并不安全。具体来说:

  • 计时器和 EventLoop 不得移动。如果它们意外移动,则会由于使用指向这些的指针而发生未定义的行为。在 Rust 中甚至无法检测到这一点。
  • 回调实现是一种 hack,但应该可以工作。请注意,这允许同一个对象接收来自两个或多个计时器的回调,如果将特征用于回调,则这是不可能的。
  • 由于使用指向要接收回调的对象的指针,回调是不安全的。
  • 理论上,对象可以删除自身,但这实际上在 Rust 中似乎不安全。因为如果 Timer 回调最终删除了自身,那么在某个时刻就会有一个无效的 &self 引用。这种不安全性的根源在于回调指针的使用。