Access multiple structs of trait `Node` as both generic and enum variant

Hi! I've recently run into a problem with my code design, I was wondering if anyone who's worked a lot with rust traits and enums could help out.

First, a bit of background: I'm designing a node-based modular synthesizer of sorts (think blender nodes or node-red). I decided to use electron to design the UI and rust to run the backend audio engine.

The main structure in my design is a node. Every node type is a unique struct that implements the Node trait. Each node keeps track of its own internal state and has its own unique exposed methods (filter exposes a set_frequency function, gain exposes a set_gain function, etc). Nodes need to be both accessible in a generic way (node.list_input_socket_types) and by specific type (gain_node.set_gain). Generic, so that there's a unified UI experience, and specific to allow fast implementations when chaining nodes together and use static analysis as much as possible.

To start off my design, I created a Node trait, that has a common API that all nodes must have (list_properties, list_input_sockets, etc). Now, to sync a node to the frontend (electron) I serialize the values returned from the required Node methods into json and send it through a socket.

All is fine and well until I need to serialize the specifics of the structure (to save to a file). When serializing it to a file I need to account for the fact that each Node has a unique underlying struct.

My first attempt was using enum that contains each of the possible node types, example:

#[derive(Serialize, Deserialize)]
pub enum NodeVariant {
    GainGraphNode(GainGraphNode),
    FilterGraphNode(FilterGraphNode),
    MonoBufferPlayerNode(MonoBufferPlayerNode)
}

With this enum structure, I can use serde to serialize and deserialize the different variants to disk. However, now I can't access the generic methods of the contained node, without doing something obnoxious like

match node_variant {
    GainGraphNode(node) => node.list_properties(),
    FilterGraphNode(node) => node.list_properties(),
    MonoBufferPlayerNode(node) => node.list_properties()
}

which obviously isn't sustainable as I add more node types.

I want to be able to do something like node_variant.as_ref_to_node().list_properties(), but I can't seem to figure out how I would do that. I could consume the enum and return a generic Node, but then I can't use it as its specific type.

I considered doing something like

pub struct NodeVariantContainer {
    variant: NodeVariant,
    variant_as_node: Box<dyn Node>
}

But from my understanding of rust, that would require owning the node in both the enum and owned as a dyn Node, thereby breaking the borrowing rules.

Is there a way to switch between referencing the node as a Node, and referencing it as a NodeVariant? Or some way to be able to both serialize the entire structure to disk and expose a generic interface across nodes to synchronize with the client? I'm open to redesigning everything if I've backed myself into a corner too :slight_smile:

One option could be to serialize them separately in some way, though this may not work well in this case. Imagine e.g. instead of Vec<Node> having

#[derive(Serialize, Deserialize)]
struct NodeList {
    gain_graph_nodes: Vec<GainGraphNode>
   // ...
}

There are some macros to help with the boilerplate, such as enum_dispatch.

You could do something like this

impl NodeVariant {
    fn as_node(&self) -> &dyn Node {
        match self {
            Self::A(a) => a as &dyn Node,
            Self::B(b) => b as &dyn Node,
        }
    }

    fn as_node_mut(&mut self) -> &mut dyn Node {
        match self {
            Self::A(a) => a as &mut dyn Node,
            Self::B(b) => b as &mut dyn Node,
        }
    }
}

or, using the AsRef and AsMut traits

impl<'a> AsRef<dyn Node + 'a> for NodeVariant {
    fn as_ref(&self) -> &(dyn Node + 'a) {
        match self {
            Self::A(a) => a as &dyn Node,
            Self::B(b) => b as &dyn Node,
        }
    }
}

impl<'a> AsMut<dyn Node + 'a> for NodeVariant {
    fn as_mut(&mut self) -> &mut (dyn Node + 'a) {
        match self {
            Self::A(a) => a as &mut dyn Node,
            Self::B(b) => b as &mut dyn Node,
        }
    }
}

Thank you! For some reason I got it in my head that whenever I used dyn anything I had to wrap it in a box, but when you put that AsRef implementation there it cleared up my thought process! :slight_smile: Maybe I need to read about dyn closer...

One question on maintaining the code, would it be good to use a macro in the as_ref and as_mut match statements when for when I add new enum variants, or should I continue doing it manually?

I think that's pretty common, since that's how it's most often used. You need some form of indirection, but that can come in many forms like &dyn Node or Arc<dyn Node>.

I'd just do it manually. I use macros to avoid duplicated code a decent amount, but I think in this case the benefits are somewhat limited, as the code for each variant is short and clear, and the compiler will catch any mistakes you make, like forgetting a variant or making a typo in one branch.

That makes sense, thank you!

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.