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 the UnifiedType.
  • root_types: Optional. Contained in braces, it is an unordered, comma-terminated set of types that implement RootMessage.
  • 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 of AurumInterface.