Do I understand function bodies correctly?

Hello friends. I’m working on a 6502 emulator as a hobby project. Due to how old it is, most of the devices that use a 6502 have direct memory mapped I/O. This means that, since I’d like to be able to resuse the emulation code for different devices (i.e. in a NES, in an Apple II etc), it would be advantageous to be able to have different functions for writing/reading to the device memory (since the location and behaviour of the memory mapped I/O portions will differ from each device to the next).

Is this a valid usecase for function bodies? Here’s some basic pseudocode that demonstrates how I more or less understand it would work. To be clear, the read and write functions won’t change at runtime and will always be comptime known.

main.zig

const STDT = @import("things.zig");

const struct_that_does_thing: STDT = .{.read = read, .write = write};

var storage: [5]u8 = @splat(0);

struct_that_does_thing.doThings();

....

fn read(idx: u8) u8 {
  //do some stuff if idx is a certain value
  //...
  return storage[idx];
} 

fn write(idx: u8, val: u8) void {
  //do some stuff if idx is a certain value
  //...
  storage[idx] = val;
}

things.zig

read: ReadType,
write: WriteType,
const WriteType = fn (idx: u8, val: u8) void;
const ReadType = fn (idx: u8) u8;

...

fn doThings(self: @This()) {
  if(some_other_condition) {
    self.write(2, 12);
  }
}

Is this a correct usage of function bodies, or would I be better of with a pointer? I’m also not sure if it matters whether storage is a stack variable or a global or a variable in some struct that might also have a STDT field.

(In case you’re wondering, the reason I’d like to avoid function pointers here is because the 6502 runs at 1.5Mhz and every single one of those cycles either reads or writes.)

it shouldn’t since you are copying to/from. I assume you meant the stack of a function higher up in the call stack.

I think you’re justified in using function bodies here. Though, my first thought was just to give each component a slice of memory it can do what it wants with. But I think that would be harder to make work well, and I’m not familiar with 6502.

I would try the generics route so that the compiler has all the comptime information it needs to avoid potential runtime overhead.

E.g. specialize the CPU type by a ‘System’ type (where System is the emulated computer system), and this has read and write methods. E.g…

pub fn Mos6502(comptime S: type) type {
    return struct {
         const Self = @This();
         system: *S;
         pub fn init(sys: *S) Self {
              return .{ .system = sys; };
         }
         pub fn tick(self: *Self) void {
              // ...
              self.system.read(...);
              self.system.write(...);
              // ...
         }
    };
}

…that way the optimizer should be able to do its thing, even inline the read/write calls.

PS: my chips emulators here are also heavily configured via generics, however I use a different approach for glueing chips in a system together (via in/out pin bitmasks - this is a very ‘clean design’ but may be slower than traditional methods: chipz/src/chips at main · floooh/chipz · GitHub - also see: Zig and Emulators)

3 Likes

After thinking on it for a bit, it seems that my pointer-less design would only really work if I had a single instance of the emulator running at a time (since each instance would need a separate slice for their own RAM and therefore read and write would basically need a self pointer etc) - and since I do want to at least have the option for multiple instances running at once, I guess pointers can’t be avoided. That being so, I think I’ll explore the generics option. Still, it’s nice to know I have the option of function bodies in the future for other projects.