Since the problem with your current approach is that e.g. Cpu
is supposed to have access to the whole Dmg
but it is also just a component of that struct. Since you can turn a &mut Dmg
into three references &mut Cpu, &mut Mmu, &mut Ppu
, any access to e.g. Mmu
that is accessible through Cpu
would need to alias the mutable &mut Mmu
.
Next to solutions that could be quite flexible but also potentially more complex at runtime, like what @farnbams discussed above, I think there is the option of having Cpu
just be an abstract view of the whole Dmg
. I think I have a way this could be done (writing this post as I’m implementing it):
First separate out the cpu state into its own struct
pub struct CpuState {
pc: u16,
}
then we can make Cpu
itself just a generic wrapper type
pub struct Cpu<C> {
context: C,
}
where C
will later be instantiated as Dmg
.
The Dmg
struct will be defined like this
mod cpu;
mod mmu;
use cpu::{CpuState};
use cpu::{MmuState};
pub struct Dmg {
cpu_state: CpuState,
mmu_state: MmuState,
}
Now, back in the cpu
module, we can start specifying what we need to know about the context:
pub trait CpuContext {
fn on_fetch_op(&self) -> u8;
}
I’ve decided that instead of passing pc
to on_fetch_op
directly, I’ll allow for public read-access to pc
:
Of course, passing pc
would’ve been possible too, with fn on_fetch_op(&self, pc: u16) -> u8;
impl CpuState {
pub fn pc(&self) -> u16 {
self.pc
}
}
We also need access to the CpuState
from Cpu<C>
:
pub trait CpuContext {
fn cpu_state(&self) -> &CpuState;
fn cpu_state_mut(&mut self) -> &mut CpuState;
fn on_fetch_op(&self) -> u8;
}
For convenience, let’s implement
impl<C: CpuContext> Deref for Cpu<C> {
type Target = CpuState;
fn deref(&self) -> &CpuState {
self.context.cpu_state()
}
}
impl<C: CpuContext> DerefMut for Cpu<C> {
fn deref_mut(&mut self) -> &mut CpuState {
self.context.cpu_state_mut()
}
}
Now we can start implementing methods:
impl CpuState {
pub fn new() -> CpuState {
CpuState { pc: 0 }
}
}
impl<C: CpuContext> Cpu<C> {
pub fn tick(&mut self) {
let opcode = self.fetch_op();
if opcode == 0xCB {
let cb_opcode = self.fetch_op();
self.exec_cb_op(opcode, cb_opcode);
} else {
self.exec_op(opcode);
}
}
fn fetch_op(&mut self) -> u8 {
let opcode = self.context.on_fetch_op();
self.pc += 1;
opcode
}
fn exec_op(&self, code: u8) {
// unimplemented
}
fn exec_cb_op(&self, code: u8, cb_code: u8) {
// unimplemented
}
}
Here’s a stub Mmu
for now:
pub struct MmuState;
impl MmuState {
pub fn new() -> Self {
MmuState
}
}
pub trait MmuContext {
fn mmu_state(&self) -> &MmuState;
fn mmu_state_mut(&mut self) -> &mut MmuState;
}
pub struct Mmu<C>{
context: C,
}
impl<C: MmuContext> Deref for Mmu<C> {
type Target = MmuState;
fn deref(&self) -> &MmuState {
self.context.mmu_state()
}
}
impl<C: MmuContext> DerefMut for Mmu<C> {
fn deref_mut(&mut self) -> &mut MmuState {
self.context.mmu_state_mut()
}
}
impl<C: MmuContext> Mmu<C> {
// guessing the type here
pub fn read(&self, addr: u16) -> Option<u16> {
Some(42) // stub
}
}
In case Mmu<C>::read
needs mutable access to self
, you’d need to change the type of CpuContext::on_fetch_op
to require &mut self
as well, but everything still can be made to work.
Finally, let’s pull everything together back in the top level module with Dmg
:
use cpu::{CpuState, CpuContext};
use mmu::{MmuState, MmuContext};
pub struct Dmg {
cpu_state: CpuState,
mmu_state: MmuState,
}
impl Dmg {
pub fn new() -> Self {
Self {
cpu_state: CpuState::new(),
mmu_state: MmuState::new(),
}
}
}
impl CpuContext for Dmg {
fn cpu_state(&self) -> &CpuState {
&self.cpu_state
}
fn cpu_state_mut(&mut self) -> &mut CpuState {
&mut self.cpu_state
}
fn on_fetch_op(&self) -> u8 {
// TOOD
// how to access method `Cpu<C>::read` here???
42
}
}
impl MmuContext for Dmg {
fn mmu_state(&self) -> &MmuState {
&self.mmu_state
}
fn mmu_state_mut(&mut self) -> &mut MmuState {
&mut self.mmu_state
}
}
The remaining problem is: How do I get a &Mmu<Dmg>
from a &Dmg
to implement on_fetch_op
?
We can use the ref-cast crate here.
use ref_cast::RefCast;
#[derive(RefCast)]
#[repr(transparent)]
pub struct Cpu<C> {
context: C,
}
use ref_cast::RefCast;
#[derive(RefCast)]
#[repr(transparent)]
pub struct Mmu<C>{
context: C,
}
Now we can do:
use ref_cast::RefCast;
use cpu::{Cpu, CpuState, CpuContext};
use mmu::{Mmu, MmuState, MmuContext};
impl Dmg {
fn cpu(&self) -> &Cpu<Self> {
Cpu::ref_cast(self)
}
fn cpu_mut(&mut self) -> &mut Cpu<Self> {
Cpu::ref_cast_mut(self)
}
fn mmu(&self) -> &Mmu<Self> {
Mmu::ref_cast(self)
}
fn mmu_mut(&mut self) -> &mut Mmu<Self> {
Mmu::ref_cast_mut(self)
}
}
And use it:
fn on_fetch_op(&self) -> u8 {
self.mmu().read(self.cpu().pc()).unwrap()
}
Here’s the full code. (Including the simplification described below.)
Edit: One further simplification: Using AsRef
and AsMut
supertraits instead of the cpu_state
and cpu_state_mut
trait methods:
pub trait CpuContext: AsRef<CpuState> + AsMut<CpuState> {
fn on_fetch_op(&mut self) -> u8;
}
impl<C: CpuContext> Deref for Cpu<C> {
type Target = CpuState;
fn deref(&self) -> &CpuState {
self.context.as_ref()
}
}
impl<C: CpuContext> DerefMut for Cpu<C> {
fn deref_mut(&mut self) -> &mut CpuState {
self.context.as_mut()
}
}
Then these can be implemented using the derive_more crate, saving some boilerplate code:
use derive_more::*;
#[derive(AsRef, AsMut)]
pub struct Dmg {
cpu_state: CpuState,
mmu_state: MmuState,
}
impl CpuContext for Dmg {
fn on_fetch_op(&mut self) -> u8 {
let pc = self.cpu().pc();
self.mmu_mut().read(pc).unwrap()
}
}