Expand description
remote-trait-object
is a general, powerful, and simple remote method invocation library
based on trait objects.
It is…
- 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.
- Based on a point-to-point connection - All operations are conducted upon a single connection, which has two ends.
- 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.
- Independent from the transport model - The transport model is abstracted and users must provide a concrete implementation of it.
- 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
- User calls a method of proxy object which is a trait object wrapped in a smart pointer.
- 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.
- The packet will be sent to the other end, (or context) by the transport.
- After the other side’s context receives the packet, it forwards the packet to the target skeleton in its registry.
- 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.
Box<dyn MyService>
std::sync::Arc<dyn MyService>
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 theContext
, 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, byArc::clone()
and holding somewhere. In this case, both its proxy object and someArc
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 withArc
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.
-
Of course, the trait must be object-safe because it will be used as a trait object.
-
It can’t have any type item.
-
No generic parameter (including lifetime) is allowed, in both trait definition and methods.
-
All types appeared in method parameter or return value must implement
serde
’sSerialize
andDeserialize
. This library performs de/serialization of data usingserde
, though the data format can be chosen. Depending on your choice of macro arguments, this condition may differ slightly. See this section -
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)
. -
You can pass only first-order reference as a parameter. For example, you can pass
&T
only if theT
doesn’t a contain reference at all. Note that T must beSized
. There are two exceptions that accept?Sized
T
s:str
and[U]
whereU
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
P
has exactly the same methods asS
declared in the same order, that differ only in types of parameter and return value.- Such different types must be compatible.
- 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§
- A union of
ServiceToExport
andServiceToImport
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
struct
s 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.