What is the correct way to define/implement a factory trait?


#1

I’m trying to build a system where a Server has to work with multiple transports (TTransport below). The caller instantiates the Server with a concrete TTransportFactory that can build the transport types they want to use at runtime (i.e. each accepted connection wraps a TcpStream with the constructed transport to process incoming/outgoing bytes). I tried the code below but can’t get the trait bounds to work properly.

use std::cell::RefCell;
use std::rc::Rc;

//
// TTransport and TTransportFactory
//

pub trait TTransport { }

pub type RcTTransport = Rc<RefCell<Box<TTransport>>>;

pub trait TTransportFactory<T: TTransport> {
    fn new(&self, inner: RcTTransport) -> T;
}

//
// TFakeTransport and TFakeTransportFactory
// (implements/builds an implementation of TTransport)
//

struct TFakeTransport { }

impl TTransport for TFakeTransport { }

struct TFakeTransportFactory { }

impl TTransportFactory<TFakeTransport> for TFakeTransportFactory {
    fn new(&self, inner: RcTTransport) -> TFakeTransport {
        unimplemented!()
    }
}

//
// TServer (builds a TTransport)
//

struct TServer<T, F> where T: TTransport, F: TTransportFactory<T> {
    factory: F,
}

impl<T, F> TServer<T, F> where T: TTransport, F: TTransportFactory<T> {

    pub fn listen() {
        unimplemented!()
    }
}

A few questions:

  1. Is this a case where I have to use associated types? (i.e. TTransport is an associated type for TTransportFactory? If so, how do I indicate its bound in Server?
  2. Does TTransportFactory have to return a boxed TTransport?

#2

What’s the role of inner: RcTTransport in this method?


#3

@colin_kiegel:

TTransport instances can be layered. At the bottom is the stuff that talks over the wire/IPC etc. and you layer buffering, framing, etc. on top.


#4

Hm, ok. I noticed that you are using type erasure (aka trait objects, e.g. Box<TTransport>) to embed lower layers into new layers. This has both advantages and disadvantages. Pro: adds a convenient abstraction - Con: blocks some powerful compiler optimisations like inlining.

Let’s assume we try to get rid of trait objects to improve the runtime performance, then you have two options. But let’s first look at this question:

Do you want to be able to compose layers in different ways?
Like Foo<Buffer<Wire>> vs. Foo<Wire>?

  1. If not, then the layer Foo always expects the same inner type and you can make this an associated type TTransport or maybe TTransportFactory if that makes more sense for some reason.
  2. If yes (which I assume), then you have to work with generic functions and trait boundaries, which result in comparable abstraction to trait objects / type erasure, but better better performance in exchange for slightly reduced coding ergonomics.

Option 2 could roughly look like this

pub trait TTransportFactory<T: TTransport> {
    fn new<Inner: TTransport>(&self, inner: Inner) -> T;
}

If you need Rc<RefCell<Box<Inner>>> instead of Inner, you could do that - but again this will come at some runtime costs. And I think you should try to avoid that.

Does that help you? If not or if you run into new problems please post the specific compiler error you are having problems with.
Do you want to explain why you tried Rc<RefCell<Box<...>>>?


#5

Yes, I definitely need to compose layers in different ways. These two stacks are equally valid:

+-----------+
|  Framed   |
+-----------+
| TcpStream |
+-----------+

+-----------+
|  Buffered |
+-----------+
|  Framed   |
+-----------+
|   Memory  |
+-----------+

The idea you’ve proposed is very similar to what I have, except that I use Rc<RefCell<Box<TTransport>>> for inner. Unfortunately I have to do that. I’m actually designing an implementation for an established API, and there it’s expected that we have an incoming transport and an outgoing transport. These may be separate (case 1 below) or share the same inner instance (case 2). It’s to support (2) that I need my transport to be boxed and ref-counted.

1. Separate instances

+---------------------------+
|            ...            |
+---------------------------+
| i: Buffered |  o: Framed  |
+---------------------------+
|  TcpStream  |  TcpStream  |
+---------------------------+

2. Shared instance

+---------------------------+
|            ...            |
+---------------------------+
| i: Buffered |  o: Framed  |
+---------------------------+
|         TcpStream         |
+---------------------------+

#6

Just in case someone is wondering, I came up with the following.

First, I redefined TTransportFactory. It’s now parameterized on two types: T - the transport being created and I - the transport being wrapped. This is a small variation on what @colin_kiegel suggested above.

pub trait TTransportFactory<T: TTransport> {
  fn new<I: TTransport>(&self, inner: Rc<RefCell<Box<I>>>) -> T;
}

Next, I redefined the TServer as below. This is almost exactly what I had above with the addition of PhantomData. In the original code since T is only used to qualify the TTransportFactory the complier bombs with an error, claiming that T is an unused type parameter (super puzzling to me because it is used). To get around that you have to put in a marker PhantomData field in your struct to satisfy the compiler. Despite this supported workaround I claim this behavior is a bug.

You may need the 'static since the compiler doesn’t know if T's lifetime is tied to that of F. In my case it’s not, and I need to box it - hence the 'static.

struct TServer<T, F> where T: TTransport + 'static, F: TTransportFactory<T> {
  transport_type: PhantomData<T>,
  factory: F,
}

#7

You should be able to write

struct TServer<T: TTransport>  {
  factory: TTransportFactory<T>
}

#8

You’re right! I totally blanked on that: I’ll give it a try.


#9

Ah. You can’t do that because TTransportFactory is a trait. Super annoying.

EDIT: Unless you box the TTransportFactory. Which feels ridiculous. So many levels of indirection, and it feels like a lot of frustrating mental overhead.


#10

Interesting - I have no more ideas about TServer then… Sorry.

Maybe you can abstract this shared vs. non-shared decision away as some kind of pseudo-layer. I.e. you could start with T: TTransport and wrap that as type Shared<T> = Rc<RefCell<Box<T>>> and impl<T> TTransport for Shared<T> where T: TTransport. I am not sure if that works… What do you think?


#11

Have you considered something like the following?

/// Factory trait with an associate Item type
///
/// The factory build function will return an Item
trait Factory {
    type Item : TTransport;
    fn build(&self) -> Self::Item;
}

/// The server
struct Server<F: Factory<Item=T>, T> {
    factory: F
}

Here’s the full code. I would’ve provided a playpen link, but it keeps giving me a 500.

/// Factory trait with an associate Item type
///
/// The factory build function will return an Item
trait Factory {
    type Item : TTransport;
    fn build(&self) -> Self::Item;
}

trait TTransport {}

/// The server
struct Server<F: Factory<Item=T>, T: TTransport> {
    factory: F
}

struct CupFactory;
struct Cup;

impl Factory for CupFactory {
    type Item = Cup;
    fn build(&self) -> Cup { Cup }
}

impl TTransport for Cup {}

fn main() {
    let server = Server { factory: CupFactory };
}

Of course, if you want to be able to swap in a different factory or have multiple factories with different item types, you’d probably need to return boxed TTransport objects instead of using the associated type.

Edit: playpen link: https://is.gd/eL3BP8