Crate remote_trait_object

Source
Expand description

remote-trait-object is a general, powerful, and simple remote method invocation library based on trait objects.

It is…

  1. Based on services that can be exported and imported as trait objects - You register a service object, which is a trait object, and export it. On the other side, you import it into a proxy object, which is also a trait object.
  2. Based on a point-to-point connection - All operations are conducted upon a single connection, which has two ends.
  3. Easy to export and import services - During a remote method call in some service, you can export and import another service as an argument or a return value of the method.
  4. Independent from the transport model - The transport model is abstracted and users must provide a concrete implementation of it.
  5. Concurrent - you can both call and handle remote calls concurrently.

Note that it is commonly abbreviated as RTO.

§Introduction

Connection is a concept of a solid point-to-point pair where all operations of remote-trait-object are conducted.

Context is one end of a connection, and one connection will have two instances of this. Each side will access the connection via the context. This corresponds to Context.

Service is a set of well-defined, coherent operations that can be provided for use by other code. All communication between two parties takes place only through services.

Service object is the subject who provides a service. It is a trait object that is wrapped in a skeleton. The wrapping skeleton will invoke its method to handle remote calls from the other side. You can use any trait object as a service object, as long as the trait is a service trait. You can even export your proxy object as a service object.

Skeleton is a wrapper of a service object, which is registered on a context and will be invoked with remote calls from its proxy object from the client side. This corresponds to ServiceToExport or Skeleton.

Proxy object is the provided service. It is a trait object that is a proxy to the remote service object on the server side. You can call its methods just like a local object. A proxy object corresponds to exactly one skeleton, and vice versa. If a proxy object is dropped, it will request its deletion on the server side. This is called delete request. With this, the server side’s context will remove the skeleton if the client doesn’t own its proxy anymore.

Service trait is a trait that represents a service. It is for two trait objects (service object and proxy object). Both can share an identical service trait, but might have different but compatible service traits as well.

Server side refers to one side (or one context) in which the skeleton resides. When talking about service exchange, it is sometimes called the exporter side.

Client side refers to one side (or one context) in which the proxy object resides. When talking about service exchange, it is sometimes called the importer side.

Note that the concept of server and client are for one skeleton-proxy pair, not the whole context-context pair itself. No context is either server nor client by itself, but can be referred as one when we say a particular skeleton-proxy pair.

Handle is an index-like identifier that corresponds to a particular skeleton in the server side’s context. Each proxy object is carrying one. This corresponds to ServiceToImport or HandleToExchange.

Exporting a service object means wrapping it in a skeleton, registering that on the server side’s context, producing a handle to it, and passing the handle to the client side. Note that you won’t go through all these processes unless you’re using raw_exchange module.

Importing a handle into a proxy object means creating an object that fulfills its method calls by remotely invoking the skeleton on the server side. It carries the handle to fill it in the packet to send, which is for identifying the skeleton that this proxy corresponds to.

It is sometimes called an exchange when referring to both export and import.

§How It Works

diagram

  1. User calls a method of proxy object which is a trait object wrapped in a smart pointer.
  2. The call will be delivered to the context from which the proxy object is imported, after serialized into a byte packet. Note that the actual transportation of data happens only at the context, which functions as a connection end.
  3. The packet will be sent to the other end, (or context) by the transport.
  4. After the other side’s context receives the packet, it forwards the packet to the target skeleton in its registry.
  5. The skeleton will dispatch the packet into an actual method call to the service object, which is a trait object wrapped in a smart pointer.

The result will go back to the user, again via the contexts and transport.

§Smart Pointers

Both service object and proxy object are trait objects like dyn MyService. To own and pass a trait object, it should be holded by some smart pointer. Currently remote-trait-object supports three types of smart pointers for service objects and proxy objects.

  1. Box<dyn MyService>
  2. std::sync::Arc<dyn MyService>
  3. std::sync::Arc<parking_lot::RwLock<dyn MyService>>

When you export a service object, you can export from whichever type among them.

On the other hand when you import a proxy object, you can import into whichever type among them. Choosing smart pointer types is completely independent for exporting & importing sides. Both can decide which to use, depending on their own requirements.

Exporter (server)

  • Use Box<> when you have nothing to do with the object after you export it. It will be registered in the Context, and will be alive until the corresponding proxy object is dropped. You can never access the object directly, since it will be moved to the registry.

  • Use Arc<> when you have something to do with the object after you export it, by Arc::clone() and holding somewhere. In this case, both its proxy object and some Arc copy on the exporter side can access the object, though the latter can only access it immutably. With this a single service object can be shared among multiple skeletons while a skeleton always matches to exactly one service object.

  • Use Arc<RwLock<>> when you have to access the object mutably, in the similar situation with Arc case.

Importer (client)

  • This is not different from the plain Rust programming. Choose whichever type you want depending on your use.

Note that Arc<> will not be supported if the trait has a method that takes &mut self. You must use either Box<> or Arc<RwLock<>> in such casse.

§Service Trait

Service trait is the core idea of the remote-trait-object. Once you define a trait that you want to use it as a interface between two ends, you can put #[remote_trait_object::service] to make it as a service trait. It will generate all required code to construct a proxy and skeleton to it.

§Trait Requirements

There are some rules to use a trait as a service trait.

  1. Of course, the trait must be object-safe because it will be used as a trait object.

  2. It can’t have any type item.

  3. No generic parameter (including lifetime) is allowed, in both trait definition and methods.

  4. All types appeared in method parameter or return value must implement serde’s Serialize and Deserialize. This library performs de/serialization of data using serde, though the data format can be chosen. Depending on your choice of macro arguments, this condition may differ slightly. See this section

  5. You can’t return a reference as a return type. This holds for a composite type too. For example, you can’t return &i32 nor (i32, i32, &i32).

  6. You can pass only first-order reference as a parameter. For example, you can pass &T only if the T doesn’t a contain reference at all. Note that T must be Sized. There are two exceptions that accept ?Sized Ts: str and [U] where U doesn’t contain reference at all.

§Example

use remote_trait_object as rto;

#[remote_trait_object_macro::service]
pub trait PizzaStore : rto::Service {
    fn order_pizza(&mut self, menu: &str, money: u64);
    fn ask_pizza_price(&self, menu: &str) -> u64;
}

§Service Compatibility

Although it is common to use the same trait for both proxy object and service object, it is possible to import a service into another trait.

TODO: We have not strictly designed the compatibility model but will be provided in the next version.

Roughly, in current version, trait P is considered to be compatible to be proxy of trait S, only if

  1. P has exactly the same methods as S declared in the same order, that differ only in types of parameter and return value.
  2. Such different types must be compatible.
  3. Types are considered to be compatible if both are serialized and deserialized with exactly the same value.

remote-trait-object always guarantees 3. between ServiceToExport, ServiceToImport and ServiceRef.

§Export & Import services

One of the core features of remote-trait-object is its simple and straightforward but extensive export & import of services. Of course this library doesn’t make you manually register a service object, passing handle and so on, but provides you a much simpler and abstracted way.

There are three ways of exporting and importing a service.

§During Initialization

When you create new remote-trait-object contexts, you can export and import one as initial services. See details here

§As a Parameter or a Return Value

This is the most common way of exporting / importing services.

See ServiceToExport, ServiceToImport and ServiceRef for more.

§Raw Exchange

You will be rarely needed to perform a service exchange using a raw skeleton and handle. If you use this method, you will do basically the same thing as what the above methods would do internally, but have some extra controls over it. Raw exchange is not that frequently required. In most cases using only method 1. and 2. will be sufficient.

See the module-level documentation for more.

§Example

use remote_trait_object::*;

#[service]
pub trait CreditCard: Service {
    fn pay(&mut self, ammount: u64) -> Result<(), ()>;
}
struct SomeCreditCard { money: u64 }
impl CreditCard for SomeCreditCard {
    fn pay(&mut self, ammount: u64) -> Result<(), ()> {
        if ammount <= self.money {
            self.money -= ammount;
            Ok(())
        } else { Err(()) }
    }
}

#[service]
pub trait PizzaStore: Service {
    fn order_pizza(&self, credit_card: ServiceRef<dyn CreditCard>) -> Result<String, ()>;
}
struct SomePizzaStore;
impl PizzaStore for SomePizzaStore {
    fn order_pizza(&self, credit_card: ServiceRef<dyn CreditCard>) -> Result<String, ()> {
        let mut credit_card_proxy: Box<dyn CreditCard> = credit_card.unwrap_import().into_proxy();
        credit_card_proxy.pay(10)?;
        Ok("Tasty Pizza".to_owned())
    }
}

// PROGRAM 1
let (send, recv) = unimplemented!("Implement your own transport medium and provide here!")
let _context_pizza_town = Context::with_initial_service_export(
    Config::default_setup(), send, recv,
    ServiceToExport::new(Box::new(SomePizzaStore) as Box<dyn PizzaStore>),
);

// PROGRAM 2
let (send, recv) = unimplemented!("Implement your own transport medium and provide here!")
let (_context_customer, pizza_store): (_, ServiceToImport<dyn PizzaStore>) =
    Context::with_initial_service_import(Config::default_setup(), send, recv);
let pizza_store_proxy: Box<dyn PizzaStore> = pizza_store.into_proxy();

let my_credit_card = Box::new(SomeCreditCard {money: 11}) as Box<dyn CreditCard>;
assert_eq!(pizza_store_proxy.order_pizza(
    ServiceRef::create_export(my_credit_card)).unwrap(), "Tasty Pizza");

let my_credit_card = Box::new(SomeCreditCard {money: 9}) as Box<dyn CreditCard>;
assert!(pizza_store_proxy.order_pizza(
    ServiceRef::create_export(my_credit_card)).is_err());

You can check the working code of this example here.

See more examples here.

Modules§

  • This module is needed only if you want to perform some raw exchange (or export/import) of services.
  • Abstractions of a transport that carries out an actual communication for remote-trait-object.

Structs§

  • A configuration of a remote-trait-object context.
  • One end of a remote-trait-object connection.
  • A special wrapper of skeleton, used to export a service object.
  • A special wrapper of handle, used to import a service.

Enums§

Traits§

  • A serde de/serialization format that will be used for a service.
  • The Service trait is a marker that is used as a supertrait for a service trait, indicating that the trait is for a service.

Functions§

  • A special function that sets static & global identifiers for the methods.

Attribute Macros§

  • It generates all necessary helper structs that makes the trait be able to be used as a service.
  • This macro consumes the target trait, and will print the expanded code. Use this when you want to see the result of macro.