Search code examples
rustconst-generics

How to workaround error `&'static str` is forbidden as the type of a const generic parameter?


I'm implementing an RPC system (the following code is simplified for illustration, it cannot be compiled, for a fully runnable example, please see this playground link):

trait RpcHandler<Req> {
    fn handle(&self, req: Req);
}

struct CommonRequest;

struct MyStruct;

impl RpcHandler<CommonRequest> for MyStruct {
    fn handle(&self, req: CommonRequest) {
        let rpc_method_name = get_rpc_method_name_from_rpc_context();
        match rpc_method_name {
            "foo" => self.foo(req),
            "bar" => self.bar(req),
            _ => (),
        }
    }
}

type HandlerMap = HashMap<String, Box<dyn RpcHandler>>;
static HANDLERS: HandlerMap;

fn register_rpc<Req>(method_name: &str, handler: &impl RpcHandler<Req>) {
    HANDLERS.insert(method_name, Box::new(handler));
}

fn on_rpc_call(buf: &[u8]) {
    let rpc_method_name: &str = decode_method_name_from_bytes(buf);
    let req = decode_request_struct_from_bytes(buf);
    let handler = HANDLERS.get(rpc_method_name).unwrap();
    handler.handle(req);
}

fn main() {
    let s = MyStruct;
    register_rpc("foo", &s);
    register_rpc("bar", &s);

    let buf: Vec<u8> = recv_some_bytes_from_network();
    on_rpc_call(&buf);
}

As you can see, I have actually two RPC methods foo and bar for MyStruct, both methods are registered as dynamic handlers. When a request is received, I decode the method name and request body from that, and find the proper handler by name, and then call that handler with decoded request body.

However, foo and bar share a same request type CommonRequest, so I can ONLY implement RpcHandler<CommonRequest> for MyStruct ONCE, so I have to write the code of foo and bar in the same implementation of RpcHandler<CommonRequest>. I think the more idiomatic way should be adding RPC method name to the trait, so I can implement RpcHandler for each RPC method (and request type) on MyStruct:

trait RpcHandler<const RpcMethodName: &'static str, Req> {
    fn handle(&self, req: Req);
}

impl RpcHandler<"foo", CommonRequest> for MyStruct {
    fn handle(&self, req: CommonRequest) {
        self.foo(req);
    }
}

impl RpcHandler<"bar", CommonRequest> for MyStruct {
    fn handle(&self, req: CommonRequest) {
        self.bar(req);
    }
}

However, the compiler complains that &'static str is forbidden as the type of a const generic parameter, any idiomatic/elegant workaround of that? Thanks.


Solution

  • The compiler forbids &str as a const generic, but I don't think it would actually help in your situation. Even if you used i32, which is allowed, you would need to make the actual call like so:

    trait RpcHandler<const Name: i32, Req> {
        fn handle(&self, req: Req);
    }
    
    // here `1` is used for the `const Name` type parameter
    <MyStruct as RpcHandler<1, _>>::handle(&x, 0);
    

    But this is a const generic, so you cannot e.g. pass a variable:

    let name = 1;
    <MyStruct as RpcHandler<name, _>>::handle(&x, 0);
    // error[E0435]: attempt to use a non-constant value in a constant
    

    So you have to still implement the actual dispatching logic, because (as expected) the type/const system cannot infer what values will flow into the program at runtime.

    Looking at your code, though, there are some things you can put into the traits rather than have to repeat throughout the code. Namely, you can associate RPC handler implementations with their name using an associated constant:

    trait RpcHandler<Req> {
        const NAME: &'static str;
        fn handle(&self, req: Req);
    }
    

    Each implementation must provide a value for NAME. Then, you can use the constant e.g. in the register method:

    fn register_rpc<Req: Default + 'static, T: RpcHandler<Req>>(
        handlers: &mut HandlerMap<T>,
    ) {
        let wrapper: MyWrapper<Req> = MyWrapper(PhantomData);
        handlers.insert(T::NAME.into(), Box::new(wrapper));
    }
    

    After some discussion in the comments: marker traits/types can be used to achieve something similar to what the const generics in the OP were meant to do. Concretely, we can define a marker trait, which does nothing except identify some types as being "methods":

    trait MethodName {}
    

    Then, some empty types will implement this trait:

    struct Foo;
    struct Bar;
    
    impl MethodName for Foo {}
    impl MethodName for Bar {}
    

    With this setup, type parameters can be constrained to MethodName to solve the problem of multiple implementations of the same trait for the same struct, but for different methods:

    trait RpcHandler<M: MethodName, Req> { ... }
    
    impl RpcHandler<Foo, CommonRequest> for MyStruct { ... }
    impl RpcHandler<Bar, CommonRequest> for MyStruct { ... }
    

    As a bonus, the MethodName trait can also contain a const NAME: &'static str;, if the implementation ever needs to reference the method name as a string.