capnproto / capnproto-rust

Cap'n Proto for Rust
MIT License
2.08k stars 222 forks source link

Accessing the super interface using interface inheritance in capnp schema #146

Open john-hern opened 5 years ago

john-hern commented 5 years ago

Not sure if this is the best place to ask this question, however how exactly are we supposed to access the base interface when using interface inheritance? I've looked through the examples and the generated rust code for my interfaces but I dont see an obvious solution. Is this actually supported in the rust implementation?

dwrensha commented 5 years ago

To upcast a Client, you need to manually construct a new Client, like this:

https://github.com/capnproto/capnproto-rust/blob/96f2f7894fbbf55e741c39b183a824e27ddbcc3e/capnp-rpc/test/test.rs#L486

Notice there are no restrictions on what interface type you cast to. I.e. this doesn't only support upcasting. If you cast to something that the underlying object does not actually implement, you'll get "unimplemented" errors on method calls.

There are probably a lot of ways that we could make this better, but working on this has not been high on my priority list.


Another time you might want access to the base class is when you're implementing a Server -- you might want to call a method on a parent interface of Self. That's currently not supported. The plan for it is https://github.com/capnproto/capnproto-rust/issues/87.

john-hern commented 5 years ago

Thanks for such a quick reply! With this information I was able to unblock myself. Appreciate it.

pepijndevos commented 3 years ago

I'm running into this problem as well.

I'm trying to use https://github.com/NyanCAD/SimServer/blob/main/Simulator.capnp with this library. It has a Simulator(Cmd) interface that is generic with the interface it returns.

When using this from Python, lack of proper generics made me define concrete simulator interfaces that inherit from the generic ones. Concrete simulators use multiple inheritance to express the supported commands, like so

interface Xyce extends(Simulator(Run)) { }
interface NgspiceCommands extends(Run, Tran, Op, Ac) {}
interface Ngspice extends(Simulator(NgspiceCommands)) { }

In Rust I have the opposite problem, where it seems my Xyce and Ngspice client interfaces don't have any methods at all. So I think I can use the generic Simulator(Cmd) in Rust, but it's not clear how I can support multiple commands.

let sim: simulator::Client<tran::Owned> = rpc_system.bootstrap(rpc_twoparty_capnp::Side::Server)

This seems to be the way to instantiate a simulator supporting a single command. (I tried tran::Client first which gave confusing errors)

Would I have to do something like this every time I want to call a different command?

 let sim_run = simulator::Client { client: sim.clone().client };
Twey commented 3 years ago

The workaround suggested here currently doesn't work if the super interface has generic parameters, as capnproto-rust will add a private PhantomData member that prevents the Client from being constructed.

As a workaround for the workaround, you can go via FromClientHook::new:

use capnp::capability::FromClientHook as _;
let super_interface_client = schema_capnp::super_interface::Client::new(sub_interface_client.client.hook);
zenhack commented 3 years ago

Perhaps it would make this slightly more ergonomic to provide impls of From<sub_interface::Client> for super_interface::Client? That should at least let you just do sim.clone() instead of having to wrap & unwrap the client.

MatthiasEckhart commented 2 years ago

I think that I am running into a similar issue. I want to have one common implementation for the communication with the server that is shared across multiple sub clients.

The schema looks like this: engine.capnp:

interface BasePlugin {
....
}
interface EngineBasePluginRegistration {}

pluginone.capnp:

using Engine = import "engine.capnp";

interface PluginOne extends(Engine.BasePlugin) {

    registerPluginOne @0 (engine_plugin_one :EnginePluginOne) -> (registration :Engine.EngineBasePluginRegistration);

    interface EnginePluginOne extends(Engine.EngineBasePlugin) {
        bla @0 () -> (bla :Text);
    }

}

Now, the problem is that I don't know how to start the RPC server in such a way that I can handle incoming connections from different clients (e.g., plugin-one, plugin-two). If I use base_plugin::Client: let capnp_client: base_plugin::Client = capnp_rpc::new_client(CapnpMyImpl::new()); when creating the RPC system on the server-side, I get a "method not implemented" error when calling the registerPluginOne method (note that CapnpMyImpl implements base_plugin::Server and plugin_one::Server etc.). Of course, changing the type of capnp_client to plugin_one::Client will make it work for the register_plugin_one method, but not for other plugins. Is such a modular approach using interface inheritance actually supported? What could be an alternative (e.g., creating one RPC system for each plugin)?

dwrensha commented 2 years ago

It's difficult to provide advice about that plugin system without seeing more about how it's intended to be used.

Depending on what you're trying to accomplish, one thing that could help is having your main server interface be separated from plugins, like this:

interface PluginServer {
   loadPlugin @0 (pluginId : UInt64) -> (plugin : Engine.BasePlugin);
}

Then the called could downcast the returned plugin.

I don't know how to start the RPC server in such a way that I can handle incoming connections from different clients (e.g., plugin-one, plugin-two).

Do you statically know the list of all plugins that you want to support? Or does the server need to dynamically support new plugins that somehow get registered at run-time?

MatthiasEckhart commented 2 years ago

Thank you @dwrensha, I really appreciate your help.

It's difficult to provide advice about that plugin system without seeing more about how it's intended to be used.

Depending on what you're trying to accomplish, one thing that could help is having your main server interface be separated from plugins, like this:

interface PluginServer {
   loadPlugin @0 (pluginId : UInt64) -> (plugin : Engine.BasePlugin);
}

Then the called could downcast the returned plugin.

The idea is that I have one server and multiple plugins that are statically known by the server. Each of these plugins use a common implementation for managing the connection to the server, but call a different 'registration' function. So, when starting a plugin, the function for handling the communication with the server from the common dependency is called with a pointer to a function of the plugin that executes the concrete 'register plugin' remote method (i.e., the plugins initiate the connection to the server to register themselves).

I don't know how to start the RPC server in such a way that I can handle incoming connections from different clients (e.g., plugin-one, plugin-two).

Do you statically know the list of all plugins that you want to support? Or does the server need to dynamically support new plugins that somehow get registered at run-time?

Yes, the server statically knows the list of supported plugins (no need to dynamically support new plugins). As the plugins implement the Server traits of both engine_base_plugin and plugin_<one>::engine_plugin_<one> (e.g., for the first plugin) I think that the plugins should be able to handle invocations of remote methods correctly.

However, it seems that the problem is on the server side: Here, I only create a base_plugin::Client and not a client for a concrete plugin (e.g., plugin_one::Client), leading to the issue that I described above. I expected that accepting connections from these 'generic' clients (interfaces of concrete plugins extend this base plugin interface), would also allow me to handle invocations of remote methods for registering concrete plugins (i.e., the server implements the plugin_<one>::Server traits).

dwrensha commented 2 years ago

So it sounds like your server implements all plugin interfaces? One thing that might work is to define a new interface that extends all of the plugin interfaces:

interface AllPlugins extends (PluginOne, PluginTwo) {}
MatthiasEckhart commented 2 years ago

So it sounds like your server implements all plugin interfaces? One thing that might work is to define a new interface that extends all of the plugin interfaces:

interface AllPlugins extends (PluginOne, PluginTwo) {}

Thank you so much! That solved my problem.

tamird commented 1 year ago

Is it still recommended practice to manually cast clients to the base interface?

interface Foo {
  getInt @0 () -> (i :UInt64);
}

interface Bar extends(Foo) {}

a Client<Bar> seems to not have any of Foo's methods.

dwrensha commented 1 year ago

Is it still recommended practice to manually cast clients to the base interface?

Nothing has changed since my previous comment https://github.com/capnproto/capnproto-rust/issues/146#issuecomment-532205453

dwrensha commented 2 months ago

https://github.com/capnproto/capnproto-rust/pull/300 made it more convenient to cast between interface client types.

tamird commented 2 months ago

300 made it more convenient to cast between interface client types.

    /// Casts `self` to another instance of `FromClientHook`. This always succeeds,
    /// but if the underlying capability does not actually implement `T`'s interface,
    /// then method calls will fail with "unimplemented" errors.

it would still be good to have a separate conversion path that is available only when the underlying client is statically known to implement T's interface.