在 Rust 中实现回调系统的问题

Troubles implementing a callback system in Rust

(完全公开,这是来自我的 reddit post 的回复post)

首先我想声明我真的不是开发人员而且我是 Rust 的初学者。我有一个可能微不足道的问题。

我正在为自定义 CPU 实现模拟器。我希望能够挂钩仿真(并在以后对其进行调试)。我想要 单独的 crates 用于 CPU、挂钩系统和模拟器,我希望我的 CPU 不知道挂钩系统的实现细节,反之亦然。但是我在建模时遇到了问题。

现在,我有类似的东西:

// crate CPU
    pub struct Cpu {
        hooks: HashMap<VirtAddr, hook::Hook>,
    }

    impl Cpu {
        pub fn add_hook(&mut self, hook: hook::Hook) {
            self.hooks.insert(hook.addr(), hook);
        }

        pub fn step(&mut self) {
            hook::hook_before!();
        }
    }


// crate HOOK
    pub struct Hook {
        addr: VirtAddr,
        callback: Box<dyn FnMut(VirtAddr) -> Result<String>>
    }

    impl Hook {
        pub fn new(
            addr: VirtAddr,
            callback: impl FnMut(VirtAddr) -> Result<String> + 'static,
        ) -> Self {
            Self {
                addr,
                callback: Box::new(callback),
            }
        }

        pub fn run(&mut self, addr: VirtAddr) -> Result<String> {
            (self.callback)(addr)
        }

        #[macro_export]
        macro_rules! hook_before {
            // do something
            hook.run()
        }
    }


// crate EMU
    pub struct Emu {
        cpu: cpu::Cpu,
    }

    impl Emu {
        pub fn add_hook(&mut self, hook: hook::Hook) {
            self.cpu.add_hook(hook);
        }

        pub fn run() {
            self.cpu.step();
        }
    }

// user's crate
fn main() {
    // create emu
    {
        let h = hook::Hook::new(
            VirtAddr(0x00016d),
            // this is VERY WRONG
            |addr| {
                let cpu = emu.cpu();
                // do stuff with the CPU
            },
        );
        emu.add_hook(h);
    }
    emu.run();
}

这不起作用,因为 rustc 告诉我我的闭包可能比当前的 (main) 函数长寿,这完全公平,因为 'static 生命周期。

这意味着我应该在我的 Hook 定义中添加生命周期,以明确通知 rustc 闭包 不能 比函数长寿。但是,我必须添加 CpuEmu 的定义。 如果我对闭包使用泛型而不是 Box<dyn>,情况也是如此。 我也不能简单地将 Cpu 作为参数传递给闭包,因为那样的话,我最终会产生循环依赖,Cpu 需要 Hook,而 Cpu 需要 Cpu。 我也不能使用函数指针 (fn),因为它无法捕获其上下文并且需要使用 Cpu 作为参数。

您可以说前两个解决方案很好,但我发现了多个问题:

所以,我觉得我错过了什么。要么是我的 Rust 技能太低,找不到好的解决方案,要么是我的开发技能。无论如何,我无法弄清楚。也许我在解决问题时完全错了,我应该颠倒一切,或者也许没有好的解决方案,我将不得不坚持 lifetime/generics.

你对我有什么想法吗?也许更适合 Rust 的设计模式?我在这里阅读了很多 post 的解决方案,但似乎对我的情况没有任何帮助。

I also cannot simply pass the Cpu as a parameter to the closure because then, i would end-up with a cyclical dependency, the Cpu requiring Hook that require Cpu.

实际上,这是最好的解决方案。在其他语言中,它可能不是,但在 Rust 中,试图让 单独的钩子函数 记住 emu 将导致无法使用 emu 做任何事情,因为它已经被借用了。一般原则是,当实体之间存在抽象循环关系时(Cpu 拥有 Hooks,但 Hooks 想与 Cpu 一起工作),最好是 将关系中的一个成员借用给另一个成员,而不是试图让他们永久相互引用。

更改您的挂钩函数以具有类似 FnMut(&mut Cpu, VirtAddr) -> Result<String>.

的签名
pub struct Hook {
    pub addr: VirtAddr,
    callback: Box<dyn FnMut(&mut Cpu, VirtAddr)>
}

您仍然需要一些小动作来满足借用检查器的要求:对 Cpu 的可变访问意味着对 hooks 的可变访问,并且不允许挂钩直接改变自身 通过访问Cpu。有几种可能的技巧来处理这个问题;最简单的方法是 暂时删除挂钩 以便它完全由函数调用拥有。 (这意味着 Hook 将始终看到不包含该挂钩的 Cpu。)

impl Cpu {
    pub fn step(&mut self) {
        let addr = VirtAddr(0x00016d); // placeholder
        if let Some(mut hook) = self.hooks.remove(&addr) {
            hook.run(self, addr);
            self.hooks.insert(addr, hook);
        }
    }
}

这是你的整个程序,经过足够的修改使其可以编译:

use std::collections::HashMap;

mod cpu {
    use super::*;

    #[derive(Clone, Copy, Eq, Hash, PartialEq)]
    pub struct VirtAddr(pub u32);
    
    pub struct Cpu {
        hooks: HashMap<VirtAddr, hook::Hook>,
    }

    impl Cpu {
        pub fn new() -> Self {
            Self { hooks: HashMap::new() }
        }

        pub fn add_hook(&mut self, hook: hook::Hook) {
            self.hooks.insert(hook.addr, hook);
        }

        pub fn step(&mut self) {
            let addr = VirtAddr(0x00016d); // placeholder
            if let Some(mut hook) = self.hooks.remove(&addr) {
                hook.run(self, addr);
                self.hooks.insert(addr, hook);
            }
        }
    }
}

mod hook {
    use super::cpu::{Cpu, VirtAddr};

    pub struct Hook {
        pub addr: VirtAddr,
        callback: Box<dyn FnMut(&mut Cpu, VirtAddr)>
    }

    impl Hook {
        pub fn new(
            addr: VirtAddr,
            callback: impl FnMut(&mut Cpu, VirtAddr) + 'static,
        ) -> Self {
            Self {
                addr,
                callback: Box::new(callback),
            }
        }

        pub fn run(&mut self, cpu: &mut Cpu, addr: VirtAddr) {
            (self.callback)(cpu, addr)
        }
    }
}

mod emu {
    use super::*;

    pub struct Emu {
        cpu: cpu::Cpu,
    }

    impl Emu {
        pub fn new() -> Self {
            Self { cpu: cpu::Cpu::new() }
        }
    
        pub fn add_hook(&mut self, hook: hook::Hook) {
            self.cpu.add_hook(hook);
        }

        pub fn run(&mut self) {
            self.cpu.step();
        }
    }
}

fn main() {
    let mut emu = emu::Emu::new();
    {
        let h = hook::Hook::new(
            cpu::VirtAddr(0x00016d),
            |_cpu, _addr| {
                println!("got to hook");
            },
        );
        emu.add_hook(h);
    }
    emu.run();
}

所以,感谢 jam1garner 在 reddit 上的评论,我能够通过在 Hook class.

上使用泛型来解决这个问题

以下代码现在有效:

use std::collections::HashMap;

pub struct Hook<T> {
    callback: Box<dyn FnMut(&mut T) -> String>,
}

impl<T> Hook<T> {
    pub fn new(callback: impl FnMut(&mut T) -> String + 'static) -> Self {
        Self {
            callback: Box::new(callback),
        }
    }

    fn run(&mut self, cpu: &mut T) -> String {
        (self.callback)(cpu)
    }
}

pub struct Cpu {
    pub hooks: HashMap<u32, Hook<Cpu>>,
}

impl Cpu {
    fn new() -> Self {
        Cpu {
            hooks: HashMap::new(),
        }
    }

    fn add_hook(&mut self, addr: u32, hook: Hook<Cpu>) {
        self.hooks.insert(addr, hook);
    }

    fn run(&mut self) {
        let mut h = self.hooks.remove(&1).unwrap();
        println!("{}", h.run(self));
        self.hooks.insert(1, h);
        self.whatever();
    }

    fn whatever(&self) {
        println!("{:?}", self.hooks.keys());
    }
}

pub struct Emu {
    cpu: Cpu,
}

impl Emu {
    fn new() -> Self {
        Emu { cpu: Cpu::new() }
    }

    fn run(&mut self) {
        self.cpu.run();
    }

    fn add_hook(&mut self, addr: u32, hook: Hook<Cpu>) {
        self.cpu.add_hook(addr, hook);
    }
}

fn main() {
    let mut emu = Emu::new();
    {
        let h = Hook::new(|_cpu: &mut Cpu| "a".to_owned());
        emu.add_hook(1, h);
    }
    emu.run();
}

看游乐场:https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=971a66ec8baca15c8828a30821869539