Macro aurum_actors::unify [−][src]
unify!() { /* proc-macro */ }
Expand description
Creates a UnifiedType
and implements traits for it.
This macro is central to how Aurum
functions. Because we have actor interfaces, there
are multiple possible interpretations of a sequence of bytes. In order to deserialize messages
correctly, we need to know how to interpret the bytes. We need to know what type was serialized.
Information on what type the message is must be included within the message.
UnifiedType
instances are also used to safely deserialize
Destination
instances, which contain a
UnifiedType
instance. Generic type information does not serialize,
so if a Destination
is deserialized and interpreted with a
generic type, we have to make sure that the ActorId
and interface
match that generic type as well as match each other, to make sure we do not create a
Destination
that is invalid according to our type system.
Rust’s TypeId
is not serializable, and does not come with any guarantees
at all. We had to create our own system of reflection to fix this problem, but it is fairly easy
to use. The UnifiedType
created by this macro is an enum, whose
variant represent all possible root message types and actor interfaces usable in an application.
A type that is not in the UnifiedType
may not be used in your
application. Aurum
uses the Case
trait to enforce this restriction.
The end users must define a single UnifiedType
for their
application. DO NOT communicate between two Node
instances with different
types, things are bound to go wrong.
Case::VARIANT
can be used to create instances of its
UnifiedType
from type information without needing to access the
variants of that UnifiedType
, which are defined in the macro. This
is how ActorRef
and Destination
construct UnifiedType
instances for forging.
If you are writing a library built on top of
Aurum
, you can use Case
to restrict your user’s
UnifiedType
to make sure it is compatible with your messages.
Users are required to list your dependent message types in their invocation of
unify
. This is how cluster
would normally be implemented,
but the Case
bounds for UnifiedType
include
the dependent message types for cluster
for the sake on convenience.
use async_trait::async_trait; use aurum_actors::{unify, AurumInterface}; use aurum_actors::core::{Actor, ActorContext, Case, UnifiedType}; use im; use serde::{Serialize, Deserialize}; #[derive(AurumInterface, Serialize, Deserialize)] enum MsgTypeForSomeThirdPartyLibrary { #[aurum] Something(InterfaceForSomeThirdPartyLibrary) } #[derive(Serialize, Deserialize)] struct InterfaceForSomeThirdPartyLibrary; struct LibraryActor; #[async_trait] impl<U> Actor<U, MsgTypeForSomeThirdPartyLibrary> for LibraryActor where U: UnifiedType + Case<MsgTypeForSomeThirdPartyLibrary> + Case<InterfaceForSomeThirdPartyLibrary> { async fn recv( &mut self, ctx: &ActorContext<U, MsgTypeForSomeThirdPartyLibrary>, msg: MsgTypeForSomeThirdPartyLibrary ) { // logic } } #[derive(AurumInterface)] #[aurum(local)] enum MyMsgType { First, Second } #[derive(AurumInterface)] #[aurum(local)] enum MyOtherMsgType { Nonserializable(::std::fs::File), #[aurum] Serializable(InterfaceForSomeThirdPartyLibrary) } unify! { unified_name = pub MyUnifiedType; root_types = { MyMsgType, MyOtherMsgType, MsgTypeForSomeThirdPartyLibrary }; interfaces = { String, InterfaceForSomeThirdPartyLibrary }; }
The syntax for unify
is structured as an unordered set of key-value pairs
terminated by a semicolon. No key should be defined more than once. The keys are:
unified_name
: Required. Give an optional visibility and an identifier to name theUnifiedType
.root_types
: Optional. Contained in braces, it is an unordered, comma-terminated set of types that implementRootMessage
.interfaces
: Optional. Contained in braces, it is an unordered, comma-terminated set of types which are used as interfaces to root types. These are types annotated with#[aurum]
on invocations ofAurumInterface
.