Associate generated types with a trait

I am trying to create a library that enables bidirectional RPC between two actors. The main item that is exported by this library is a struct called Runner that can be configured with an RPC client and an RPC server. The library also has two private generic enums InboundMessage and OutboundMessage which are used by the Runner to deserialize received server requests and client responses and to serialize server responses and client requests.

The library that I am creating will also export a proc macro that works similar to tarpc::service in that it consumes a trait and generates the necessary enums and structs for the Runner. For example, one could provide the following trait to my library's proc macro:

trait MyServer {
    fn say_hello(name: String);
}

Which would then generate:

enum MyServerRequest { SayHello(String) }
enum MyServerResponse { SayHello }

Now, I would to somehow associate these enums with the trait MyServer (which I can modify since it will be fed into a proc macro). The reason for this, is that I would to have users of my library provide an implementation of the trait MyServer as follows:

struct MyServerImpl;
impl MyServer for MyServerImpl {
    fn say_hello() { /* user implementation */ }
}

Which can then be used to configure my struct Runner via generics and inference as follows:

let my_server = MyServerImpl;
let runner = Runner::new(my_server, my_client)

This should allow Runner to use the generic enums OutboundMessage and InboundMessage with the client/server request/response types. I tried to add the enums MyServerRequest and MyServerResponse to the trait MyServer as associated types, but default associated types are unstable and would allow the user to override these types, which is not desirable and seems like the wrong abstraction. How can I provide the Runner with the enums MyServerRequest and MyServerResponse in a way that is transparent to the user?

Maybe have some trait with the associated types and implement that for MyServerImpl as well? I guess that would require you to add a macro to your impl MyServer for ServerImpl. I was thinking something like this:

trait Runnable {
    type Request;
    type Response;
}

struct Runner;

impl Runner {
    fn new<T: Runnable>(my_server: T) {}
}

are types provided by your library and

#[your_macro]
trait MyServer {
    fn say_hello(name: String);
}

// ---------
// Generates
// ---------

enum MyServerRequest { SayHello(String) }
enum MyServerResponse { SayHello }

while

struct MyServerImpl;

#[your_macro]
impl MyServer for MyServerImpl {
    fn say_hello(name: String) {}
}

// ---------
// Generates
// ---------

impl Runnable for MyServerImpl {
    type Request = MyServerRequest;
    type Response = MyServerResponse;
}

in your user code.

1 Like

That would work as far as I can tell, but I really wanted something a bit more transparent. Moreover I would like to avoid placing that proc macro invocation above the impl block, since it feels somewhat like leaking implementation details into user code.

One thing I considered was using a second trait with a blanket implementation, e.g.,

// defined in the library crate
trait Server {
    type Request;
    type Response;
}

and then:

// generated in user crate
impl<T> Server for T where T: MyServer {
    type Request = MyServerRequest;
    type Response = MyServerResponse;
}

In this case, Runner would be something like this:

struct Runner;

impl Runner {
    fn new<T: Server>(my_server: T) { /*... */ }
}

However, I think this did not work due to the orphan rule, e.g., Server is not defined in the user crate and hence cannot have a blanket implementation in that crate?

Yes, that would violate the orphan rules.

Indeed, and it would not make sense to generate the trait Server in the user crate since then Runner is unaware of it and would be unable to make use of the associated types...

And if you were to generate the enums from a struct's impl block, rather than a trait? Then you could implement Server directly for that struct as well:

struct MyServerImpl;

#[your_macro]
impl MyServerImpl {
    fn say_hello(name: String) {}
}

// ---------
// Generates
// ---------

enum MyServerRequest { SayHello(String) }
enum MyServerResponse { SayHello }

impl Server for MyServerImpl {
    type Request = MyServerRequest;
    type Response = MyServerResponse;
}

I would like to avoid defining the macro on the impl block since if I consider the full use case with the trait also being used to generate a client struct and implementation, it seems like it would get messy.

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.