Passing instances to other instances

If I create instances such as:

let i_seq SeqData = protocol::seq_man::SeqData::new();
let i_cc CCDataMutex= protocol::cc_out::CCDataMutex::new();

but want to call methods on those instances from a different module or several different modules I can pass those instances simply by:

let mut i_udp = udp::udp_man::UDPdata::new(i_seq, i_cc);

and I can call methods on those instances from UDPdata.

However, once I store those in the UDPdata struct I can no longer pass them on to another instance as they are then mutable. Do I have to create my instances as non-mutable in one place and then pass them to all other instances that require them rather than hierarchically or is there another way to handle this? I know CCDataMutexcan be called from several places so its already wrapped in a Mutex and I know SeqData won't have conflicts.
I don't think Arc helps as its content isn't mutable.

I’m having a hard time following your explanation/question, perhaps more concrete code examples might help? The way you describe problems with passing a value to multiple places and mutability suggests you might need to look up / learn more about Rust’s ownership model; there’s a chapter in the book as a good starting point, but feel free to ask more questions ^^. Assuming your troubles are in trying to cope with some compilation errors you don’t quite know how to solve correctly, try to include code samples and error messages in your questions.

4 Likes

Take the simple case of an OO language. I create instances of classes maybe during program initialisation. The methods on those instances may be called from other instances, not in the same module that created them. In order that another instance can call those methods it needs a reference. Now to my mind protection is at the method implementation level using a Mutex, not at the level of the method call.

Now in Rust I simply want to do the same thing but it stops me from passing the object or a reference to it as soon as it becomes mutable. The question is what is the correct pattern for passing instances as parameters and then having those instances store them in their own struct (works for one level) and then pass them down the hierarchy (does not work).

78 | let mut i_udp_writer = udp_writer::UDPWData::new(arc1, self.i_seq, self.i_cc);
| ^^^^^^^^^^ move occurs because self.i_seq has type SeqData, which does not implement the Copy trait

error[E0507]: cannot move out of self.i_cc which is behind a mutable reference
--> src\udp\udp_man.rs:78:76

If it's still unclear I will knock up a simple example to demonstrate.

If you are talking about dependency injection, it can be done in Rust as well if you own the struct.

An example might be best.


I do think there's some terminology difficulty going on. The ways in which types are mutable or not are

  • When borrowing
    • Is the borrow exclusive (which we unfortunately call &mut) or shared (&)
  • When supporting "interior mutability" (aka shared mutability)
    • That is, the ability to mutate given a shared reference (&)
      • Usually but not always by providing some checked way to ensure unique access (a &mut)
    • E.g. Cell, RefCell, Mutex, RwLock, ...

Bindings can be mutable or not, which can be considered a hard-coded lint. But you can always rebind a non-mutable binding as mutable. It doesn't change the types involved or enable / disable the ability to give away ownership. "Create something as non-mutable in one place, then put it in a struct to make it mutable" doesn't make any sense to me.


When I see

cannot move out of self.i_cc which is behind a mutable reference

I think this is more about Rust's move semantics than mutability per se. Rust's moves are destructive (for non-Copy types) and non-overloadable. The error is because you can't invalidate something that you're only borrowing by moving fields out of it. You can't move non-Copy data out from behind a shared reference either. You can pass a &mut self.i_cc or &self.i_cc somewhere.

If you need multiple structs to own the same data, you need some sort of shared ownership. And that's what types like Arc give you. If you need shared mutability on top of your shared ownership, then you'll need something like an Arc<Mutex<_>>.

3 Likes

Very rough rule of thumb for avoiding a bad time in Rust: store by value, pass by reference.

I probably need to step back and think whether a conventional structure works in Rust. I have about 15 modules, most in OO style and a few procedural and a few threads. Problem is they can't talk to each other so isolated islands right now.

Yeah, it sounds like you're taking a typical OO approach where objects will hold references to other objects so they can call methods to communicate. If you were to draw the pointers out, this would look like a big interconnected web.

The problem is this architecture almost always requires shared mutation, and Rust (deliberately) adds friction (i.e. Rc<RefCell<T>> and friends) to steer you away from this approach. Instead, Rust really likes your code's "ownership story" to be tree-like.

The nice thing is that you shouldn't need to change your approach to coding too much to satisfy this. The main tweak to the way you do Dependency Injection is instead of storing dependencies as fields, pass a reference into the function that needs it.

For example, instead of doing something like this:

struct PacketSerializer {
  client: HttpClient,
  // other state
}

impl PacketSerializer {
  fn send(&mut self, packet: &Packet) { ... }
} 

I might write my code like this

struct PacketSerializer { /* other state */ }

impl PacketSerializer {
  fn send(&mut self, packet: &Packet, client: &mut HttpClient) { ... }
} 

This means the PacketSerializer doesn't need to own HttpClient to do its job and the caller can enforce who has access to the HttpClient at every point in your program.

It's also worth separating "plain old data" structs (bags of variables with no meaningful behaviour) from "objects" (stateful objects that contain business logic). There are no hard-and-fast rules around how POD types and objects should interact or who owns what (e.g. "objects should never own objects, only POD types"), but it's a good idea to get into that mindset so you avoid muddying the distinction between business logic and state.

5 Likes

Thanks for the explanation. I need to draw out the dependency map now and see if I can make it tree-like and/or how I could orchestrate the process from a single module that has all the instances or maybe a combination of both.

1 Like