Home > Mobile >  Troubles implementing a callback system in Rust
Troubles implementing a callback system in Rust

Time:12-11

(full disclosure, this is a repost from my reddit post)

First of i'd like to state that i'm really not a developer and i'm a beginner in Rust. I have a question that may be trivial.

I'm working on implementing a emulator for a custom CPU. I'd like to be able to hook the emulation (and debug it later). I want to have separate crates for the CPU, hooking system and emulator and i want my CPU to not know implementation details of the hooking system and vice versa. But i'm having problems modeling that.

Right now, i have something like:

// 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();
}

This does not work because rustc tells me that my closure may outlive the current (main) function, which is totally fair because of the 'static lifetime.

This means that i should add a lifetime to my Hook definition to explicitly inform rustc that the closure cannot outlive the function. But then, i have to add the definition of my Cpu and Emu. The same goes if i use generics for the closure instead of Box<dyn>. 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. I also cannot use a function pointer (fn) as it cannot capture its context and would require to use the Cpu as a parameter.

You could say that the first two solutions are fine but i see multiple problems with this:

  • it complicates the usage of the end-user (that will not be me)
  • Cpu will then know too much about what is a Hook

So, i feel like i'm missing something. Either my Rust skills are too low to find a good solutions or it's my developer skills. In any case, i can't figure that out. Maybe i'm addressing the problem all wrong and i should flip everything upside down, or maybe there's no good solution and i'll have to stick with the lifetime/generics.

Do you have some ideas for me? Maybe a design pattern more suitable to Rust? I read quite a few posts solutions here but nothing seems to work for my case.

CodePudding user response:

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.

Actually, this is the best solution. In other languages, it might not be, but in Rust, trying to have the individual hook functions remember emu will result in inability to do anything with emu because it is already borrowed. The general principle is that when you have an abstract circular relationship between entities like this (the Cpu owns Hooks, but the Hooks want to work with the Cpu), it is better to pass a borrow of one member of the relationship to the other than to try to have them permanently refer to each other.

Change your hook functions to have a signature like FnMut(&mut Cpu, VirtAddr) -> Result<String>.

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

You'll still need some fiddling to satisfy the borrow checker: mutable access to Cpu means mutable access to hooks, and a hook won't be allowed to mutate itself directly and through access to the Cpu. There are several possible tricks to handle this; the simplest one is to remove the hook temporarily so that it is solely owned by the function call. (This means that the Hook will always see a Cpu that doesn't contain that hook.)

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);
        }
    }
}

Here's your whole program with enough changes to make it compile:

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();
}

CodePudding user response:

So, thanks to jam1garner's comment on reddit, i was able to solve this by using generics on the Hook class.

The following code now works:

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();
}

See playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=971a66ec8baca15c8828a30821869539

  • Related