#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