I'm thinking about trying to implement UVM in Rust. UVM is a way of modelling chips where you pass messages between components that are connected by ports. Imagine the Blender nodes interface but with little packets flowing along the edges.
In C++ this is really easy. You just do something like this:
template <class PacketType>
struct Port {
void connect_to(std::function<void (PacketType)> f) {
m_targets.push_back(f);
}
void send(PacketType packet) {
for (const auto& target : m_targets) {
target(packet);
}
}
};
struct ComponentA {
Port<Foo> foo_output;
};
struct ComponentB {
Port<Bar> bar_output;
bool foo_received = false;
void foo_input(Foo f) {
foo_received = true;
bar_output.send(...);
}
};
void test() {
ComponentA a;
ComponentB b;
a.foo_output.connect_to([](auto pkt) { b.foo_input(pkt); });
...
}
In other words, you can treat the ports as a fancy way of storing a load of callbacks. This has some nice advantages:
- It's really fast.
- When debugging the stack trace is really nice and shows you the history of how you got to where you are.
However, this cannot be directly translated to Rust for two reasons:
- The above code makes the implicit requirement that
a
andb
do not move (you can enforce it in C++ by deleting the copy/move operators) and that they all live for the same lifetime. - The callbacks need mutable access to the state in the component (e.g.
foo_received
). There can also be loops in the connections, so you can't just mutably borrow the whole component for the connection.
The "obvious" way to do this in Rust is via a load of Rc<RefCell<>>
s, but I can't work out a way to do it without splitting each component into two, kind of like this:
struct ComponentA {
state: RefCell<ComponentAState>,
}
struct ComponentAState {
foo_received: bool,
}
Because you can't do self: Rc<RefCell<Self>>
. You also end up with a ton of .borrow()
s. All very boilerplatey.
Can anyone think of a more elegant way to do this? Has anyone already done something like this?