在 Rust 中实现回调系统时遇到问题

mia*_*lop 1 generics callback lifetime rust

(完全公开,这是我的 reddit 帖子的转发)

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

我正在致力于为定制 CPU 实现模拟器。我希望能够挂钩仿真(并稍后进行调试)。我希望为 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();
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

  • 它使最终用户的使用变得复杂(那不会是我)
  • Cpu然后就会知道太多关于什么是aHook

所以,我觉得我错过了一些东西。要么是我的 Rust 技能太低,无法找到好的解决方案,要么是我的开发技能问题。无论如何,我无法弄清楚。也许我解决问题的方式全错了,我应该把一切都颠倒过来,或者也许没有好的解决方案,我将不得不坚持生命周期/泛型。

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

Kev*_*eid 5

我也不能简单地将 theCpu作为参数传递给闭包,因为那样的话,我最终会产生循环依赖,即Cpu需要Hookrequire Cpu

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

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

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

您仍然需要一些摆弄来满足借用检查器:对 的可变访问Cpu意味着对 的可变访问hooks,并且不允许钩子直接通过访问 Cpu 来改变自身。有几种可能的技巧可以解决这个问题;最简单的一种方法是暂时删除钩子,使其仅由函数调用拥有。(这意味着将Hook始终看到Cpu不包含该钩子的 a 。)

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

这是您的整个程序,进行了足够的更改以使其编译:

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