Working with different types, but same interface (traits?)

Hello. I'm not sure how to describe this problem; I'm sure there is a name for this concept, but I don't know what it is.

let stream = if needs_tls {
  (returns TlsStream<TcpStream>)
} else {
  (returns TlsStream)
}

let mut reader = BufReader::new(stream);

stream.write(...);
reader.read(...);

Obviously that won't work because those are two different types, but both types implement the Read and Write traits, which is what I care about.

How to do this? All I really care about are the Read and Write traits, but a trait object only allows to specify one trait? Also, I read somewhere that trait objects are for doing "Java like OOP". What's the Rust way for doing this?

It feels like I want to say that stream is "something" that implements both Read and Write then get all the compile time checking based on that.

Apologies for butchering various static typing terminology and concepts... :grimacing:

Thanks for the help!

Exactly like you wanted to do. You can always declare “combining ReadWrite trait” which would be automatically implemented for all types which implement Read and Write.

You can not do “Java like OOP” in Rust because that would mean implementation inheritance.

Traits implement interface inheritance and that one is well-supported in Rust. In Java that would be use of interfaces and wouldn't be as flexible (since you can not make foreign type implement your own interface).

1 Like

Using a trait object with a custom trait is one option. You could also use an enum with a case for each type. Then just implement Read and Write on the enum.

1 Like

a trait object can only have a single trait because of the way it works.
a trait object is composed of 2 pointers, one for the actual data, and one for the trait's v-table. we can't have 2 traits, because that would mean 2 v-tables (it would be cool if we could have multiple v-table trait objects, it seems like something the compiler could figure out).

what we can do, however, is to combine the v-tables into a single one, by implementing a new "super"-trait:

trait ReadAndWrite : Read + Write { }

and automatically implement that trait for everything that's Read and Write:

impl<T> ReadAndWrite for T where T : Read + Write { }

now you could use a ReadAndWrite trait object

1 Like

To spell out what I think VorfeedCanal was saying:

trait ReadWrite: Read + Write{}

impl<T: Read + Write> ReadWrite for T {}

and then I think you can do something like

let stream: Box<dyn ReadWrite> = if needs_tls {
  Box::new(TlsStream)
} else {
  Box::new(TcpStream)
}

stream.write(...);
1 Like

Haha, why did you delete your post? I was literally going to ask for a code example, when I saw your post pop up for for a second. It actually got me to working code... :slight_smile:

1 Like

jvcmarcenes was faster and his example was actually correct while mine had an error. But happy to hear I could help.

If you're using an if to alternate between a small number of different types that all share common traits, you could just wrap the types in either::Either, which "propagates" traits that all input types share.

Getting close...

trait ReadWrite: Read + Write {}
impl<T> ReadWrite for T where T: Read + Write {}

let stream : Box<dyn ReadWrite> = ...

let mut reader = BufReader::new(stream);
reader.read_exact(&mut recv_buf).unwrap();

// Error, borrow of moved value stream
stream.write(&send_buf).unwrap();

I've seen in other posts that you can get a BufReader and still write by using references, a la:

let mut stream = ...;
let reader = BufReader::new(&stream);
let writer = BufWriter::new(&stream);

But I'm not sure how to do that with a trait object. At some point I tried, mut_deref(), but no go.

Thanks again.

Trait objects always have to be behind some reference. For example &dyn Trait or Box<dyn Trait>.

Edit:
Ah sorry, I just realized this doesn't help you here.

The problem here is that you want two handles that should be able to read/write using the same stream. I don't think you can do that with TlsStream, because &TlsStream doesn't implement Read or Write (assuming it's from TlsStream in native_tls - Rust), so you you need to move it inside the first handle (BufReader), making it unavailable for the second. Can you make the code work if you remove the trait objects and the if needs_tls and just try to use TlsStream?

Unless you really need to pass ownership of the stream around (i.e. you want to eventually move out of the Box), it's probably simplest to operate on a &mut dyn ReadWrite instead of Box<dyn ReadWrite>.

In other words, this function is well formed and probably works as you expect:

fn use_stream(stream : &mut dyn ReadWrite)
{
  let recv_buf : &mut [u8] = ...;
  let send_buf : &[u8] = ...;

  let mut reader = BufReader::new(stream);
  reader.read_exact(&mut recv_buf).unwrap();

  stream.write(&send_buf).unwrap();
}

The slightly more difficult question is how to use this function, something like

if needs_tls
{
  use_stream(TlsStream<TcpStream>);
}
else
{
  use_stream(TcpStream);
}

is quite common but not very scalable.

You can use the box as before but use_stream doesn't care about that detail

  // this allocation is pointless, we only need it because we need actual storage to back our reference
  let streamb : Box<dyn ReadWrite> = if needs_tls
  {
    Box::new(TlsStream<TcpStream>);
  }
  else
  {
    Box::new(TcpStream);
  };

  {
    let stream = streamb.deref_mut();
    use_stream(stream);
  }

  // down here we can put `stream' into a container, or pass it to the caller,
  // or something else, but if you always just let it drop here, the Box is useless

You can rid yourself of the Box completely if you use CPS, which is syntactically bulky but sometimes very powerful. This package which makes writing CPS code in rust look much nicer than it normally would (in fact the example in this package is exactly the one you have: trying to "return" a &dyn from a function)

use with_locals::with;

#[with]
fn make_stream() -> &'ref mut dyn ReadWrite
{
  if needs_tls
  {
    &mut TlsStream<TcpStream>;
  }
  else
  {
    &mut TcpStream;
  };
}

#[with]
fn test()
{
  #[with] let stream = make_stream();
  use_stream(stream);
}

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.