Проблемы с реализацией системы обратного вызова в Rust

#generics #rust #callback #lifetime

#дженерики #Ржавчина #обратный вызов #срок службы

Вопрос:

(полное раскрытие, это репост из моего поста на reddit)

Прежде всего, я хотел бы заявить, что я действительно не разработчик, и я новичок в Rust. У меня есть вопрос, который может быть тривиальным.

Я работаю над внедрением эмулятора для пользовательского процессора. Я хотел бы иметь возможность подключить эмуляцию (и отладить ее позже). Я хочу иметь отдельные ящики для процессора, системы подключения и эмулятора, и я хочу, чтобы мой процессор не знал деталей реализации системы подключения и наоборот. Но у меня возникают проблемы с моделированием этого.

Прямо сейчас у меня есть что-то вроде:

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

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

        pub fn step(amp;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(amp;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(amp;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 сообщает мне, что мое закрытие может пережить функцию current ( main ), что совершенно справедливо из-за 'static срока службы.

Это означает, что я должен добавить время жизни к своему Hook определению, чтобы явно сообщить rustc, что закрытие не может пережить функцию. Но тогда я должен добавить определение моего Cpu и Emu . То же самое происходит, если я использую дженерики для закрытия вместо Box<dyn> . Я также не могу просто передать в Cpu качестве параметра замыкание, потому что тогда я бы в конечном итоге получил циклическую зависимость, Cpu требующую Hook , которая требует Cpu . Я также не могу использовать указатель на функцию ( fn ), так как он не может захватить ее контекст и потребует использования в Cpu качестве параметра.

Вы могли бы сказать, что первые два решения хороши, но я вижу множество проблем с этим:

  • это усложняет использование конечного пользователя (это буду не я)
  • Cpu тогда будет слишком много знать о том, что такое Hook

Итак, я чувствую, что что-то упускаю. Либо мои навыки Rust слишком низки, чтобы найти хорошие решения, либо это мои навыки разработчика. В любом случае, я не могу этого понять. Может быть, я неправильно решаю проблему, и мне следует перевернуть все с ног на голову, или, может быть, нет хорошего решения, и мне придется придерживаться срока службы / дженериков.

У вас есть какие-нибудь идеи для меня? Может быть, шаблон проектирования, более подходящий для Rust? Я прочитал здесь довольно много сообщений о решениях, но, похоже, в моем случае ничего не работает.

Ответ №1:

Я также не могу просто передать в Cpu качестве параметра замыкание, потому что тогда я бы в конечном итоге получил циклическую зависимость, Cpu требующую Hook , которая требует Cpu .

На самом деле, это лучшее решение. На других языках этого может и не быть, но в Rust попытка запомнить отдельные функции перехвата emu приведет к невозможности что-либо emu сделать, потому что она уже заимствована. Общий принцип заключается в том, что когда у вас есть абстрактные циклические отношения между подобными объектами (Cpu владеет перехватами, но перехватчики хотят работать с Cpu), лучше передать заимствование одного члена отношения другому, чем пытаться заставить их постоянно ссылаться друг на друга.

Измените свои функции перехвата, чтобы они имели подобную подпись FnMut(amp;mut Cpu, VirtAddr) -> Result<String> .

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

Вам все равно понадобится немного повозиться, чтобы удовлетворить проверку заимствования: изменяемый доступ к Cpu означает изменяемый доступ к hooks , и перехватчику не будет разрешено изменять себя напрямую и через доступ к процессору. Существует несколько возможных приемов для решения этой проблемы; самый простой — временно удалить перехват, чтобы он принадлежал исключительно вызову функции. (Это означает, что Hook всегда будет отображаться a Cpu , который не содержит этого крючка.)

 impl Cpu {
    pub fn step(amp;mut self) {
        let addr = VirtAddr(0x00016d); // placeholder
        if let Some(mut hook) = self.hooks.remove(amp;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(amp;mut self, hook: hook::Hook) {
            self.hooks.insert(hook.addr, hook);
        }

        pub fn step(amp;mut self) {
            let addr = VirtAddr(0x00016d); // placeholder
            if let Some(mut hook) = self.hooks.remove(amp;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(amp;mut Cpu, VirtAddr)>
    }

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

        pub fn run(amp;mut self, cpu: amp;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(amp;mut self, hook: hook::Hook) {
            self.cpu.add_hook(hook);
        }

        pub fn run(amp;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();
}
 

Комментарии:

1. Привет! Спасибо за ваш ответ. Тем не менее, из-за того, что мой процессор и хук находятся в разных ячейках, даже используя то, что вы предлагаете, я в конечном итоге сталкиваюсь с проблемой циклической зависимости, поскольку мне приходится добавлять зависимость cpu в hook ящик и наоборот. Несмотря на то, что я не могу ее использовать, это было очень полезно и информативно! Я просто отредактировал свой вопрос, чтобы отразить то, что я только что сказал о ящиках

Ответ №2:

Итак, благодаря комментарию jam1garner на reddit, я смог решить эту проблему, используя дженерики в Hook классе.

Теперь работает следующий код:

 use std::collections::HashMap;

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

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

    fn run(amp;mut self, cpu: amp;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(amp;mut self, addr: u32, hook: Hook<Cpu>) {
        self.hooks.insert(addr, hook);
    }

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

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

pub struct Emu {
    cpu: Cpu,
}

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

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

    fn add_hook(amp;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: amp;mut Cpu| "a".to_owned());
        emu.add_hook(1, h);
    }
    emu.run();
}
 

См. playground: https://play.rust-lang.org/?version=stableamp;mode=debugamp;edition=2021amp;gist=971a66ec8baca15c8828a30821869539