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.
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.