Best practices for &self in array of function pointers

To handle I/O, my emulator uses a table of function pointers. The problem is that many of these functions are within structs and require &self or &mut self pointers.

In C, this was easily solvable -- I could store a void* that held a reference to self:

void write_port(void* opaque, uint32_t port, uint32_t data) {
    device_t* self = opaque;
    self->field = some_value; 
}

write_ports[port & 0xFFFF](devs[port & 0xFFFF], port, data);

In JavaScript, I could call .bind on the function, implicitly passing the this pointer whenever I called it.

Ideally, I'd like to prevent creating function pointer types for each device -- I'd like single unified function pointer for all reads, another for writes, so on. I was looking into using raw pointers, but it would be nice to avoid unsafe code. Is there a way to do this elegantly and cleanly in Rust?

The reason that I can't use a simple match port {} (which would be infinitely easier) is that some devices can have their I/O addresses remapped at runtime.

If you know the type and method to call, then take advantage of the fact that:

object.method();

is the same as:

ObjectType::method(object);

So self is not special. It's just the first argument to a function.

You should also be able to coerce method pointer to fn(&ObjectType) function pointer.

let fnpointer: fn(&ObjectType) = ObjectType::some_method;

If you don't know types, then use closures. Closures are fat pointers (data pointer + function pointer), so you will need double indirection Box<Box<dyn FnMut()>> to be able to cast them as a thin void*.

2 Likes

Thanks for the pointers (no pun intended haha).

How would I store these &mut self references? In C, I just stored the void* opaque stuff in a separate array, and it worked because C was rather flexible with its casts. I was thinking of using an array of *mut _ and casting to my desired pointer type using .as_ref(), but I'm wondering if there's a better solution.

&mut is not a general-purpose pointer like in C, but a temporary borrow tied to a scope. It's super dangerous to use it with C callbacks, unless you can guarantee these callbacks will happen synchronously in the same scope, and not later.

You would typically use Box::new(object) to guarantee a stable address for it (otherwise objects are on stack and can move). And then Box::into_raw to pass ownership to C, or cast to a pointer with as if you're lending to C.

Oh, and you don't store the object as &mut . It's not possible - temporary borrows can't store any data by definition. You only temporarily borrow some other type as &mut at the last second when you call a method.

How about storing addr->device mapping in a HashMap<u16, Device>? You can define a Device struct, e.g.:

struct Device {
  // some state
}

impl Device {
   fn read(&self) {}
   fn write(&mut self, data: u32) {}
}

Then populate the map with devices and their initial port addresses. If the port is remapped, update the map. You can similarly use an array instead of a map and maintain the devices in there in a similar manner. Pseudocode:

let mut devices: HashMap<u16, Device> = ...
loop {
    // simulation
   let port: u16 = ...;
   devices.get(&port).unwrap().read();
   ...
   let data: u32 = ...;
   devices.get_mut(&port).unwrap().write(data);
   ...
   // check if ports remapped, if so, update `devices`
}

Maybe there's something crucial I'm missing though?

I really like the idea, but how would you go around creating multiple kinds of devices? I see how this could work with abstract classes, like the ones in Java, and perhaps it could work with traits. But I don't believe there's a way to do HashMap<u16, DeviceTrait>, unfortunately.

Trait objects are one way to store pointers to multiple types in a single collection.

As @mbrubeck mentioned, you can use trait objects to abstract away the device impl. Another option is to use an enum if the set/type of devices is known a priori and controlled by your simulation.

Yes, the latter is what I've decided to go with.

Thank you all for your help, really appreciate it.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.