在 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 闭包 不能 比函数长寿。但是,我必须添加 Cpu
和 Emu
的定义。
如果我对闭包使用泛型而不是 Box<dyn>
,情况也是如此。
我也不能简单地将 Cpu
作为参数传递给闭包,因为那样的话,我最终会产生循环依赖,Cpu
需要 Hook
,而 Cpu
需要 Cpu
。
我也不能使用函数指针 (fn
),因为它无法捕获其上下文并且需要使用 Cpu
作为参数。
您可以说前两个解决方案很好,但我发现了多个问题:
- 它使最终用户(不会是我)的使用变得复杂
Cpu
就会知道什么是 Hook
所以,我觉得我错过了什么。要么是我的 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();
}
(完全公开,这是来自我的 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 闭包 不能 比函数长寿。但是,我必须添加 Cpu
和 Emu
的定义。
如果我对闭包使用泛型而不是 Box<dyn>
,情况也是如此。
我也不能简单地将 Cpu
作为参数传递给闭包,因为那样的话,我最终会产生循环依赖,Cpu
需要 Hook
,而 Cpu
需要 Cpu
。
我也不能使用函数指针 (fn
),因为它无法捕获其上下文并且需要使用 Cpu
作为参数。
您可以说前两个解决方案很好,但我发现了多个问题:
- 它使最终用户(不会是我)的使用变得复杂
Cpu
就会知道什么是Hook
所以,我觉得我错过了什么。要么是我的 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, theCpu
requiringHook
that requireCpu
.
实际上,这是最好的解决方案。在其他语言中,它可能不是,但在 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();
}